@labelgrid/core
Advanced tools
| /** | ||
| * Shared parsing for the configurable request/transfer timeouts, used by both | ||
| * the MCP server (env vars) and the CLI (flags and env vars). A value must be a | ||
| * positive integer number of milliseconds; anything else is rejected so the | ||
| * caller can fall back to the client default and warn once. | ||
| */ | ||
| export type TimeoutParse = { | ||
| /** The parsed positive-integer ms, or undefined when unset OR invalid. */ | ||
| value: number | undefined; | ||
| /** True only when a value was supplied but was not a positive integer. */ | ||
| invalid: boolean; | ||
| }; | ||
| /** Parses a timeout string into a positive-integer millisecond value. */ | ||
| export declare function parseTimeoutMs(raw: string | undefined): TimeoutParse; |
| /** | ||
| * Shared parsing for the configurable request/transfer timeouts, used by both | ||
| * the MCP server (env vars) and the CLI (flags and env vars). A value must be a | ||
| * positive integer number of milliseconds; anything else is rejected so the | ||
| * caller can fall back to the client default and warn once. | ||
| */ | ||
| /** Parses a timeout string into a positive-integer millisecond value. */ | ||
| export function parseTimeoutMs(raw) { | ||
| if (raw === undefined || raw.trim() === '') | ||
| return { value: undefined, invalid: false }; | ||
| const n = Number(raw.trim()); | ||
| if (Number.isInteger(n) && n > 0) | ||
| return { value: n, invalid: false }; | ||
| return { value: undefined, invalid: true }; | ||
| } |
+40
-0
@@ -11,2 +11,42 @@ # Changelog | ||
| ## [0.2.0] - 2026-07-23 | ||
| ### Added | ||
| - `MAX_UPLOAD_BYTES` (4 GiB) upload ceiling and a `FILE_TOO_LARGE` structured | ||
| error. An oversized file is now rejected with an honest size-and-limit | ||
| message instead of being mislabeled `FILE_NOT_FOUND`. | ||
| - `mintUpload`, `putToPresignedUrl` and `commitUpload` — the presigned upload | ||
| flow's three steps are now individually exported and composed by | ||
| `uploadViaPresignedUrl`, so an alternate transport can reuse mint + commit and | ||
| swap only the byte-transfer step. Pure refactor; no behavior change. | ||
| - `LabelGridClient.getRaw(path, query?)` — an authenticated raw GET for file | ||
| downloads that returns the live response for streaming, or the same | ||
| normalized structured error as the JSON path, honoring the raw transfer | ||
| timeout. Consolidates the duplicated authed-download helpers that lived in the | ||
| MCP and CLI packages. | ||
| ### Changed | ||
| - Presigned-URL uploads now stream the file from disk with an explicit | ||
| `Content-Length` header instead of buffering the whole file into memory, so | ||
| peak memory stays flat regardless of file size. (An explicit `Content-Length` | ||
| is required — S3-compatible presigned PUTs reject a chunked body with 411.) | ||
| - Multipart uploads no longer make a redundant in-memory copy of the file bytes | ||
| before building the form Blob. | ||
| - `UploadOptions` accepts an optional `onProgress` callback, invoked with the | ||
| running byte count as the presigned PUT streams (for a transfer-progress UI). | ||
| ### Fixed | ||
| - Multipart uploads (cover art, license documents) now use the longer raw | ||
| transfer timeout instead of the 60s JSON request timeout, so a large file on | ||
| a slow uplink is no longer aborted mid-upload. | ||
| - `raw()` now composes a caller-supplied `AbortSignal` with the transfer-timeout | ||
| signal (`AbortSignal.any`) instead of letting the caller's signal silently | ||
| replace and disable the timeout. | ||
| - Presigned-upload progress reporting now forwards a source-stream error onto | ||
| the composed request body, so a file that becomes unreadable mid-transfer | ||
| fails cleanly as `UPLOAD_FAILED` instead of raising an unhandled stream error. | ||
| ## [0.1.0] - 2026-07-20 | ||
@@ -13,0 +53,0 @@ |
+23
-0
@@ -26,2 +26,13 @@ /** | ||
| }; | ||
| /** | ||
| * The outcome of a raw authenticated GET: either the live {@link Response} (for | ||
| * the caller to stream a file body from) or a normalized structured error. | ||
| */ | ||
| export type RawResult = { | ||
| ok: true; | ||
| res: Response; | ||
| } | { | ||
| ok: false; | ||
| error: ApiError; | ||
| }; | ||
| export declare class LabelGridClient { | ||
@@ -78,2 +89,14 @@ private readonly baseUrl; | ||
| raw(url: string, init: RequestInit): Promise<Response>; | ||
| /** | ||
| * Authenticated raw GET for file-body downloads (statement CSV/PDF): sends the | ||
| * same auth headers as the JSON path but returns the live {@link Response} on | ||
| * success so the caller streams the body to disk, never buffering a large file | ||
| * in memory. A non-2xx is read and normalized to the same structured | ||
| * {@link ApiError} as the JSON path; a network/timeout failure maps to the same | ||
| * NETWORK_ERROR/TIMEOUT. Uses the longer raw transfer timeout, since a download | ||
| * is a byte transfer, not a JSON call. | ||
| */ | ||
| getRaw(path: string, query?: Record<string, unknown>): Promise<RawResult>; | ||
| /** Maps a fetch rejection (abort/timeout vs other) to a structured error. */ | ||
| private mapFetchError; | ||
| } |
+84
-5
@@ -196,3 +196,7 @@ /** | ||
| } | ||
| const init = { method, headers, signal: AbortSignal.timeout(this.timeoutMs) }; | ||
| // A raw body is a byte transfer (a multipart upload), not a JSON call — it | ||
| // gets the longer transfer timeout so a large file on a slow uplink is not | ||
| // aborted at the 60s JSON deadline. | ||
| const effectiveTimeoutMs = opts.rawBody !== undefined ? this.rawTimeoutMs : this.timeoutMs; | ||
| const init = { method, headers, signal: AbortSignal.timeout(effectiveTimeoutMs) }; | ||
| if (opts.rawBody !== undefined) { | ||
@@ -215,3 +219,3 @@ init.body = opts.rawBody; | ||
| code: 'TIMEOUT', | ||
| message: `The request timed out after ${Math.round(this.timeoutMs / 1000)} seconds. Try again, or narrow the request.`, | ||
| message: `The request timed out after ${Math.round(effectiveTimeoutMs / 1000)} seconds. Try again, or narrow the request.`, | ||
| status: 0, | ||
@@ -262,3 +266,3 @@ }, | ||
| code: 'TIMEOUT', | ||
| message: `The request timed out after ${Math.round(this.timeoutMs / 1000)} seconds while reading the response. Try again, or narrow the request.`, | ||
| message: `The request timed out after ${Math.round(effectiveTimeoutMs / 1000)} seconds while reading the response. Try again, or narrow the request.`, | ||
| status: 0, | ||
@@ -382,3 +386,16 @@ }, | ||
| const form = new FormData(); | ||
| form.append(fieldName, new Blob([new Uint8Array(bytes)], { type: contentType(filePath) }), basename(filePath)); | ||
| // Wrap the buffer's bytes in a zero-copy Uint8Array VIEW (same underlying | ||
| // memory, honoring byteOffset/byteLength on a pooled Buffer), which the Blob | ||
| // then copies once. The previous `new Blob([new Uint8Array(bytes)])` made an | ||
| // extra full copy before the Blob's own copy — that redundant copy is gone. | ||
| // Node's FormData accepts a Blob field. Streaming multipart is not practical | ||
| // with undici's FormData (it materializes the parts), so this stays a single | ||
| // in-memory buffer — acceptable for the small files (images, PDFs, lyrics) | ||
| // that use the multipart path; the large binaries go through the streaming | ||
| // presigned-URL flow instead. | ||
| // The `as BlobPart` cast is only because the DOM lib types a Buffer's | ||
| // ArrayBufferLike (which could be a SharedArrayBuffer) too narrowly for | ||
| // BlobPart; the view is a valid Blob part at runtime. | ||
| const view = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength); | ||
| form.append(fieldName, new Blob([view], { type: contentType(filePath) }), basename(filePath)); | ||
| for (const [key, value] of Object.entries(extra ?? {})) { | ||
@@ -396,4 +413,66 @@ form.append(key, value); | ||
| raw(url, init) { | ||
| return this.fetchFn(url, { signal: AbortSignal.timeout(this.rawTimeoutMs), ...init }); | ||
| const timeoutSignal = AbortSignal.timeout(this.rawTimeoutMs); | ||
| // Compose, never replace: spreading `...init` last would let a caller-supplied | ||
| // signal silently drop the transfer timeout. AbortSignal.any aborts when | ||
| // EITHER the caller's signal or the timeout fires, so the timeout always holds. | ||
| const signal = init.signal != null ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal; | ||
| return this.fetchFn(url, { ...init, signal }); | ||
| } | ||
| /** | ||
| * Authenticated raw GET for file-body downloads (statement CSV/PDF): sends the | ||
| * same auth headers as the JSON path but returns the live {@link Response} on | ||
| * success so the caller streams the body to disk, never buffering a large file | ||
| * in memory. A non-2xx is read and normalized to the same structured | ||
| * {@link ApiError} as the JSON path; a network/timeout failure maps to the same | ||
| * NETWORK_ERROR/TIMEOUT. Uses the longer raw transfer timeout, since a download | ||
| * is a byte transfer, not a JSON call. | ||
| */ | ||
| async getRaw(path, query) { | ||
| const url = `${this.baseUrl}${path}${buildQuery(query)}`; | ||
| let res; | ||
| try { | ||
| res = await this.fetchFn(url, { | ||
| method: 'GET', | ||
| headers: this.authHeaders(), | ||
| signal: AbortSignal.timeout(this.rawTimeoutMs), | ||
| }); | ||
| } | ||
| catch (err) { | ||
| return { ok: false, error: this.mapFetchError(err) }; | ||
| } | ||
| if (res.ok) | ||
| return { ok: true, res }; | ||
| // Read and normalize the error body exactly as the JSON path does. | ||
| let body = null; | ||
| try { | ||
| const text = await res.text(); | ||
| if (text.length > 0) { | ||
| try { | ||
| body = JSON.parse(text); | ||
| } | ||
| catch { | ||
| body = text; | ||
| } | ||
| } | ||
| } | ||
| catch { | ||
| // no readable error body — normalizeError falls back to status defaults | ||
| } | ||
| return { ok: false, error: normalizeError(res, body) }; | ||
| } | ||
| /** Maps a fetch rejection (abort/timeout vs other) to a structured error. */ | ||
| mapFetchError(err) { | ||
| if (err instanceof DOMException && (err.name === 'TimeoutError' || err.name === 'AbortError')) { | ||
| return { | ||
| code: 'TIMEOUT', | ||
| message: `The request timed out after ${Math.round(this.rawTimeoutMs / 1000)} seconds. Try again, or narrow the request.`, | ||
| status: 0, | ||
| }; | ||
| } | ||
| return { | ||
| code: 'NETWORK_ERROR', | ||
| message: err instanceof Error ? err.message : 'Network request failed.', | ||
| status: 0, | ||
| }; | ||
| } | ||
| } |
+50
-1
@@ -16,5 +16,23 @@ /** | ||
| * never finalized. Business rules (format checks, transcoding) stay server-side. | ||
| * | ||
| * The step-2 PUT streams the file from disk rather than buffering it whole: the | ||
| * request body is a read stream and the object's byte size is sent as an | ||
| * explicit Content-Length. (An S3-compatible presigned PUT rejects a chunked | ||
| * Transfer-Encoding body with 411 MissingContentLength, and fetch defaults a | ||
| * stream body to chunked, so the header is mandatory.) This keeps peak memory | ||
| * flat regardless of file size. | ||
| */ | ||
| import type { ApiResult, LabelGridClient } from './http.js'; | ||
| import type { ApiError, ApiResult, LabelGridClient } from './http.js'; | ||
| /** Hard ceiling on a single uploaded file, in bytes (4 GiB). */ | ||
| export declare const MAX_UPLOAD_BYTES: number; | ||
| /** | ||
| * Composes a byte-counting Transform onto `src` for progress reporting and — | ||
| * critically — forwards a SOURCE error onto the composed body. `pipe()` does NOT | ||
| * propagate a source-stream error to its destination, so without this a file | ||
| * that becomes unreadable mid-transfer (deleted, an I/O fault) would emit an | ||
| * unhandled 'error' on the read stream — crashing the process — instead of | ||
| * destroying the composed body so `fetch` rejects into the UPLOAD_FAILED catch. | ||
| */ | ||
| export declare function attachProgressCounter(src: NodeJS.ReadableStream, onProgress: (bytesSoFar: number) => void): NodeJS.ReadableStream; | ||
| /** | ||
| * The structural subset of {@link LabelGridClient} the presigned-upload flow | ||
@@ -32,3 +50,34 @@ * needs. Declared as a Pick so any object with these methods (including a test | ||
| filePath: string; | ||
| /** Byte ceiling override (defaults to {@link MAX_UPLOAD_BYTES}); for tests. */ | ||
| maxBytes?: number; | ||
| /** Called with the running byte count as the upload streams (for a progress UI). */ | ||
| onProgress?: (bytesSoFar: number) => void; | ||
| }; | ||
| /** | ||
| * THE SEAM: the presigned flow is three independently-callable steps — | ||
| * {@link mintUpload} (get a signed URL + key), {@link putToPresignedUrl} (send | ||
| * the bytes), {@link commitUpload} (record the object) — and | ||
| * {@link uploadViaPresignedUrl} just composes them. An alternate transport that | ||
| * does the PUT out of process (e.g. a browser or a worker doing the byte | ||
| * transfer directly) can reuse mint + commit and swap only the middle step, | ||
| * without reimplementing the URL-minting or commit contracts. | ||
| */ | ||
| /** The result of minting a presigned URL: the signed URL + object key, or an error. */ | ||
| export type MintResult = { | ||
| uploadUrl: string; | ||
| key: string; | ||
| } | { | ||
| error: ApiError; | ||
| }; | ||
| /** Step 1: mint the presigned URL + object key for `filename`. */ | ||
| export declare function mintUpload(client: Pick<UploadHttp, 'post'>, uploadUrlPath: string, filename: string): Promise<MintResult>; | ||
| /** | ||
| * Step 2: stream the file's bytes to the presigned URL — NO auth header (the URL | ||
| * is signed). Re-stats the file so Content-Length is its size at upload time, | ||
| * enforcing the byte ceiling and the FILE_NOT_FOUND (TOCTOU) contract there. | ||
| * Returns null on success, or a structured error. | ||
| */ | ||
| export declare function putToPresignedUrl(client: Pick<UploadHttp, 'raw'>, uploadUrl: string, filePath: string, maxBytes?: number, onProgress?: (bytesSoFar: number) => void): Promise<ApiError | null>; | ||
| /** Step 3: commit the uploaded object key (idempotent — a retried commit will not duplicate). */ | ||
| export declare function commitUpload(client: Pick<UploadHttp, 'put'>, commitPath: string, key: string): Promise<ApiResult<unknown>>; | ||
| export declare function uploadViaPresignedUrl(client: UploadHttp, opts: UploadOptions): Promise<ApiResult<unknown>>; |
+123
-51
@@ -16,65 +16,114 @@ /** | ||
| * never finalized. Business rules (format checks, transcoding) stay server-side. | ||
| * | ||
| * The step-2 PUT streams the file from disk rather than buffering it whole: the | ||
| * request body is a read stream and the object's byte size is sent as an | ||
| * explicit Content-Length. (An S3-compatible presigned PUT rejects a chunked | ||
| * Transfer-Encoding body with 411 MissingContentLength, and fetch defaults a | ||
| * stream body to chunked, so the header is mandatory.) This keeps peak memory | ||
| * flat regardless of file size. | ||
| */ | ||
| import { statSync } from 'node:fs'; | ||
| import { readFile } from 'node:fs/promises'; | ||
| import { createReadStream, statSync } from 'node:fs'; | ||
| import { basename } from 'node:path'; | ||
| import { Transform } from 'node:stream'; | ||
| import { log } from '../log.js'; | ||
| import { contentType } from './content-types.js'; | ||
| /** True only for an existing regular file. */ | ||
| function isReadableFile(p) { | ||
| /** Hard ceiling on a single uploaded file, in bytes (4 GiB). */ | ||
| export const MAX_UPLOAD_BYTES = 4 * 1024 * 1024 * 1024; | ||
| /** Stats a path, returning its size only for an existing regular file. */ | ||
| function statReadableFile(p) { | ||
| try { | ||
| return statSync(p).isFile(); | ||
| const st = statSync(p); | ||
| return st.isFile() ? { size: st.size } : null; | ||
| } | ||
| catch { | ||
| return false; | ||
| return null; | ||
| } | ||
| } | ||
| export async function uploadViaPresignedUrl(client, opts) { | ||
| // Fail fast and locally: never touch the network for a file we cannot read. | ||
| if (!isReadableFile(opts.filePath)) { | ||
| const error = { | ||
| code: 'FILE_NOT_FOUND', | ||
| message: `No readable file at ${opts.filePath}.`, | ||
| status: 0, | ||
| }; | ||
| return { error }; | ||
| } | ||
| // Step 1: mint the presigned URL. | ||
| const minted = await client.post(opts.uploadUrlPath, { | ||
| filename: basename(opts.filePath), | ||
| /** | ||
| * Composes a byte-counting Transform onto `src` for progress reporting and — | ||
| * critically — forwards a SOURCE error onto the composed body. `pipe()` does NOT | ||
| * propagate a source-stream error to its destination, so without this a file | ||
| * that becomes unreadable mid-transfer (deleted, an I/O fault) would emit an | ||
| * unhandled 'error' on the read stream — crashing the process — instead of | ||
| * destroying the composed body so `fetch` rejects into the UPLOAD_FAILED catch. | ||
| */ | ||
| export function attachProgressCounter(src, onProgress) { | ||
| let transferred = 0; | ||
| const counter = new Transform({ | ||
| transform(chunk, _enc, cb) { | ||
| transferred += chunk.length; | ||
| onProgress(transferred); | ||
| cb(null, chunk); | ||
| }, | ||
| }); | ||
| src.pipe(counter); | ||
| src.on('error', (e) => counter.destroy(e instanceof Error ? e : new Error(String(e)))); | ||
| return counter; | ||
| } | ||
| function fileTooLargeError(size, limit) { | ||
| return { | ||
| code: 'FILE_TOO_LARGE', | ||
| message: `The file is ${size} bytes, over the ${limit}-byte upload limit.`, | ||
| status: 0, | ||
| }; | ||
| } | ||
| /** Step 1: mint the presigned URL + object key for `filename`. */ | ||
| export async function mintUpload(client, uploadUrlPath, filename) { | ||
| const minted = await client.post(uploadUrlPath, { filename }); | ||
| if ('error' in minted) | ||
| return minted; | ||
| return { error: minted.error }; | ||
| const uploadUrl = minted.data?.upload_url; | ||
| const key = minted.data?.key; | ||
| if (typeof uploadUrl !== 'string' || typeof key !== 'string') { | ||
| const error = { | ||
| code: 'UPLOAD_URL_INVALID', | ||
| message: 'The upload-url response did not contain a usable upload_url and key.', | ||
| status: 0, | ||
| return { | ||
| error: { | ||
| code: 'UPLOAD_URL_INVALID', | ||
| message: 'The upload-url response did not contain a usable upload_url and key.', | ||
| status: 0, | ||
| }, | ||
| }; | ||
| return { error }; | ||
| } | ||
| // Step 2: PUT the bytes directly to storage — NO auth header (the URL is signed). | ||
| // The file passed isReadableFile above, but it can vanish before this read | ||
| // (a TOCTOU race); a structured FILE_NOT_FOUND is the contract, not a throw. | ||
| let bytes; | ||
| try { | ||
| bytes = await readFile(opts.filePath); | ||
| } | ||
| catch { | ||
| const error = { | ||
| return { uploadUrl, key }; | ||
| } | ||
| /** | ||
| * Step 2: stream the file's bytes to the presigned URL — NO auth header (the URL | ||
| * is signed). Re-stats the file so Content-Length is its size at upload time, | ||
| * enforcing the byte ceiling and the FILE_NOT_FOUND (TOCTOU) contract there. | ||
| * Returns null on success, or a structured error. | ||
| */ | ||
| export async function putToPresignedUrl(client, uploadUrl, filePath, maxBytes = MAX_UPLOAD_BYTES, onProgress) { | ||
| // The file may vanish between an earlier stat and this transfer (a TOCTOU | ||
| // race); a structured FILE_NOT_FOUND is the contract, not a throw. | ||
| const stat = statReadableFile(filePath); | ||
| if (stat === null) { | ||
| return { | ||
| code: 'FILE_NOT_FOUND', | ||
| message: `The file at ${opts.filePath} could not be read.`, | ||
| message: `The file at ${filePath} could not be read.`, | ||
| status: 0, | ||
| }; | ||
| return { error }; | ||
| } | ||
| if (stat.size > maxBytes) | ||
| return fileTooLargeError(stat.size, maxBytes); | ||
| let putRes; | ||
| try { | ||
| putRes = await client.raw(uploadUrl, { | ||
| // A stream body must declare Content-Length (a chunked PUT is rejected 411 by | ||
| // S3-compatible storage) and requires duplex: 'half' for undici's fetch. | ||
| let body = createReadStream(filePath); | ||
| if (onProgress !== undefined) { | ||
| // Count bytes as they pass through, inside the Transform (never a 'data' | ||
| // listener, which would flip the stream to flowing mode and race the | ||
| // reader). attachProgressCounter also forwards a source-stream error so a | ||
| // mid-transfer read failure becomes a clean UPLOAD_FAILED, not a crash. | ||
| body = attachProgressCounter(body, onProgress); | ||
| } | ||
| const putInit = { | ||
| method: 'PUT', | ||
| headers: { 'Content-Type': contentType(opts.filePath) }, | ||
| body: new Uint8Array(bytes), | ||
| }); | ||
| headers: { | ||
| 'Content-Type': contentType(filePath), | ||
| 'Content-Length': String(stat.size), | ||
| }, | ||
| body, | ||
| duplex: 'half', | ||
| }; | ||
| putRes = await client.raw(uploadUrl, putInit); | ||
| } | ||
@@ -87,12 +136,6 @@ catch (err) { | ||
| }); | ||
| const error = { | ||
| code: 'UPLOAD_FAILED', | ||
| message: 'Uploading the file to storage failed.', | ||
| status: 0, | ||
| }; | ||
| return { error }; | ||
| return { code: 'UPLOAD_FAILED', message: 'Uploading the file to storage failed.', status: 0 }; | ||
| } | ||
| if (!putRes.ok) { | ||
| // Abort BEFORE the commit — a half-uploaded object is never finalized. | ||
| const error = { | ||
| return { | ||
| code: 'UPLOAD_FAILED', | ||
@@ -102,6 +145,35 @@ message: `Uploading the file to storage failed with status ${putRes.status}.`, | ||
| }; | ||
| return { error }; | ||
| } | ||
| // Step 3: commit the object key (idempotent — a retried commit will not duplicate). | ||
| return client.put(opts.commitPath, { s3_key: key }, { idempotency: true }); | ||
| return null; | ||
| } | ||
| /** Step 3: commit the uploaded object key (idempotent — a retried commit will not duplicate). */ | ||
| export function commitUpload(client, commitPath, key) { | ||
| return client.put(commitPath, { s3_key: key }, { idempotency: true }); | ||
| } | ||
| export async function uploadViaPresignedUrl(client, opts) { | ||
| const limit = opts.maxBytes ?? MAX_UPLOAD_BYTES; | ||
| // Fail fast and locally: never touch the network for a file we cannot read, | ||
| // and reject an oversized file with an honest size error (not FILE_NOT_FOUND). | ||
| const initialStat = statReadableFile(opts.filePath); | ||
| if (initialStat === null) { | ||
| return { | ||
| error: { | ||
| code: 'FILE_NOT_FOUND', | ||
| message: `No readable file at ${opts.filePath}.`, | ||
| status: 0, | ||
| }, | ||
| }; | ||
| } | ||
| if (initialStat.size > limit) { | ||
| return { error: fileTooLargeError(initialStat.size, limit) }; | ||
| } | ||
| // Compose the seam: mint → PUT the bytes → commit. A failed PUT aborts before | ||
| // the commit, so a half-uploaded object is never finalized. | ||
| const minted = await mintUpload(client, opts.uploadUrlPath, basename(opts.filePath)); | ||
| if ('error' in minted) | ||
| return { error: minted.error }; | ||
| const putError = await putToPresignedUrl(client, minted.uploadUrl, opts.filePath, limit, opts.onProgress); | ||
| if (putError !== null) | ||
| return { error: putError }; | ||
| return commitUpload(client, opts.commitPath, minted.key); | ||
| } |
+1
-0
@@ -14,1 +14,2 @@ /** | ||
| export * from './log.js'; | ||
| export * from './timeouts.js'; |
+1
-0
@@ -14,1 +14,2 @@ /** | ||
| export * from './log.js'; | ||
| export * from './timeouts.js'; |
+12
-3
| { | ||
| "name": "@labelgrid/core", | ||
| "version": "0.1.0", | ||
| "version": "0.2.0", | ||
| "description": "Shared LabelGrid public-API client: HTTP transport, uploads, content types, the catalog-entity registry, and redacting logging", | ||
| "type": "module", | ||
| "keywords": ["labelgrid", "music-distribution", "api-client"], | ||
| "keywords": [ | ||
| "labelgrid", | ||
| "music-distribution", | ||
| "api-client" | ||
| ], | ||
| "main": "dist/index.js", | ||
@@ -15,3 +19,8 @@ "types": "dist/index.d.ts", | ||
| }, | ||
| "files": ["dist", "README.md", "CHANGELOG.md", "LICENSE"], | ||
| "files": [ | ||
| "dist", | ||
| "README.md", | ||
| "CHANGELOG.md", | ||
| "LICENSE" | ||
| ], | ||
| "scripts": { | ||
@@ -18,0 +27,0 @@ "build": "tsc", |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
56597
37.3%18
12.5%1136
28.8%3
-25%