@onlineapps/content-resolver 1.1.16 → 2.0.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.
- package/README.md +36 -8
- package/package.json +9 -8
- package/src/index.js +173 -31
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ const resolver = new ContentResolver({
|
|
|
31
31
|
port: 9000,
|
|
32
32
|
accessKey: 'minioadmin',
|
|
33
33
|
secretKey: 'minioadmin'
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
logger // required - the constructor throws without a logger exposing warn()
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
// Resolve reference to content
|
|
@@ -57,7 +58,7 @@ const descriptor = await resolver.store(largeText, { workflow_id: 'wf-123' }, 'd
|
|
|
57
58
|
const ContentResolver = require('@onlineapps/content-resolver');
|
|
58
59
|
|
|
59
60
|
exports.processDocument = async (input, context = {}) => {
|
|
60
|
-
const resolver = new ContentResolver();
|
|
61
|
+
const resolver = new ContentResolver({ logger }); // logger is required
|
|
61
62
|
|
|
62
63
|
// Input can be either text or reference - resolve transparently
|
|
63
64
|
const resolvedInput = await resolver.resolveInput(input, ['content', 'markdown']);
|
|
@@ -80,17 +81,37 @@ exports.processDocument = async (input, context = {}) => {
|
|
|
80
81
|
|--------|------|---------|-------------|
|
|
81
82
|
| `threshold` | number | 16384 | Size threshold in bytes |
|
|
82
83
|
| `storage` | Object | env-based | Storage connector config |
|
|
83
|
-
| `logger` | Object |
|
|
84
|
+
| `logger` | Object | **required** | Logger instance — must expose `warn()`. The constructor throws without it (`src/index.js`); there is no `console` default. |
|
|
85
|
+
|
|
86
|
+
### Input contract
|
|
87
|
+
|
|
88
|
+
Every method below takes one of two input sets, and **throws on anything else** —
|
|
89
|
+
there is no `String(value)` coercion anywhere in the API. An object used to be
|
|
90
|
+
stored and shipped as the literal bytes `[object Object]`; that path is gone.
|
|
91
|
+
|
|
92
|
+
| Set | Accepted | Methods |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| **Content value** | a string (inline content **or** a `minio://<bucket>/<path>` / `internal://storage/<path>` reference), or a Content Descriptor (`{ _descriptor: true, type: 'inline' \| 'file' }`, or type-only) | `getAsBuffer`, `getAsString`, `getMetadata`, `normalizeToDescriptor` |
|
|
95
|
+
| **Raw content** | a string or a `Buffer` | `createDescriptor`, `store` |
|
|
96
|
+
|
|
97
|
+
`resolve()` is narrower still: a string only, because it promises to return one.
|
|
98
|
+
|
|
99
|
+
Rejection message format: `[ContentResolver] Unsupported content value - <method>() accepts <shapes>, got <kind>. Fix: …`
|
|
84
100
|
|
|
85
101
|
### Methods
|
|
86
102
|
|
|
87
103
|
#### `resolve(value): Promise<string>`
|
|
88
104
|
If value is a reference (`minio://...`), downloads and returns content.
|
|
89
|
-
|
|
105
|
+
A non-reference string is returned unchanged. **Anything that is not a string throws** —
|
|
106
|
+
use `getAsString()` to read a Descriptor.
|
|
90
107
|
|
|
91
108
|
#### `store(content, context, filename?, content_type?): Promise<Object>`
|
|
92
109
|
Stores content and returns **Content Descriptor**. If size > threshold, stores in MinIO.
|
|
93
110
|
Returns Descriptor with `type: 'inline'` or `type: 'file'`.
|
|
111
|
+
Content must be a string or a `Buffer`. The **empty string** `''` is content: it returns
|
|
112
|
+
the documented empty inline descriptor (`filename: 'empty.txt'`, `size: 0`, no
|
|
113
|
+
fingerprint). Every other falsy value — `null`, `undefined`, `0`, `false` — is a MISSING
|
|
114
|
+
value and **throws** the same contract error `createDescriptor()` throws.
|
|
94
115
|
|
|
95
116
|
#### `getAsBuffer(value): Promise<Buffer>`
|
|
96
117
|
Unified API to get content as Buffer. Accepts:
|
|
@@ -98,17 +119,24 @@ Unified API to get content as Buffer. Accepts:
|
|
|
98
119
|
- Storage reference (`minio://...`) → downloads and returns Buffer
|
|
99
120
|
- Content Descriptor → extracts content as Buffer
|
|
100
121
|
|
|
122
|
+
A raw `Buffer` is **not** accepted here — this method reads content, it does not wrap it.
|
|
123
|
+
|
|
101
124
|
#### `getAsString(value): Promise<string>`
|
|
102
125
|
Unified API to get content as string. Works with string, reference, or Descriptor.
|
|
103
126
|
|
|
104
127
|
#### `getMetadata(value): Object`
|
|
105
|
-
Get metadata (filename, content_type, size, fingerprint) from
|
|
128
|
+
Get metadata (filename, content_type, size, fingerprint) from a string or a Descriptor —
|
|
129
|
+
**exactly** the values `getAsBuffer()` accepts, so the two never disagree about what a
|
|
130
|
+
value is. Throws on anything else.
|
|
106
131
|
|
|
107
132
|
#### `createDescriptor(content, options): Promise<Object>`
|
|
108
|
-
Create Content Descriptor from raw content
|
|
133
|
+
Create Content Descriptor from raw content — a **string or a `Buffer`**.
|
|
134
|
+
Automatically decides inline vs file storage; a Buffer is always stored as a file.
|
|
109
135
|
|
|
110
136
|
#### `normalizeToDescriptor(value, options): Promise<Object>`
|
|
111
|
-
Normalize
|
|
137
|
+
Normalize a string, a reference or a Descriptor to a Content Descriptor.
|
|
138
|
+
A raw `Buffer` is **not** accepted (it carries no filename/content-type context) —
|
|
139
|
+
pass it to `createDescriptor()` or `store()` instead.
|
|
112
140
|
|
|
113
141
|
#### `createDescriptorFromFile(tempPath, options): Promise<Object>`
|
|
114
142
|
Create Content Descriptor from a temp file. **Used by ApiMapper for file outputs.**
|
|
@@ -176,7 +204,7 @@ Stores large content fields as Descriptors (returns Descriptors, not plain strin
|
|
|
176
204
|
### Usage Example
|
|
177
205
|
|
|
178
206
|
```javascript
|
|
179
|
-
const resolver = new ContentResolver();
|
|
207
|
+
const resolver = new ContentResolver({ logger }); // logger is required
|
|
180
208
|
|
|
181
209
|
// Work with attachments - unified API
|
|
182
210
|
async function processAttachment(attachment) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/content-resolver",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Automatic conversion between text content and storage references with Content Descriptor pattern",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "npm run test:unit && npm run test:integration",
|
|
8
|
+
"test:unit": "jest",
|
|
9
|
+
"test:integration": "jest --config=jest.integration.config.js"
|
|
8
10
|
},
|
|
9
11
|
"keywords": [
|
|
10
12
|
"content",
|
|
@@ -16,11 +18,10 @@
|
|
|
16
18
|
"author": "OnlineApps",
|
|
17
19
|
"license": "ISC",
|
|
18
20
|
"dependencies": {
|
|
19
|
-
"@onlineapps/conn-base-storage": "
|
|
20
|
-
"@onlineapps/runtime-config": "1.0.
|
|
21
|
+
"@onlineapps/conn-base-storage": "2.0.0",
|
|
22
|
+
"@onlineapps/runtime-config": "1.0.3"
|
|
21
23
|
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
}
|
|
25
|
-
"devDependencies": {}
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"jest": "^29.7.0"
|
|
26
|
+
}
|
|
26
27
|
}
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,16 @@ const INTERNAL_REF_PATTERN = /^internal:\/\/storage\/(.+)$/;
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Check if a value is a Content Descriptor
|
|
26
|
+
*
|
|
27
|
+
* A descriptor is recognised by the explicit `_descriptor` flag OR by its `type`
|
|
28
|
+
* alone. The second form is part of the contract, not a backward-compatibility
|
|
29
|
+
* shim: `emailer` treats an externally supplied `{ type: 'inline' | 'file' }`
|
|
30
|
+
* payload as canonical and forwards it to the resolver WITHOUT stamping
|
|
31
|
+
* `_descriptor` (api_biz/emailer/src/services/email.service.js:332 → :431), and
|
|
32
|
+
* its operation schema declares `attachments[].value` as an untyped object
|
|
33
|
+
* (api_biz/emailer/config/service/operations.json), so such payloads arrive from
|
|
34
|
+
* outside this codebase. Dropping type-only recognition breaks that consumer.
|
|
35
|
+
*
|
|
26
36
|
* @param {*} value - Value to check
|
|
27
37
|
* @returns {boolean} True if value is a Descriptor object
|
|
28
38
|
*/
|
|
@@ -30,10 +40,73 @@ function isDescriptor(value) {
|
|
|
30
40
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
31
41
|
return false;
|
|
32
42
|
}
|
|
33
|
-
// Check explicit identifier first, then fallback to type check for backward compatibility
|
|
34
43
|
return value._descriptor === true || (value.type === 'inline' || value.type === 'file');
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Name a value's kind for an error message — `typeof` alone cannot tell null,
|
|
48
|
+
* an array and an object apart, and those are the three shapes that actually
|
|
49
|
+
* reach the resolver by mistake.
|
|
50
|
+
* @param {*} value
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function describeValue(value) {
|
|
54
|
+
if (value === null) return 'null';
|
|
55
|
+
if (Array.isArray(value)) return 'array';
|
|
56
|
+
return typeof value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The input contract, written down ONCE.
|
|
61
|
+
*
|
|
62
|
+
* Every public method that takes a content value takes one of these sets and
|
|
63
|
+
* throws on anything else. Before 2026-08-28 four of them silently coerced with
|
|
64
|
+
* `String(value)` instead, so the same object was a hard error in
|
|
65
|
+
* `getAsBuffer()` and the literal text `[object Object]` two lines later in
|
|
66
|
+
* `getMetadata()` (api_biz/emailer/src/services/email.service.js:431 → :434).
|
|
67
|
+
*
|
|
68
|
+
* @see ../../../../.claude/rules/architecture-principles.md §3 (No Fallbacks), §4 (Fail-Fast)
|
|
69
|
+
*/
|
|
70
|
+
const ACCEPTS = {
|
|
71
|
+
/** A string (inline content or a reference). Used where the return type is a string. */
|
|
72
|
+
reference:
|
|
73
|
+
`a string (inline content or a minio://<bucket>/<path> / internal://storage/<path> reference)`,
|
|
74
|
+
|
|
75
|
+
/** A string or a Content Descriptor — the set `getAsBuffer()` established. */
|
|
76
|
+
contentValue:
|
|
77
|
+
`a string (inline content or a minio://<bucket>/<path> / internal://storage/<path> reference) ` +
|
|
78
|
+
`or a Content Descriptor ({ _descriptor: true, type: 'inline' | 'file' })`,
|
|
79
|
+
|
|
80
|
+
/** Raw bytes on the way IN to storage: a string or a Buffer. */
|
|
81
|
+
rawContent: 'a string or a Buffer'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The actionable half of the message, per `architecture-principles.md` §5. */
|
|
85
|
+
const FIXES = {
|
|
86
|
+
wrap: 'Fix: wrap raw data with store() or createDescriptor() before passing it.',
|
|
87
|
+
serialize: 'Fix: serialize the value yourself (e.g. JSON.stringify) or pass a Buffer.',
|
|
88
|
+
readDescriptor:
|
|
89
|
+
'Fix: read a Content Descriptor with getAsString() instead, or pass the string itself.'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build the one rejection message this package uses — one owner, not one copy
|
|
94
|
+
* per method.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} method - The public method that received the value.
|
|
97
|
+
* @param {string} accepted - One of `ACCEPTS`.
|
|
98
|
+
* @param {*} value - The rejected value.
|
|
99
|
+
* @param {string} fix - One of `FIXES`.
|
|
100
|
+
* @returns {Error}
|
|
101
|
+
*/
|
|
102
|
+
function unsupportedValueError(method, accepted, value, fix) {
|
|
103
|
+
return new Error(
|
|
104
|
+
`[ContentResolver] Unsupported content value - ${method}() accepts ${accepted}, got ` +
|
|
105
|
+
`${describeValue(value)}. ` +
|
|
106
|
+
`${fix}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
37
110
|
/**
|
|
38
111
|
* Get content type from filename or content
|
|
39
112
|
* @param {string} filename - Filename with extension
|
|
@@ -155,12 +228,20 @@ class ContentResolver {
|
|
|
155
228
|
|
|
156
229
|
/**
|
|
157
230
|
* Resolve a value - if it's a reference, download content; if it's text, return as-is
|
|
231
|
+
*
|
|
232
|
+
* Accepts a string and nothing else. The method promises to RETURN a string,
|
|
233
|
+
* so a non-string input has no correct answer; handing it back unchanged made
|
|
234
|
+
* that promise false and pushed the type error into whichever caller did
|
|
235
|
+
* string work on the result next. Both real call sites already reject a
|
|
236
|
+
* non-string themselves (api_biz/pdfgen/src/handlers/pdf.js:221 and :297).
|
|
237
|
+
*
|
|
158
238
|
* @param {string} value - Text content or storage reference
|
|
159
239
|
* @returns {Promise<string>} Resolved text content
|
|
240
|
+
* @throws {Error} If the value is not a string
|
|
160
241
|
*/
|
|
161
242
|
async resolve(value) {
|
|
162
|
-
if (
|
|
163
|
-
|
|
243
|
+
if (typeof value !== 'string') {
|
|
244
|
+
throw unsupportedValueError('resolve', ACCEPTS.reference, value, FIXES.readDescriptor);
|
|
164
245
|
}
|
|
165
246
|
|
|
166
247
|
// Check if it's a reference
|
|
@@ -168,12 +249,17 @@ class ContentResolver {
|
|
|
168
249
|
return value; // Already text content
|
|
169
250
|
}
|
|
170
251
|
|
|
171
|
-
// Parse reference
|
|
252
|
+
// Parse reference.
|
|
253
|
+
//
|
|
254
|
+
// No null check: `isReference()` above and `parseReference()` read the SAME
|
|
255
|
+
// two patterns, neither with the `g` flag, so a value that passed the first
|
|
256
|
+
// cannot fail the second. The `if (!parsed)` branch that used to sit here
|
|
257
|
+
// warned and returned the raw reference as if it were content — a fallback
|
|
258
|
+
// that could never fire, and would have handed back a URI instead of a
|
|
259
|
+
// document if it had (`architecture-principles.md` §3). The invariant is
|
|
260
|
+
// asserted in tests/unit/content-resolver.test.js
|
|
261
|
+
// ('invariant — everything isReference() accepts, parseReference() parses').
|
|
172
262
|
const parsed = parseReference(value);
|
|
173
|
-
if (!parsed) {
|
|
174
|
-
this.logger.warn(`[ContentResolver] Invalid reference format: ${value}`);
|
|
175
|
-
return value;
|
|
176
|
-
}
|
|
177
263
|
|
|
178
264
|
// Download content
|
|
179
265
|
try {
|
|
@@ -206,7 +292,14 @@ class ContentResolver {
|
|
|
206
292
|
* @returns {Promise<Object>} Content Descriptor
|
|
207
293
|
*/
|
|
208
294
|
async store(content, context = {}, filename = null, content_type = null) {
|
|
209
|
-
|
|
295
|
+
// The EMPTY STRING is content: the caller said "store nothing", and gets the
|
|
296
|
+
// documented empty inline descriptor for it. Everything else falsy — `null`,
|
|
297
|
+
// `undefined`, `0`, `false`, `NaN` — is a MISSING value, i.e. a defect at the
|
|
298
|
+
// call site, and falls through to the one input contract this package owns
|
|
299
|
+
// (`createDescriptor()` / `ACCEPTS.rawContent`). The old `if (!content)` test
|
|
300
|
+
// could not tell the two apart and answered a mistake with a valid-looking
|
|
301
|
+
// descriptor (`architecture-principles.md` §3, §4).
|
|
302
|
+
if (content === '') {
|
|
210
303
|
return {
|
|
211
304
|
_descriptor: true,
|
|
212
305
|
type: 'inline',
|
|
@@ -314,30 +407,63 @@ class ContentResolver {
|
|
|
314
407
|
|
|
315
408
|
/**
|
|
316
409
|
* Get content as Buffer - unified API for string, reference, or Descriptor
|
|
410
|
+
*
|
|
411
|
+
* Accepts exactly two input types, and nothing else:
|
|
412
|
+
* - a string — either inline content or a storage reference. This is an
|
|
413
|
+
* input type, not a leniency path: `api_monitoring` passes a bare
|
|
414
|
+
* `minio://…` string (api/infra/api_monitoring/src/consumer/index.js:2815).
|
|
415
|
+
* - a Content Descriptor — stamped or type-only (see `isDescriptor`).
|
|
416
|
+
*
|
|
417
|
+
* Anything else throws. It used to be coerced with `Buffer.from(String(value))`,
|
|
418
|
+
* which turned an object into the literal bytes `[object Object]` and shipped
|
|
419
|
+
* them as an email attachment or an HTTP download body — a silent fallback
|
|
420
|
+
* banned by `architecture-principles.md` §3.
|
|
421
|
+
*
|
|
317
422
|
* @param {string|Object} value - String, reference, or Content Descriptor
|
|
318
423
|
* @returns {Promise<Buffer>} Content as Buffer
|
|
424
|
+
* @throws {Error} If the value is not a string or a well-formed Descriptor
|
|
319
425
|
*/
|
|
320
426
|
async getAsBuffer(value) {
|
|
321
|
-
// Plain string (backward compatibility)
|
|
322
427
|
if (typeof value === 'string') {
|
|
323
428
|
if (isReference(value)) {
|
|
324
429
|
return await this.downloadAsBuffer(value);
|
|
325
430
|
}
|
|
326
431
|
return Buffer.from(value, 'utf-8');
|
|
327
432
|
}
|
|
328
|
-
|
|
329
|
-
// Content Descriptor
|
|
433
|
+
|
|
330
434
|
if (isDescriptor(value)) {
|
|
331
435
|
if (value.type === 'file') {
|
|
436
|
+
if (typeof value.storage_ref !== 'string' || value.storage_ref.length === 0) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`[ContentResolver] File descriptor without storage_ref - a descriptor of type 'file' requires ` +
|
|
439
|
+
`a non-empty storage_ref string (minio://<bucket>/<path> or internal://storage/<path>), got ` +
|
|
440
|
+
`${describeValue(value.storage_ref)}. ` +
|
|
441
|
+
`Fix: build the descriptor with store() or createDescriptor() instead of by hand.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
332
444
|
return await this.downloadAsBuffer(value.storage_ref);
|
|
333
445
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
446
|
+
|
|
447
|
+
if (value.type === 'inline') {
|
|
448
|
+
if (typeof value.content !== 'string') {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`[ContentResolver] Inline descriptor without content - a descriptor of type 'inline' requires ` +
|
|
451
|
+
`a content string, got ${describeValue(value.content)}. ` +
|
|
452
|
+
`Fix: build the descriptor with store() or createDescriptor() instead of by hand.`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
const encoding = value.encoding || 'utf-8';
|
|
456
|
+
return Buffer.from(value.content, encoding);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
throw new Error(
|
|
460
|
+
`[ContentResolver] Descriptor with unsupported type - expected type 'inline' or 'file', got ` +
|
|
461
|
+
`${describeValue(value.type)}. ` +
|
|
462
|
+
`Fix: build the descriptor with store() or createDescriptor() instead of by hand.`
|
|
463
|
+
);
|
|
337
464
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return Buffer.from(String(value), 'utf-8');
|
|
465
|
+
|
|
466
|
+
throw unsupportedValueError('getAsBuffer', ACCEPTS.contentValue, value, FIXES.wrap);
|
|
341
467
|
}
|
|
342
468
|
|
|
343
469
|
/**
|
|
@@ -352,8 +478,16 @@ class ContentResolver {
|
|
|
352
478
|
|
|
353
479
|
/**
|
|
354
480
|
* Get metadata from value - unified API
|
|
481
|
+
*
|
|
482
|
+
* Accepts exactly what `getAsBuffer()` accepts, because it is called on the
|
|
483
|
+
* SAME value one line later (api_biz/emailer/src/services/email.service.js:431
|
|
484
|
+
* → :434). While this method coerced with `String(value)`, the pair disagreed:
|
|
485
|
+
* a value `getAsBuffer()` refused was still described here as a 15-byte
|
|
486
|
+
* `content.txt` — the metadata of the text `[object Object]`.
|
|
487
|
+
*
|
|
355
488
|
* @param {string|Object} value - String, reference, or Content Descriptor
|
|
356
489
|
* @returns {Object} Metadata object with filename, content_type, size, fingerprint
|
|
490
|
+
* @throws {Error} If the value is not a string or a Descriptor
|
|
357
491
|
*/
|
|
358
492
|
getMetadata(value) {
|
|
359
493
|
// Plain string
|
|
@@ -375,13 +509,7 @@ class ContentResolver {
|
|
|
375
509
|
};
|
|
376
510
|
}
|
|
377
511
|
|
|
378
|
-
|
|
379
|
-
const str = String(value);
|
|
380
|
-
return {
|
|
381
|
-
filename: 'content.txt',
|
|
382
|
-
content_type: 'text/plain',
|
|
383
|
-
size: Buffer.byteLength(str, 'utf-8')
|
|
384
|
-
};
|
|
512
|
+
throw unsupportedValueError('getMetadata', ACCEPTS.contentValue, value, FIXES.wrap);
|
|
385
513
|
}
|
|
386
514
|
|
|
387
515
|
/**
|
|
@@ -393,11 +521,17 @@ class ContentResolver {
|
|
|
393
521
|
* @param {Object} [options.context] - Workflow context
|
|
394
522
|
* @param {boolean} [options.forceFile=false] - Force storage as file even if small
|
|
395
523
|
* @returns {Promise<Object>} Content Descriptor
|
|
524
|
+
* @throws {Error} If the content is neither a string nor a Buffer
|
|
396
525
|
*/
|
|
397
526
|
async createDescriptor(content, options = {}) {
|
|
398
527
|
const { filename, content_type, context = {}, forceFile = false } = options;
|
|
399
|
-
|
|
400
|
-
//
|
|
528
|
+
|
|
529
|
+
// A string or a Buffer, and nothing else. Buffer is a MEASURED input type,
|
|
530
|
+
// not a leniency path: `storeOutput()` forwards Buffers by name (:289) and
|
|
531
|
+
// `normalizeToDescriptor()` hands over the bytes it just downloaded (:577).
|
|
532
|
+
// Anything else used to be coerced with `String(content)`, which stored the
|
|
533
|
+
// eight bytes of `[object Object]` under a sha256 of themselves and reported
|
|
534
|
+
// it back as a valid descriptor.
|
|
401
535
|
let contentString;
|
|
402
536
|
let isBinary = false;
|
|
403
537
|
if (Buffer.isBuffer(content)) {
|
|
@@ -406,7 +540,7 @@ class ContentResolver {
|
|
|
406
540
|
} else if (typeof content === 'string') {
|
|
407
541
|
contentString = content;
|
|
408
542
|
} else {
|
|
409
|
-
|
|
543
|
+
throw unsupportedValueError('createDescriptor', ACCEPTS.rawContent, content, FIXES.serialize);
|
|
410
544
|
}
|
|
411
545
|
|
|
412
546
|
// For binary content, always store as file
|
|
@@ -492,9 +626,18 @@ class ContentResolver {
|
|
|
492
626
|
|
|
493
627
|
/**
|
|
494
628
|
* Normalize value to Content Descriptor if needed
|
|
629
|
+
*
|
|
630
|
+
* Accepts the same set as `getAsBuffer()`: a string or a Descriptor. A Buffer
|
|
631
|
+
* is NOT in that set even though `createDescriptor()` takes one — this method
|
|
632
|
+
* only RECOGNISES a value, and a bare Buffer carries none of the
|
|
633
|
+
* filename/content-type context `createDescriptor()` takes as arguments. It
|
|
634
|
+
* used to be swallowed by `String(value)`, which quietly decoded the bytes
|
|
635
|
+
* into an inline text descriptor via `Buffer.prototype.toString`.
|
|
636
|
+
*
|
|
495
637
|
* @param {string|Object} value - String, reference, or Descriptor
|
|
496
638
|
* @param {Object} options - Options for descriptor creation
|
|
497
639
|
* @returns {Promise<Object>} Content Descriptor
|
|
640
|
+
* @throws {Error} If the value is not a string or a Descriptor
|
|
498
641
|
*/
|
|
499
642
|
async normalizeToDescriptor(value, options = {}) {
|
|
500
643
|
// Already a Descriptor - ensure it has _descriptor flag
|
|
@@ -518,9 +661,8 @@ class ContentResolver {
|
|
|
518
661
|
// Plain string - create descriptor
|
|
519
662
|
return await this.createDescriptor(value, options);
|
|
520
663
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return await this.createDescriptor(String(value), options);
|
|
664
|
+
|
|
665
|
+
throw unsupportedValueError('normalizeToDescriptor', ACCEPTS.contentValue, value, FIXES.wrap);
|
|
524
666
|
}
|
|
525
667
|
|
|
526
668
|
/**
|