@atproto/lex-schema 0.0.17 → 0.0.18
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 +11 -0
- package/dist/schema/blob.d.ts +6 -20
- package/dist/schema/blob.d.ts.map +1 -1
- package/dist/schema/blob.js +23 -28
- package/dist/schema/blob.js.map +1 -1
- package/package.json +3 -3
- package/src/schema/blob.test.ts +223 -263
- package/src/schema/blob.ts +27 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @atproto/lex-schema
|
|
2
2
|
|
|
3
|
+
## 0.0.18
|
|
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), [`c62651d`](https://github.com/bluesky-social/atproto/commit/c62651dd69f1e18bd854b66e499b91fee9eaa856), [`c62651d`](https://github.com/bluesky-social/atproto/commit/c62651dd69f1e18bd854b66e499b91fee9eaa856)]:
|
|
12
|
+
- @atproto/lex-data@0.0.15
|
|
13
|
+
|
|
3
14
|
## 0.0.17
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/dist/schema/blob.d.ts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import { BlobRef, LegacyBlobRef, isBlobRef, isLegacyBlobRef } from '@atproto/lex-data';
|
|
1
|
+
import { BlobRef, LegacyBlobRef, TypedBlobRef, isBlobRef, isLegacyBlobRef, isTypedBlobRef } from '@atproto/lex-data';
|
|
2
2
|
import { Schema, ValidationContext } from '../core.js';
|
|
3
3
|
/**
|
|
4
4
|
* Configuration options for blob schema validation.
|
|
5
5
|
*/
|
|
6
6
|
export type BlobSchemaOptions = {
|
|
7
|
-
/**
|
|
8
|
-
* Whether to allow legacy blob references format
|
|
9
|
-
*
|
|
10
|
-
* @default false
|
|
11
|
-
* @see {@link LegacyBlobRef}
|
|
12
|
-
*/
|
|
13
|
-
allowLegacy?: boolean;
|
|
14
7
|
/**
|
|
15
8
|
* List of accepted MIME types (supports wildcards like 'image/*' or '*\/*')
|
|
16
9
|
*
|
|
@@ -24,8 +17,8 @@ export type BlobSchemaOptions = {
|
|
|
24
17
|
*/
|
|
25
18
|
maxSize?: number;
|
|
26
19
|
};
|
|
27
|
-
export type { BlobRef, LegacyBlobRef };
|
|
28
|
-
export { isBlobRef, isLegacyBlobRef };
|
|
20
|
+
export type { BlobRef, LegacyBlobRef, TypedBlobRef };
|
|
21
|
+
export { isBlobRef, isLegacyBlobRef, isTypedBlobRef };
|
|
29
22
|
/**
|
|
30
23
|
* Schema for validating blob references in AT Protocol.
|
|
31
24
|
*
|
|
@@ -41,13 +34,11 @@ export { isBlobRef, isLegacyBlobRef };
|
|
|
41
34
|
* const result = schema.validate(blobRef)
|
|
42
35
|
* ```
|
|
43
36
|
*/
|
|
44
|
-
export declare class BlobSchema<const TOptions extends BlobSchemaOptions = NonNullable<unknown>> extends Schema<
|
|
45
|
-
allowLegacy: true;
|
|
46
|
-
} ? BlobRef | LegacyBlobRef : BlobRef> {
|
|
37
|
+
export declare class BlobSchema<const TOptions extends BlobSchemaOptions = NonNullable<unknown>> extends Schema<BlobRef> {
|
|
47
38
|
readonly options?: TOptions | undefined;
|
|
48
39
|
readonly type: "blob";
|
|
49
40
|
constructor(options?: TOptions | undefined);
|
|
50
|
-
validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").ValidationResult<
|
|
41
|
+
validateInContext(input: unknown, ctx: ValidationContext): import("../core.js").ValidationResult<BlobRef>;
|
|
51
42
|
matchesMime(mime: string): boolean;
|
|
52
43
|
}
|
|
53
44
|
/**
|
|
@@ -69,12 +60,7 @@ export declare class BlobSchema<const TOptions extends BlobSchemaOptions = NonNu
|
|
|
69
60
|
*
|
|
70
61
|
* // Any image type with size limit
|
|
71
62
|
* const avatarSchema = l.blob({ accept: ['image/*'], maxSize: 1000000 })
|
|
72
|
-
*
|
|
73
|
-
* // Allow legacy format
|
|
74
|
-
* const legacySchema = l.blob({ allowLegacy: true })
|
|
75
63
|
* ```
|
|
76
64
|
*/
|
|
77
|
-
export declare const blob: <O extends BlobSchemaOptions = {
|
|
78
|
-
allowLegacy?: false;
|
|
79
|
-
}>(options?: O) => BlobSchema<O>;
|
|
65
|
+
export declare const blob: <O extends BlobSchemaOptions = {}>(options?: O) => BlobSchema<O>;
|
|
80
66
|
//# sourceMappingURL=blob.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../src/schema/blob.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,aAAa,EACb,SAAS,EACT,eAAe,
|
|
1
|
+
{"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../src/schema/blob.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,aAAa,EACb,YAAY,EAEZ,SAAS,EACT,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAGtD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,CAAA;AAErD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,UAAU,CACrB,KAAK,CAAC,QAAQ,SAAS,iBAAiB,GAAG,WAAW,CAAC,OAAO,CAAC,CAC/D,SAAQ,MAAM,CAAC,OAAO,CAAC;IAGX,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ;IAFvC,QAAQ,CAAC,IAAI,EAAG,MAAM,CAAS;gBAEV,OAAO,CAAC,EAAE,QAAQ,YAAA;IAIvC,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,iBAAiB;IA8BxD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAKnC;AA+BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,IAAI,GACf,CAAC,SAAS,iBAAiB,iBACjB,CAAC,kBAEX,CAAA"}
|
package/dist/schema/blob.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.blob = exports.BlobSchema = exports.isLegacyBlobRef = exports.isBlobRef = void 0;
|
|
3
|
+
exports.blob = exports.BlobSchema = exports.isTypedBlobRef = exports.isLegacyBlobRef = exports.isBlobRef = void 0;
|
|
4
4
|
const lex_data_1 = require("@atproto/lex-data");
|
|
5
5
|
Object.defineProperty(exports, "isBlobRef", { enumerable: true, get: function () { return lex_data_1.isBlobRef; } });
|
|
6
6
|
Object.defineProperty(exports, "isLegacyBlobRef", { enumerable: true, get: function () { return lex_data_1.isLegacyBlobRef; } });
|
|
7
|
+
Object.defineProperty(exports, "isTypedBlobRef", { enumerable: true, get: function () { return lex_data_1.isTypedBlobRef; } });
|
|
7
8
|
const core_js_1 = require("../core.js");
|
|
8
9
|
const memoize_js_1 = require("../util/memoize.js");
|
|
9
10
|
/**
|
|
@@ -29,20 +30,28 @@ class BlobSchema extends core_js_1.Schema {
|
|
|
29
30
|
this.options = options;
|
|
30
31
|
}
|
|
31
32
|
validateInContext(input, ctx) {
|
|
32
|
-
const blob = parseValue.call(ctx, input
|
|
33
|
+
const blob = parseValue.call(ctx, input);
|
|
33
34
|
if (!blob) {
|
|
34
35
|
return ctx.issueUnexpectedType(input, 'blob');
|
|
35
36
|
}
|
|
36
37
|
// In non-strict mode, we allow blob refs to pass through without MIME
|
|
37
38
|
// type or size checks.
|
|
38
|
-
if (ctx.options.strict) {
|
|
39
|
-
const accept = this.options
|
|
39
|
+
if (ctx.options.strict && this.options != null) {
|
|
40
|
+
const { accept } = this.options;
|
|
40
41
|
if (accept && !matchesMime(blob.mimeType, accept)) {
|
|
41
42
|
return ctx.issueInvalidPropertyValue(blob, 'mimeType', accept);
|
|
42
43
|
}
|
|
43
|
-
const maxSize = this.options
|
|
44
|
-
if (maxSize != null
|
|
45
|
-
|
|
44
|
+
const { maxSize } = this.options;
|
|
45
|
+
if (maxSize != null) {
|
|
46
|
+
const size = (0, lex_data_1.getBlobSize)(blob);
|
|
47
|
+
if (size === undefined) {
|
|
48
|
+
// Unable to enforce size constraint if size is not available (legacy
|
|
49
|
+
// blob ref), so we treat it as a validation failure in strict mode.
|
|
50
|
+
return ctx.issueInvalidPropertyType(blob, 'size', 'integer');
|
|
51
|
+
}
|
|
52
|
+
else if (size > maxSize) {
|
|
53
|
+
return ctx.issueTooBig(blob, 'blob', maxSize, size);
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
return ctx.success(blob);
|
|
@@ -55,30 +64,19 @@ class BlobSchema extends core_js_1.Schema {
|
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
exports.BlobSchema = BlobSchema;
|
|
58
|
-
function parseValue(input
|
|
59
|
-
// If there is a $type property, we treat if as a potential
|
|
67
|
+
function parseValue(input) {
|
|
68
|
+
// If there is a $type property, we treat if as a potential TypedBlobRef and
|
|
60
69
|
// validate accordingly.
|
|
61
70
|
if (input?.$type !== undefined) {
|
|
62
71
|
// Use the context's option for the "strict" check
|
|
63
|
-
return (0, lex_data_1.
|
|
72
|
+
return (0, lex_data_1.isTypedBlobRef)(input, this.options) ? input : null;
|
|
64
73
|
}
|
|
65
74
|
// If there is no $type property, we may be dealing with a legacy blob ref. If
|
|
66
|
-
// legacy refs are allowed,
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (options?.allowLegacy) {
|
|
71
|
-
if ((0, lex_data_1.isLegacyBlobRef)(input)) {
|
|
75
|
+
// legacy refs are allowed (non-strict mode), we check if the input matches
|
|
76
|
+
// the legacy format.
|
|
77
|
+
if (!this.options.strict) {
|
|
78
|
+
if ((0, lex_data_1.isLegacyBlobRef)(input, this.options))
|
|
72
79
|
return input;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (!this.options.strict && this.options.mode === 'parse') {
|
|
76
|
-
if ((0, lex_data_1.isLegacyBlobRef)(input)) {
|
|
77
|
-
const { cid, mimeType } = input;
|
|
78
|
-
const ref = (0, lex_data_1.parseCidSafe)(cid);
|
|
79
|
-
if (ref)
|
|
80
|
-
return { $type: 'blob', ref, mimeType, size: -1 };
|
|
81
|
-
}
|
|
82
80
|
}
|
|
83
81
|
return null;
|
|
84
82
|
}
|
|
@@ -113,9 +111,6 @@ function matchesMime(mime, accepted) {
|
|
|
113
111
|
*
|
|
114
112
|
* // Any image type with size limit
|
|
115
113
|
* const avatarSchema = l.blob({ accept: ['image/*'], maxSize: 1000000 })
|
|
116
|
-
*
|
|
117
|
-
* // Allow legacy format
|
|
118
|
-
* const legacySchema = l.blob({ allowLegacy: true })
|
|
119
114
|
* ```
|
|
120
115
|
*/
|
|
121
116
|
exports.blob = (0, memoize_js_1.memoizedOptions)(function (options) {
|
package/dist/schema/blob.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob.js","sourceRoot":"","sources":["../../src/schema/blob.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"blob.js","sourceRoot":"","sources":["../../src/schema/blob.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BlobRef,\n LegacyBlobRef,\n TypedBlobRef,\n getBlobSize,\n isBlobRef,\n isLegacyBlobRef,\n isTypedBlobRef,\n} from '@atproto/lex-data'\nimport { Schema, ValidationContext } from '../core.js'\nimport { memoizedOptions } from '../util/memoize.js'\n\n/**\n * Configuration options for blob schema validation.\n */\nexport type BlobSchemaOptions = {\n /**\n * List of accepted MIME types (supports wildcards like 'image/*' or '*\\/*')\n *\n * @default undefined // accepts all MIME types\n */\n accept?: string[]\n\n /**\n * Maximum blob size in bytes\n *\n * @default undefined // no size limit\n */\n maxSize?: number\n}\n\nexport type { BlobRef, LegacyBlobRef, TypedBlobRef }\nexport { isBlobRef, isLegacyBlobRef, isTypedBlobRef }\n\n/**\n * Schema for validating blob references in AT Protocol.\n *\n * Validates BlobRef objects which contain a CID reference to binary data,\n * along with metadata like MIME type and size. Can optionally accept\n * legacy blob reference format.\n *\n * @template TOptions - The configuration options type\n *\n * @example\n * ```ts\n * const schema = new BlobSchema({ accept: ['image/*'], maxSize: 1000000 })\n * const result = schema.validate(blobRef)\n * ```\n */\nexport class BlobSchema<\n const TOptions extends BlobSchemaOptions = NonNullable<unknown>,\n> extends Schema<BlobRef> {\n readonly type = 'blob' as const\n\n constructor(readonly options?: TOptions) {\n super()\n }\n\n validateInContext(input: unknown, ctx: ValidationContext) {\n const blob = parseValue.call(ctx, input)\n if (!blob) {\n return ctx.issueUnexpectedType(input, 'blob')\n }\n\n // In non-strict mode, we allow blob refs to pass through without MIME\n // type or size checks.\n if (ctx.options.strict && this.options != null) {\n const { accept } = this.options\n if (accept && !matchesMime(blob.mimeType, accept)) {\n return ctx.issueInvalidPropertyValue(blob, 'mimeType', accept)\n }\n\n const { maxSize } = this.options\n if (maxSize != null) {\n const size = getBlobSize(blob)\n if (size === undefined) {\n // Unable to enforce size constraint if size is not available (legacy\n // blob ref), so we treat it as a validation failure in strict mode.\n return ctx.issueInvalidPropertyType(blob, 'size' as any, 'integer')\n } else if (size > maxSize) {\n return ctx.issueTooBig(blob, 'blob', maxSize, size)\n }\n }\n }\n\n return ctx.success(blob)\n }\n\n matchesMime(mime: string): boolean {\n const accept = this.options?.accept\n if (!accept) return true\n return matchesMime(mime, accept)\n }\n}\n\nfunction parseValue(this: ValidationContext, input: unknown): BlobRef | null {\n // If there is a $type property, we treat if as a potential TypedBlobRef and\n // validate accordingly.\n if ((input as any)?.$type !== undefined) {\n // Use the context's option for the \"strict\" check\n return isTypedBlobRef(input, this.options) ? input : null\n }\n\n // If there is no $type property, we may be dealing with a legacy blob ref. If\n // legacy refs are allowed (non-strict mode), we check if the input matches\n // the legacy format.\n if (!this.options.strict) {\n if (isLegacyBlobRef(input, this.options)) return input\n }\n\n return null\n}\n\nfunction matchesMime(mime: string, accepted: string[]): boolean {\n if (accepted.includes('*/*')) return true\n if (accepted.includes(mime)) return true\n for (const value of accepted) {\n if (value.endsWith('/*') && mime.startsWith(value.slice(0, -1))) {\n return true\n }\n }\n return false\n}\n\n/**\n * Creates a blob schema for validating blob references with optional constraints.\n *\n * Blob references are used in AT Protocol to reference binary data stored\n * separately from records. They contain a CID, MIME type, and size information.\n *\n * @param options - Optional configuration for MIME type filtering and size limits\n * @returns A new {@link BlobSchema} instance\n *\n * @example\n * ```ts\n * // Basic blob reference\n * const fileSchema = l.blob()\n *\n * // Image files only\n * const imageSchema = l.blob({ accept: ['image/png', 'image/jpeg', 'image/gif'] })\n *\n * // Any image type with size limit\n * const avatarSchema = l.blob({ accept: ['image/*'], maxSize: 1000000 })\n * ```\n */\nexport const blob = /*#__PURE__*/ memoizedOptions(function <\n O extends BlobSchemaOptions = NonNullable<unknown>,\n>(options?: O) {\n return new BlobSchema(options)\n})\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex-schema",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Lexicon schema system for AT Lexicons",
|
|
6
6
|
"keywords": [
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@standard-schema/spec": "^1.1.0",
|
|
39
39
|
"iso-datestring-validator": "^2.2.2",
|
|
40
40
|
"tslib": "^2.8.1",
|
|
41
|
-
"@atproto/syntax": "^0.5.
|
|
42
|
-
"@atproto/lex-data": "^0.0.
|
|
41
|
+
"@atproto/syntax": "^0.5.3",
|
|
42
|
+
"@atproto/lex-data": "^0.0.15"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"vitest": "^4.0.16"
|
package/src/schema/blob.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assert, describe, expect, it } from 'vitest'
|
|
2
|
-
import { parseCid } from '@atproto/lex-data'
|
|
2
|
+
import { isLegacyBlobRef, isTypedBlobRef, parseCid } from '@atproto/lex-data'
|
|
3
3
|
import { blob } from './blob.js'
|
|
4
4
|
|
|
5
5
|
// await cidForRawBytes(Buffer.from('Hello, World!'))
|
|
@@ -22,12 +22,11 @@ describe('BlobSchema', () => {
|
|
|
22
22
|
mimeType: 'image/jpeg',
|
|
23
23
|
size: 10000,
|
|
24
24
|
})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
25
|
+
assert(result.success)
|
|
26
|
+
assert(isTypedBlobRef(result.value))
|
|
27
|
+
expect(result.value.$type).toBe('blob')
|
|
28
|
+
expect(result.value.mimeType).toBe('image/jpeg')
|
|
29
|
+
expect(result.value.size).toBe(10000)
|
|
31
30
|
})
|
|
32
31
|
|
|
33
32
|
it('validates blob with different mime types', () => {
|
|
@@ -37,7 +36,7 @@ describe('BlobSchema', () => {
|
|
|
37
36
|
mimeType: 'image/png',
|
|
38
37
|
size: 5000,
|
|
39
38
|
})
|
|
40
|
-
|
|
39
|
+
assert(result.success)
|
|
41
40
|
})
|
|
42
41
|
|
|
43
42
|
it('validates blob with size 0', () => {
|
|
@@ -47,37 +46,37 @@ describe('BlobSchema', () => {
|
|
|
47
46
|
mimeType: 'text/plain',
|
|
48
47
|
size: 0,
|
|
49
48
|
})
|
|
50
|
-
|
|
49
|
+
assert(result.success)
|
|
51
50
|
})
|
|
52
51
|
|
|
53
52
|
it('rejects non-objects', () => {
|
|
54
53
|
const result = schema.safeParse('not an object')
|
|
55
|
-
|
|
54
|
+
assert(!result.success)
|
|
56
55
|
})
|
|
57
56
|
|
|
58
57
|
it('rejects null', () => {
|
|
59
58
|
const result = schema.safeParse(null)
|
|
60
|
-
|
|
59
|
+
assert(!result.success)
|
|
61
60
|
})
|
|
62
61
|
|
|
63
62
|
it('rejects undefined', () => {
|
|
64
63
|
const result = schema.safeParse(undefined)
|
|
65
|
-
|
|
64
|
+
assert(!result.success)
|
|
66
65
|
})
|
|
67
66
|
|
|
68
67
|
it('rejects arrays', () => {
|
|
69
68
|
const result = schema.safeParse([])
|
|
70
|
-
|
|
69
|
+
assert(!result.success)
|
|
71
70
|
})
|
|
72
71
|
|
|
73
72
|
it('rejects numbers', () => {
|
|
74
73
|
const result = schema.safeParse(123)
|
|
75
|
-
|
|
74
|
+
assert(!result.success)
|
|
76
75
|
})
|
|
77
76
|
|
|
78
77
|
it('rejects booleans', () => {
|
|
79
78
|
const result = schema.safeParse(true)
|
|
80
|
-
|
|
79
|
+
assert(!result.success)
|
|
81
80
|
})
|
|
82
81
|
})
|
|
83
82
|
|
|
@@ -90,7 +89,7 @@ describe('BlobSchema', () => {
|
|
|
90
89
|
mimeType: 'image/jpeg',
|
|
91
90
|
size: 10000,
|
|
92
91
|
})
|
|
93
|
-
|
|
92
|
+
assert(!result.success)
|
|
94
93
|
})
|
|
95
94
|
|
|
96
95
|
it('rejects blob with wrong $type', () => {
|
|
@@ -100,7 +99,7 @@ describe('BlobSchema', () => {
|
|
|
100
99
|
mimeType: 'image/jpeg',
|
|
101
100
|
size: 10000,
|
|
102
101
|
})
|
|
103
|
-
|
|
102
|
+
assert(!result.success)
|
|
104
103
|
})
|
|
105
104
|
|
|
106
105
|
it('rejects blob without ref', () => {
|
|
@@ -109,7 +108,7 @@ describe('BlobSchema', () => {
|
|
|
109
108
|
mimeType: 'image/jpeg',
|
|
110
109
|
size: 10000,
|
|
111
110
|
})
|
|
112
|
-
|
|
111
|
+
assert(!result.success)
|
|
113
112
|
})
|
|
114
113
|
|
|
115
114
|
it('rejects blob without mimeType', () => {
|
|
@@ -118,7 +117,7 @@ describe('BlobSchema', () => {
|
|
|
118
117
|
ref: blobCid,
|
|
119
118
|
size: 10000,
|
|
120
119
|
})
|
|
121
|
-
|
|
120
|
+
assert(!result.success)
|
|
122
121
|
})
|
|
123
122
|
|
|
124
123
|
it('rejects blob without size', () => {
|
|
@@ -127,7 +126,7 @@ describe('BlobSchema', () => {
|
|
|
127
126
|
ref: blobCid,
|
|
128
127
|
mimeType: 'image/jpeg',
|
|
129
128
|
})
|
|
130
|
-
|
|
129
|
+
assert(!result.success)
|
|
131
130
|
})
|
|
132
131
|
|
|
133
132
|
it('rejects blob with invalid ref type', () => {
|
|
@@ -137,7 +136,7 @@ describe('BlobSchema', () => {
|
|
|
137
136
|
mimeType: 'image/jpeg',
|
|
138
137
|
size: 10000,
|
|
139
138
|
})
|
|
140
|
-
|
|
139
|
+
assert(!result.success)
|
|
141
140
|
})
|
|
142
141
|
|
|
143
142
|
it('rejects blob with invalid mimeType type', () => {
|
|
@@ -147,7 +146,7 @@ describe('BlobSchema', () => {
|
|
|
147
146
|
mimeType: 123,
|
|
148
147
|
size: 10000,
|
|
149
148
|
})
|
|
150
|
-
|
|
149
|
+
assert(!result.success)
|
|
151
150
|
})
|
|
152
151
|
|
|
153
152
|
it('rejects blob with invalid size type', () => {
|
|
@@ -157,7 +156,7 @@ describe('BlobSchema', () => {
|
|
|
157
156
|
mimeType: 'image/jpeg',
|
|
158
157
|
size: '10000',
|
|
159
158
|
})
|
|
160
|
-
|
|
159
|
+
assert(!result.success)
|
|
161
160
|
})
|
|
162
161
|
|
|
163
162
|
it('rejects blob with negative size', () => {
|
|
@@ -167,7 +166,7 @@ describe('BlobSchema', () => {
|
|
|
167
166
|
mimeType: 'image/jpeg',
|
|
168
167
|
size: -1,
|
|
169
168
|
})
|
|
170
|
-
|
|
169
|
+
assert(!result.success)
|
|
171
170
|
})
|
|
172
171
|
|
|
173
172
|
it('rejects blob with decimal size', () => {
|
|
@@ -177,7 +176,7 @@ describe('BlobSchema', () => {
|
|
|
177
176
|
mimeType: 'image/jpeg',
|
|
178
177
|
size: 10000.5,
|
|
179
178
|
})
|
|
180
|
-
|
|
179
|
+
assert(!result.success)
|
|
181
180
|
})
|
|
182
181
|
|
|
183
182
|
it('rejects blob with extra properties', () => {
|
|
@@ -188,7 +187,7 @@ describe('BlobSchema', () => {
|
|
|
188
187
|
size: 10000,
|
|
189
188
|
extra: 'not allowed',
|
|
190
189
|
})
|
|
191
|
-
|
|
190
|
+
assert(!result.success)
|
|
192
191
|
})
|
|
193
192
|
|
|
194
193
|
it('rejects blob with $link format for ref', () => {
|
|
@@ -198,7 +197,7 @@ describe('BlobSchema', () => {
|
|
|
198
197
|
mimeType: 'image/jpeg',
|
|
199
198
|
size: 10000,
|
|
200
199
|
})
|
|
201
|
-
|
|
200
|
+
assert(!result.success)
|
|
202
201
|
})
|
|
203
202
|
|
|
204
203
|
it('rejects blob with unknown properties', () => {
|
|
@@ -209,7 +208,7 @@ describe('BlobSchema', () => {
|
|
|
209
208
|
size: 10000,
|
|
210
209
|
unknownProp: 42,
|
|
211
210
|
})
|
|
212
|
-
|
|
211
|
+
assert(!result.success)
|
|
213
212
|
})
|
|
214
213
|
})
|
|
215
214
|
|
|
@@ -226,7 +225,7 @@ describe('BlobSchema', () => {
|
|
|
226
225
|
},
|
|
227
226
|
{ strict: true },
|
|
228
227
|
)
|
|
229
|
-
|
|
228
|
+
assert(result.success)
|
|
230
229
|
})
|
|
231
230
|
|
|
232
231
|
it('rejects non-raw CID in strict mode', () => {
|
|
@@ -239,7 +238,7 @@ describe('BlobSchema', () => {
|
|
|
239
238
|
},
|
|
240
239
|
{ strict: true },
|
|
241
240
|
)
|
|
242
|
-
|
|
241
|
+
assert(!result.success)
|
|
243
242
|
})
|
|
244
243
|
|
|
245
244
|
it('accepts non-raw CID in non-strict mode', () => {
|
|
@@ -252,107 +251,148 @@ describe('BlobSchema', () => {
|
|
|
252
251
|
},
|
|
253
252
|
{ strict: false },
|
|
254
253
|
)
|
|
255
|
-
expect(result.success).toBe(true)
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('coerces legacy blob format in non-strict parse mode', () => {
|
|
259
|
-
const result = schema.safeParse(
|
|
260
|
-
{
|
|
261
|
-
cid: lexCid.toString(),
|
|
262
|
-
mimeType: 'image/jpeg',
|
|
263
|
-
},
|
|
264
|
-
{ strict: false },
|
|
265
|
-
)
|
|
266
254
|
assert(result.success)
|
|
267
|
-
expect(result.value).toEqual({
|
|
268
|
-
$type: 'blob',
|
|
269
|
-
ref: lexCid,
|
|
270
|
-
mimeType: 'image/jpeg',
|
|
271
|
-
size: -1,
|
|
272
|
-
})
|
|
273
255
|
})
|
|
274
256
|
})
|
|
275
257
|
|
|
276
258
|
describe('legacy blob format', () => {
|
|
277
|
-
it('rejects legacy format by default', () => {
|
|
259
|
+
it('rejects legacy format by default (strict mode)', () => {
|
|
278
260
|
const schema = blob({})
|
|
279
|
-
const
|
|
261
|
+
const parseResult = schema.safeParse({
|
|
280
262
|
cid: blobCid.toString(),
|
|
281
263
|
mimeType: 'image/jpeg',
|
|
282
264
|
})
|
|
283
|
-
|
|
284
|
-
})
|
|
265
|
+
assert(!parseResult.success)
|
|
285
266
|
|
|
286
|
-
|
|
287
|
-
const schema = blob({ allowLegacy: true })
|
|
288
|
-
const result = schema.safeParse({
|
|
267
|
+
const validateResult = schema.safeValidate({
|
|
289
268
|
cid: blobCid.toString(),
|
|
290
269
|
mimeType: 'image/jpeg',
|
|
291
270
|
})
|
|
292
|
-
|
|
293
|
-
if (result.success) {
|
|
294
|
-
expect('cid' in result.value && result.value.cid).toBe(
|
|
295
|
-
blobCid.toString(),
|
|
296
|
-
)
|
|
297
|
-
expect(result.value.mimeType).toBe('image/jpeg')
|
|
298
|
-
}
|
|
271
|
+
assert(!validateResult.success)
|
|
299
272
|
})
|
|
300
273
|
|
|
301
|
-
it('
|
|
302
|
-
const schema = blob({
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
274
|
+
it('rejects legacy format when strict: true is explicit', () => {
|
|
275
|
+
const schema = blob({})
|
|
276
|
+
const parseResult = schema.safeParse(
|
|
277
|
+
{
|
|
278
|
+
cid: blobCid.toString(),
|
|
279
|
+
mimeType: 'image/jpeg',
|
|
280
|
+
},
|
|
281
|
+
{ strict: true },
|
|
282
|
+
)
|
|
283
|
+
assert(!parseResult.success)
|
|
284
|
+
|
|
285
|
+
const validateResult = schema.safeValidate(
|
|
286
|
+
{
|
|
287
|
+
cid: blobCid.toString(),
|
|
288
|
+
mimeType: 'image/jpeg',
|
|
289
|
+
},
|
|
290
|
+
{ strict: true },
|
|
291
|
+
)
|
|
292
|
+
assert(!validateResult.success)
|
|
308
293
|
})
|
|
309
294
|
|
|
310
|
-
it('
|
|
311
|
-
const schema = blob({
|
|
312
|
-
const
|
|
295
|
+
it('accepts legacy format with strict: false in both parse and validate', () => {
|
|
296
|
+
const schema = blob({})
|
|
297
|
+
const parseResult = schema.safeParse(
|
|
298
|
+
{
|
|
299
|
+
cid: blobCid.toString(),
|
|
300
|
+
mimeType: 'image/jpeg',
|
|
301
|
+
},
|
|
302
|
+
{ strict: false },
|
|
303
|
+
)
|
|
304
|
+
assert(parseResult.success)
|
|
305
|
+
assert(isLegacyBlobRef(parseResult.value))
|
|
306
|
+
expect(parseResult.value).toMatchObject({
|
|
307
|
+
cid: blobCid.toString(),
|
|
313
308
|
mimeType: 'image/jpeg',
|
|
314
309
|
})
|
|
315
|
-
expect(
|
|
310
|
+
expect(parseResult.value.cid).toBe(blobCid.toString())
|
|
311
|
+
|
|
312
|
+
const validateResult = schema.safeValidate(
|
|
313
|
+
{
|
|
314
|
+
cid: blobCid.toString(),
|
|
315
|
+
mimeType: 'image/jpeg',
|
|
316
|
+
},
|
|
317
|
+
{ strict: false },
|
|
318
|
+
)
|
|
319
|
+
assert(validateResult.success)
|
|
320
|
+
assert(isLegacyBlobRef(validateResult.value))
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('accepts legacy format with lexCid in non-strict mode', () => {
|
|
324
|
+
const schema = blob({})
|
|
325
|
+
const result = schema.safeParse(
|
|
326
|
+
{
|
|
327
|
+
cid: lexCid.toString(),
|
|
328
|
+
mimeType: 'image/png',
|
|
329
|
+
},
|
|
330
|
+
{ strict: false },
|
|
331
|
+
)
|
|
332
|
+
assert(result.success)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('rejects legacy format without cid', () => {
|
|
336
|
+
const schema = blob({})
|
|
337
|
+
const result = schema.safeParse(
|
|
338
|
+
{
|
|
339
|
+
mimeType: 'image/jpeg',
|
|
340
|
+
},
|
|
341
|
+
{ strict: false },
|
|
342
|
+
)
|
|
343
|
+
assert(!result.success)
|
|
316
344
|
})
|
|
317
345
|
|
|
318
346
|
it('rejects legacy format without mimeType', () => {
|
|
319
|
-
const schema = blob({
|
|
320
|
-
const result = schema.safeParse(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
347
|
+
const schema = blob({})
|
|
348
|
+
const result = schema.safeParse(
|
|
349
|
+
{
|
|
350
|
+
cid: blobCid.toString(),
|
|
351
|
+
},
|
|
352
|
+
{ strict: false },
|
|
353
|
+
)
|
|
354
|
+
assert(!result.success)
|
|
324
355
|
})
|
|
325
356
|
|
|
326
357
|
it('rejects legacy format with invalid cid', () => {
|
|
327
|
-
const schema = blob({
|
|
328
|
-
const result = schema.safeParse(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
358
|
+
const schema = blob({})
|
|
359
|
+
const result = schema.safeParse(
|
|
360
|
+
{
|
|
361
|
+
cid: 'invalid-cid',
|
|
362
|
+
mimeType: 'image/jpeg',
|
|
363
|
+
},
|
|
364
|
+
{ strict: false },
|
|
365
|
+
)
|
|
366
|
+
assert(!result.success)
|
|
333
367
|
})
|
|
334
368
|
|
|
335
369
|
it('rejects legacy format with numeric cid', () => {
|
|
336
|
-
const schema = blob({
|
|
337
|
-
const result = schema.safeParse(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
370
|
+
const schema = blob({})
|
|
371
|
+
const result = schema.safeParse(
|
|
372
|
+
{
|
|
373
|
+
cid: 123,
|
|
374
|
+
mimeType: 'image/jpeg',
|
|
375
|
+
},
|
|
376
|
+
{ strict: false },
|
|
377
|
+
)
|
|
378
|
+
assert(!result.success)
|
|
342
379
|
})
|
|
343
380
|
|
|
344
381
|
it('rejects legacy format with extra properties', () => {
|
|
345
|
-
const schema = blob({
|
|
346
|
-
const result = schema.safeParse(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
382
|
+
const schema = blob({})
|
|
383
|
+
const result = schema.safeParse(
|
|
384
|
+
{
|
|
385
|
+
cid: blobCid.toString(),
|
|
386
|
+
mimeType: 'image/jpeg',
|
|
387
|
+
extra: 'not allowed',
|
|
388
|
+
},
|
|
389
|
+
{ strict: false },
|
|
390
|
+
)
|
|
391
|
+
assert(!result.success)
|
|
352
392
|
})
|
|
353
393
|
|
|
354
|
-
it('accepts
|
|
355
|
-
const schema = blob({
|
|
394
|
+
it('accepts standard BlobRef always, LegacyBlobRef only with strict: false', () => {
|
|
395
|
+
const schema = blob({})
|
|
356
396
|
|
|
357
397
|
const blobRefResult = schema.safeParse({
|
|
358
398
|
$type: 'blob',
|
|
@@ -360,13 +400,22 @@ describe('BlobSchema', () => {
|
|
|
360
400
|
mimeType: 'image/jpeg',
|
|
361
401
|
size: 10000,
|
|
362
402
|
})
|
|
363
|
-
|
|
403
|
+
assert(blobRefResult.success)
|
|
364
404
|
|
|
365
|
-
const
|
|
405
|
+
const legacyResultStrict = schema.safeParse({
|
|
366
406
|
cid: blobCid.toString(),
|
|
367
407
|
mimeType: 'image/jpeg',
|
|
368
408
|
})
|
|
369
|
-
|
|
409
|
+
assert(!legacyResultStrict.success)
|
|
410
|
+
|
|
411
|
+
const legacyResultNonStrict = schema.safeParse(
|
|
412
|
+
{
|
|
413
|
+
cid: blobCid.toString(),
|
|
414
|
+
mimeType: 'image/jpeg',
|
|
415
|
+
},
|
|
416
|
+
{ strict: false },
|
|
417
|
+
)
|
|
418
|
+
assert(legacyResultNonStrict.success)
|
|
370
419
|
})
|
|
371
420
|
})
|
|
372
421
|
|
|
@@ -379,7 +428,7 @@ describe('BlobSchema', () => {
|
|
|
379
428
|
mimeType: 'image/gif',
|
|
380
429
|
size: 10000,
|
|
381
430
|
})
|
|
382
|
-
|
|
431
|
+
assert(!result.success)
|
|
383
432
|
})
|
|
384
433
|
|
|
385
434
|
it('accepts blob with maxSize option (not enforced)', () => {
|
|
@@ -390,7 +439,7 @@ describe('BlobSchema', () => {
|
|
|
390
439
|
mimeType: 'image/jpeg',
|
|
391
440
|
size: 10000,
|
|
392
441
|
})
|
|
393
|
-
|
|
442
|
+
assert(!result.success)
|
|
394
443
|
})
|
|
395
444
|
|
|
396
445
|
it('accepts blob matching accept constraint', () => {
|
|
@@ -401,7 +450,7 @@ describe('BlobSchema', () => {
|
|
|
401
450
|
mimeType: 'image/jpeg',
|
|
402
451
|
size: 10000,
|
|
403
452
|
})
|
|
404
|
-
|
|
453
|
+
assert(result.success)
|
|
405
454
|
})
|
|
406
455
|
|
|
407
456
|
it('accepts blob matching maxSize constraint', () => {
|
|
@@ -412,7 +461,7 @@ describe('BlobSchema', () => {
|
|
|
412
461
|
mimeType: 'image/jpeg',
|
|
413
462
|
size: 10000,
|
|
414
463
|
})
|
|
415
|
-
|
|
464
|
+
assert(result.success)
|
|
416
465
|
})
|
|
417
466
|
})
|
|
418
467
|
|
|
@@ -426,17 +475,17 @@ describe('BlobSchema', () => {
|
|
|
426
475
|
mimeType: 'video/mp4',
|
|
427
476
|
size: Number.MAX_SAFE_INTEGER,
|
|
428
477
|
})
|
|
429
|
-
|
|
478
|
+
assert(result.success)
|
|
430
479
|
})
|
|
431
480
|
|
|
432
481
|
it('rejects empty object', () => {
|
|
433
482
|
const result = schema.safeParse({})
|
|
434
|
-
|
|
483
|
+
assert(!result.success)
|
|
435
484
|
})
|
|
436
485
|
|
|
437
486
|
it('rejects object with only $type', () => {
|
|
438
487
|
const result = schema.safeParse({ $type: 'blob' })
|
|
439
|
-
|
|
488
|
+
assert(!result.success)
|
|
440
489
|
})
|
|
441
490
|
|
|
442
491
|
it('rejects blob with empty mimeType', () => {
|
|
@@ -446,7 +495,7 @@ describe('BlobSchema', () => {
|
|
|
446
495
|
mimeType: '',
|
|
447
496
|
size: 10000,
|
|
448
497
|
})
|
|
449
|
-
|
|
498
|
+
assert(!result.success)
|
|
450
499
|
})
|
|
451
500
|
|
|
452
501
|
it('rejects blob with null ref', () => {
|
|
@@ -456,7 +505,7 @@ describe('BlobSchema', () => {
|
|
|
456
505
|
mimeType: 'image/jpeg',
|
|
457
506
|
size: 10000,
|
|
458
507
|
})
|
|
459
|
-
|
|
508
|
+
assert(!result.success)
|
|
460
509
|
})
|
|
461
510
|
|
|
462
511
|
it('rejects blob with null mimeType', () => {
|
|
@@ -466,7 +515,7 @@ describe('BlobSchema', () => {
|
|
|
466
515
|
mimeType: null,
|
|
467
516
|
size: 10000,
|
|
468
517
|
})
|
|
469
|
-
|
|
518
|
+
assert(!result.success)
|
|
470
519
|
})
|
|
471
520
|
|
|
472
521
|
it('rejects blob with null size', () => {
|
|
@@ -476,171 +525,82 @@ describe('BlobSchema', () => {
|
|
|
476
525
|
mimeType: 'image/jpeg',
|
|
477
526
|
size: null,
|
|
478
527
|
})
|
|
479
|
-
|
|
528
|
+
assert(!result.success)
|
|
480
529
|
})
|
|
481
530
|
})
|
|
482
531
|
|
|
483
532
|
describe('legacy blob format with strict mode combinations', () => {
|
|
484
|
-
|
|
485
|
-
const schema = blob()
|
|
533
|
+
const schema = blob()
|
|
486
534
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
})
|
|
493
|
-
expect(result.success).toBe(false)
|
|
535
|
+
describe('strict: true (default)', () => {
|
|
536
|
+
it('rejects legacy blob format by default', () => {
|
|
537
|
+
const parseResult = schema.safeParse({
|
|
538
|
+
cid: blobCid.toString(),
|
|
539
|
+
mimeType: 'image/jpeg',
|
|
494
540
|
})
|
|
541
|
+
assert(!parseResult.success)
|
|
495
542
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
ref: blobCid,
|
|
500
|
-
mimeType: 'image/jpeg',
|
|
501
|
-
size: 10000,
|
|
502
|
-
})
|
|
503
|
-
expect(result.success).toBe(true)
|
|
543
|
+
const validateResult = schema.safeValidate({
|
|
544
|
+
cid: blobCid.toString(),
|
|
545
|
+
mimeType: 'image/jpeg',
|
|
504
546
|
})
|
|
547
|
+
assert(!validateResult.success)
|
|
505
548
|
})
|
|
506
549
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
},
|
|
514
|
-
{ strict: false },
|
|
515
|
-
)
|
|
516
|
-
assert(result.success)
|
|
517
|
-
expect(result.value).toEqual({
|
|
518
|
-
$type: 'blob',
|
|
519
|
-
ref: blobCid,
|
|
520
|
-
mimeType: 'image/jpeg',
|
|
521
|
-
size: -1,
|
|
522
|
-
})
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
it('coerces legacy blob format with lexCid', () => {
|
|
526
|
-
const result = schema.safeParse(
|
|
527
|
-
{
|
|
528
|
-
cid: lexCid.toString(),
|
|
529
|
-
mimeType: 'image/png',
|
|
530
|
-
},
|
|
531
|
-
{ strict: false },
|
|
532
|
-
)
|
|
533
|
-
assert(result.success)
|
|
534
|
-
expect(result.value).toEqual({
|
|
535
|
-
$type: 'blob',
|
|
536
|
-
ref: lexCid,
|
|
537
|
-
mimeType: 'image/png',
|
|
538
|
-
size: -1,
|
|
539
|
-
})
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
it('rejects legacy blob format with invalid cid', () => {
|
|
543
|
-
const result = schema.safeParse(
|
|
544
|
-
{
|
|
545
|
-
cid: 'invalid-cid',
|
|
546
|
-
mimeType: 'image/jpeg',
|
|
547
|
-
},
|
|
548
|
-
{ strict: false },
|
|
549
|
-
)
|
|
550
|
-
expect(result.success).toBe(false)
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
it('accepts standard BlobRef with non-raw CID', () => {
|
|
554
|
-
const result = schema.safeParse(
|
|
555
|
-
{
|
|
556
|
-
$type: 'blob',
|
|
557
|
-
ref: lexCid,
|
|
558
|
-
mimeType: 'image/jpeg',
|
|
559
|
-
size: 10000,
|
|
560
|
-
},
|
|
561
|
-
{ strict: false },
|
|
562
|
-
)
|
|
563
|
-
expect(result.success).toBe(true)
|
|
550
|
+
it('accepts standard BlobRef', () => {
|
|
551
|
+
const result = schema.safeParse({
|
|
552
|
+
$type: 'blob',
|
|
553
|
+
ref: blobCid,
|
|
554
|
+
mimeType: 'image/jpeg',
|
|
555
|
+
size: 10000,
|
|
564
556
|
})
|
|
557
|
+
assert(result.success)
|
|
565
558
|
})
|
|
566
559
|
})
|
|
567
560
|
|
|
568
|
-
describe('
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
it('accepts legacy blob format as LegacyBlobRef', () => {
|
|
573
|
-
const result = schema.safeParse({
|
|
561
|
+
describe('strict: false', () => {
|
|
562
|
+
it('accepts legacy blob format in both parse and validate', () => {
|
|
563
|
+
const parseResult = schema.safeParse(
|
|
564
|
+
{
|
|
574
565
|
cid: blobCid.toString(),
|
|
575
566
|
mimeType: 'image/jpeg',
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
)
|
|
581
|
-
})
|
|
567
|
+
},
|
|
568
|
+
{ strict: false },
|
|
569
|
+
)
|
|
570
|
+
assert(parseResult.success)
|
|
582
571
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
ref: blobCid,
|
|
572
|
+
const validateResult = schema.safeValidate(
|
|
573
|
+
{
|
|
574
|
+
cid: blobCid.toString(),
|
|
587
575
|
mimeType: 'image/jpeg',
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
576
|
+
},
|
|
577
|
+
{ strict: false },
|
|
578
|
+
)
|
|
579
|
+
assert(validateResult.success)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('accepts legacy blob format with lexCid', () => {
|
|
583
|
+
const result = schema.safeParse(
|
|
584
|
+
{
|
|
585
|
+
cid: lexCid.toString(),
|
|
586
|
+
mimeType: 'image/png',
|
|
587
|
+
},
|
|
588
|
+
{ strict: false },
|
|
589
|
+
)
|
|
590
|
+
assert(result.success)
|
|
591
|
+
})
|
|
592
592
|
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
it('accepts standard BlobRef with non-raw CID', () => {
|
|
594
|
+
const result = schema.safeParse(
|
|
595
|
+
{
|
|
595
596
|
$type: 'blob',
|
|
596
597
|
ref: lexCid,
|
|
597
598
|
mimeType: 'image/jpeg',
|
|
598
599
|
size: 10000,
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
describe('strict: false', () => {
|
|
605
|
-
it('accepts legacy blob format as LegacyBlobRef', () => {
|
|
606
|
-
const result = schema.safeParse(
|
|
607
|
-
{
|
|
608
|
-
cid: blobCid.toString(),
|
|
609
|
-
mimeType: 'image/jpeg',
|
|
610
|
-
},
|
|
611
|
-
{ strict: false },
|
|
612
|
-
)
|
|
613
|
-
assert(result.success)
|
|
614
|
-
expect('cid' in result.value && result.value.cid).toBe(
|
|
615
|
-
blobCid.toString(),
|
|
616
|
-
)
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
it('accepts standard BlobRef with non-raw CID (non-strict)', () => {
|
|
620
|
-
const result = schema.safeParse(
|
|
621
|
-
{
|
|
622
|
-
$type: 'blob',
|
|
623
|
-
ref: lexCid,
|
|
624
|
-
mimeType: 'image/jpeg',
|
|
625
|
-
size: 10000,
|
|
626
|
-
},
|
|
627
|
-
{ strict: false },
|
|
628
|
-
)
|
|
629
|
-
expect(result.success).toBe(true)
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
it('accepts standard BlobRef with raw CID', () => {
|
|
633
|
-
const result = schema.safeParse(
|
|
634
|
-
{
|
|
635
|
-
$type: 'blob',
|
|
636
|
-
ref: blobCid,
|
|
637
|
-
mimeType: 'image/jpeg',
|
|
638
|
-
size: 10000,
|
|
639
|
-
},
|
|
640
|
-
{ strict: false },
|
|
641
|
-
)
|
|
642
|
-
expect(result.success).toBe(true)
|
|
643
|
-
})
|
|
600
|
+
},
|
|
601
|
+
{ strict: false },
|
|
602
|
+
)
|
|
603
|
+
assert(result.success)
|
|
644
604
|
})
|
|
645
605
|
})
|
|
646
606
|
})
|
|
@@ -656,7 +616,7 @@ describe('BlobSchema', () => {
|
|
|
656
616
|
mimeType: 'image/gif',
|
|
657
617
|
size: 10000,
|
|
658
618
|
})
|
|
659
|
-
|
|
619
|
+
assert(!result.success)
|
|
660
620
|
})
|
|
661
621
|
|
|
662
622
|
it('accepts non-matching mime type in non-strict mode', () => {
|
|
@@ -669,7 +629,7 @@ describe('BlobSchema', () => {
|
|
|
669
629
|
},
|
|
670
630
|
{ strict: false },
|
|
671
631
|
)
|
|
672
|
-
|
|
632
|
+
assert(result.success)
|
|
673
633
|
})
|
|
674
634
|
|
|
675
635
|
it('accepts matching mime type in strict mode', () => {
|
|
@@ -679,7 +639,7 @@ describe('BlobSchema', () => {
|
|
|
679
639
|
mimeType: 'image/jpeg',
|
|
680
640
|
size: 10000,
|
|
681
641
|
})
|
|
682
|
-
|
|
642
|
+
assert(result.success)
|
|
683
643
|
})
|
|
684
644
|
})
|
|
685
645
|
|
|
@@ -693,7 +653,7 @@ describe('BlobSchema', () => {
|
|
|
693
653
|
mimeType: 'image/jpeg',
|
|
694
654
|
size: 5000,
|
|
695
655
|
})
|
|
696
|
-
|
|
656
|
+
assert(!result.success)
|
|
697
657
|
})
|
|
698
658
|
|
|
699
659
|
it('accepts oversized blob in non-strict mode', () => {
|
|
@@ -706,7 +666,7 @@ describe('BlobSchema', () => {
|
|
|
706
666
|
},
|
|
707
667
|
{ strict: false },
|
|
708
668
|
)
|
|
709
|
-
|
|
669
|
+
assert(result.success)
|
|
710
670
|
})
|
|
711
671
|
|
|
712
672
|
it('accepts correctly sized blob in strict mode', () => {
|
|
@@ -716,7 +676,7 @@ describe('BlobSchema', () => {
|
|
|
716
676
|
mimeType: 'image/jpeg',
|
|
717
677
|
size: 500,
|
|
718
678
|
})
|
|
719
|
-
|
|
679
|
+
assert(result.success)
|
|
720
680
|
})
|
|
721
681
|
})
|
|
722
682
|
|
|
@@ -733,7 +693,7 @@ describe('BlobSchema', () => {
|
|
|
733
693
|
mimeType: 'image/jpeg',
|
|
734
694
|
size: 10000,
|
|
735
695
|
})
|
|
736
|
-
|
|
696
|
+
assert(result.success)
|
|
737
697
|
})
|
|
738
698
|
|
|
739
699
|
it('rejects wrong mime in strict mode', () => {
|
|
@@ -743,7 +703,7 @@ describe('BlobSchema', () => {
|
|
|
743
703
|
mimeType: 'image/png',
|
|
744
704
|
size: 10000,
|
|
745
705
|
})
|
|
746
|
-
|
|
706
|
+
assert(!result.success)
|
|
747
707
|
})
|
|
748
708
|
|
|
749
709
|
it('rejects oversized in strict mode', () => {
|
|
@@ -753,7 +713,7 @@ describe('BlobSchema', () => {
|
|
|
753
713
|
mimeType: 'image/jpeg',
|
|
754
714
|
size: 30000,
|
|
755
715
|
})
|
|
756
|
-
|
|
716
|
+
assert(!result.success)
|
|
757
717
|
})
|
|
758
718
|
|
|
759
719
|
it('accepts wrong mime and oversized in non-strict mode', () => {
|
|
@@ -766,7 +726,7 @@ describe('BlobSchema', () => {
|
|
|
766
726
|
},
|
|
767
727
|
{ strict: false },
|
|
768
728
|
)
|
|
769
|
-
|
|
729
|
+
assert(result.success)
|
|
770
730
|
})
|
|
771
731
|
})
|
|
772
732
|
})
|
package/src/schema/blob.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BlobRef,
|
|
3
3
|
LegacyBlobRef,
|
|
4
|
+
TypedBlobRef,
|
|
5
|
+
getBlobSize,
|
|
4
6
|
isBlobRef,
|
|
5
7
|
isLegacyBlobRef,
|
|
6
|
-
|
|
8
|
+
isTypedBlobRef,
|
|
7
9
|
} from '@atproto/lex-data'
|
|
8
10
|
import { Schema, ValidationContext } from '../core.js'
|
|
9
11
|
import { memoizedOptions } from '../util/memoize.js'
|
|
@@ -12,14 +14,6 @@ import { memoizedOptions } from '../util/memoize.js'
|
|
|
12
14
|
* Configuration options for blob schema validation.
|
|
13
15
|
*/
|
|
14
16
|
export type BlobSchemaOptions = {
|
|
15
|
-
/**
|
|
16
|
-
* Whether to allow legacy blob references format
|
|
17
|
-
*
|
|
18
|
-
* @default false
|
|
19
|
-
* @see {@link LegacyBlobRef}
|
|
20
|
-
*/
|
|
21
|
-
allowLegacy?: boolean
|
|
22
|
-
|
|
23
17
|
/**
|
|
24
18
|
* List of accepted MIME types (supports wildcards like 'image/*' or '*\/*')
|
|
25
19
|
*
|
|
@@ -35,8 +29,8 @@ export type BlobSchemaOptions = {
|
|
|
35
29
|
maxSize?: number
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
export type { BlobRef, LegacyBlobRef }
|
|
39
|
-
export { isBlobRef, isLegacyBlobRef }
|
|
32
|
+
export type { BlobRef, LegacyBlobRef, TypedBlobRef }
|
|
33
|
+
export { isBlobRef, isLegacyBlobRef, isTypedBlobRef }
|
|
40
34
|
|
|
41
35
|
/**
|
|
42
36
|
* Schema for validating blob references in AT Protocol.
|
|
@@ -55,9 +49,7 @@ export { isBlobRef, isLegacyBlobRef }
|
|
|
55
49
|
*/
|
|
56
50
|
export class BlobSchema<
|
|
57
51
|
const TOptions extends BlobSchemaOptions = NonNullable<unknown>,
|
|
58
|
-
> extends Schema<
|
|
59
|
-
TOptions extends { allowLegacy: true } ? BlobRef | LegacyBlobRef : BlobRef
|
|
60
|
-
> {
|
|
52
|
+
> extends Schema<BlobRef> {
|
|
61
53
|
readonly type = 'blob' as const
|
|
62
54
|
|
|
63
55
|
constructor(readonly options?: TOptions) {
|
|
@@ -65,23 +57,29 @@ export class BlobSchema<
|
|
|
65
57
|
}
|
|
66
58
|
|
|
67
59
|
validateInContext(input: unknown, ctx: ValidationContext) {
|
|
68
|
-
const blob = parseValue.call(ctx, input
|
|
69
|
-
|
|
60
|
+
const blob = parseValue.call(ctx, input)
|
|
70
61
|
if (!blob) {
|
|
71
62
|
return ctx.issueUnexpectedType(input, 'blob')
|
|
72
63
|
}
|
|
73
64
|
|
|
74
65
|
// In non-strict mode, we allow blob refs to pass through without MIME
|
|
75
66
|
// type or size checks.
|
|
76
|
-
if (ctx.options.strict) {
|
|
77
|
-
const accept = this.options
|
|
67
|
+
if (ctx.options.strict && this.options != null) {
|
|
68
|
+
const { accept } = this.options
|
|
78
69
|
if (accept && !matchesMime(blob.mimeType, accept)) {
|
|
79
70
|
return ctx.issueInvalidPropertyValue(blob, 'mimeType', accept)
|
|
80
71
|
}
|
|
81
72
|
|
|
82
|
-
const maxSize = this.options
|
|
83
|
-
if (maxSize != null
|
|
84
|
-
|
|
73
|
+
const { maxSize } = this.options
|
|
74
|
+
if (maxSize != null) {
|
|
75
|
+
const size = getBlobSize(blob)
|
|
76
|
+
if (size === undefined) {
|
|
77
|
+
// Unable to enforce size constraint if size is not available (legacy
|
|
78
|
+
// blob ref), so we treat it as a validation failure in strict mode.
|
|
79
|
+
return ctx.issueInvalidPropertyType(blob, 'size' as any, 'integer')
|
|
80
|
+
} else if (size > maxSize) {
|
|
81
|
+
return ctx.issueTooBig(blob, 'blob', maxSize, size)
|
|
82
|
+
}
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
@@ -95,33 +93,19 @@ export class BlobSchema<
|
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
function parseValue(
|
|
99
|
-
|
|
100
|
-
input: unknown,
|
|
101
|
-
options?: BlobSchemaOptions,
|
|
102
|
-
): BlobRef | LegacyBlobRef | null {
|
|
103
|
-
// If there is a $type property, we treat if as a potential BlobRef and
|
|
96
|
+
function parseValue(this: ValidationContext, input: unknown): BlobRef | null {
|
|
97
|
+
// If there is a $type property, we treat if as a potential TypedBlobRef and
|
|
104
98
|
// validate accordingly.
|
|
105
99
|
if ((input as any)?.$type !== undefined) {
|
|
106
100
|
// Use the context's option for the "strict" check
|
|
107
|
-
return
|
|
101
|
+
return isTypedBlobRef(input, this.options) ? input : null
|
|
108
102
|
}
|
|
109
103
|
|
|
110
104
|
// If there is no $type property, we may be dealing with a legacy blob ref. If
|
|
111
|
-
// legacy refs are allowed,
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (options?.allowLegacy) {
|
|
116
|
-
if (isLegacyBlobRef(input)) {
|
|
117
|
-
return input
|
|
118
|
-
}
|
|
119
|
-
} else if (!this.options.strict && this.options.mode === 'parse') {
|
|
120
|
-
if (isLegacyBlobRef(input)) {
|
|
121
|
-
const { cid, mimeType } = input
|
|
122
|
-
const ref = parseCidSafe(cid)
|
|
123
|
-
if (ref) return { $type: 'blob', ref, mimeType, size: -1 }
|
|
124
|
-
}
|
|
105
|
+
// legacy refs are allowed (non-strict mode), we check if the input matches
|
|
106
|
+
// the legacy format.
|
|
107
|
+
if (!this.options.strict) {
|
|
108
|
+
if (isLegacyBlobRef(input, this.options)) return input
|
|
125
109
|
}
|
|
126
110
|
|
|
127
111
|
return null
|
|
@@ -157,13 +141,10 @@ function matchesMime(mime: string, accepted: string[]): boolean {
|
|
|
157
141
|
*
|
|
158
142
|
* // Any image type with size limit
|
|
159
143
|
* const avatarSchema = l.blob({ accept: ['image/*'], maxSize: 1000000 })
|
|
160
|
-
*
|
|
161
|
-
* // Allow legacy format
|
|
162
|
-
* const legacySchema = l.blob({ allowLegacy: true })
|
|
163
144
|
* ```
|
|
164
145
|
*/
|
|
165
146
|
export const blob = /*#__PURE__*/ memoizedOptions(function <
|
|
166
|
-
O extends BlobSchemaOptions =
|
|
147
|
+
O extends BlobSchemaOptions = NonNullable<unknown>,
|
|
167
148
|
>(options?: O) {
|
|
168
149
|
return new BlobSchema(options)
|
|
169
150
|
})
|