@wtfalch/files 0.4.0 → 0.6.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 +27 -0
- package/dist/client.d.ts +22 -3
- package/dist/client.js +17 -2
- package/dist/types.d.ts +51 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,22 @@ of the same operation should retain its request ID. API failures throw
|
|
|
32
32
|
`ApiError` with `status` and `code`; the SDK does not expose raw upstream error
|
|
33
33
|
bodies.
|
|
34
34
|
|
|
35
|
+
Large files can go up in parts. Reserve with `multipart: true`; the reply has
|
|
36
|
+
`state: 'multipart'`, `partBytes` and `partCount`, and the upload stays open for
|
|
37
|
+
a day. `uploadPartUrls(uploadId, from, count)` returns presigned URLs for up to
|
|
38
|
+
100 parts. PUT part n as bytes `[(n-1)*partBytes, n*partBytes)` and keep each
|
|
39
|
+
response's `ETag` header. Then call `confirmUpload(uploadId, parts)` with every
|
|
40
|
+
part in order. To resume, ask for fresh URLs for the parts that failed. A
|
|
41
|
+
multipart file has `checksum: null`. The R2 backend supports this; the
|
|
42
|
+
local-disk backend does not yet.
|
|
43
|
+
|
|
44
|
+
Files can sit in folders inside a collection. Pass `path` (for example
|
|
45
|
+
`reports/2026`) when you reserve an upload. `listFiles(collectionId, { path })`
|
|
46
|
+
lists the files directly in that folder, and `listFolders(collectionId, path)`
|
|
47
|
+
lists its subfolders. A folder exists while it holds a file, so there are no
|
|
48
|
+
empty folders. A replacement keeps the file's path; moving a file is not
|
|
49
|
+
supported yet.
|
|
50
|
+
|
|
35
51
|
The root export provides the client and public request/response types.
|
|
36
52
|
`@wtfalch/files/client` is also supported. There are no server, database,
|
|
37
53
|
migration, provider-adapter or authorization-engine exports. The service
|
|
@@ -40,6 +56,17 @@ implementation is maintained in a private repository.
|
|
|
40
56
|
The first 0.1.0 SDK targets the new service API. Production migration from Valet
|
|
41
57
|
is a separate rollout; installing the SDK does not migrate accounts or files.
|
|
42
58
|
|
|
59
|
+
## Migrating from `@wtfalch/storage`
|
|
60
|
+
|
|
61
|
+
`@wtfalch/storage` is this same SDK under its pre-rename name (the 2026-09-20
|
|
62
|
+
rename, [#26](https://github.com/wtfalch/files/pull/26)); its `dist/` is
|
|
63
|
+
byte-identical to `@wtfalch/files` apart from renamed exports and the
|
|
64
|
+
reference prefix. It receives no further fixes. Install `@wtfalch/files` and
|
|
65
|
+
replace `createStorageClient` with `createFilesClient` (same shape). npm does
|
|
66
|
+
not yet show `@wtfalch/storage` as deprecated: that requires `npm deprecate`,
|
|
67
|
+
run by someone holding publish credentials for it, tracked in
|
|
68
|
+
[#29](https://github.com/wtfalch/files/issues/29).
|
|
69
|
+
|
|
43
70
|
## Management integration
|
|
44
71
|
|
|
45
72
|
`createFilesManagementClient` covers operator rates, organisations and audit,
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ClientOptions, type ManagementClientOptions } from './transport.js';
|
|
2
|
-
import type { AuditPage, AuditQuery, BudgetSummary, CreatedShare, FilesRate, OrganisationShare, OrganisationSummary, ProvisionOrganisationInput, ProvisionOrganisationResult, QuotaSummary, SetRateInput, Share, StoredFile, UpdateOrganisationCeilingInput, UploadInput, UploadReservation, UsageDay } from './types.js';
|
|
2
|
+
import type { AuditPage, AuditQuery, BudgetSummary, CreatedShare, FilesRate, OrganisationShare, OrganisationSummary, ProvisionOrganisationInput, ProvisionOrganisationResult, QuotaSummary, SetRateInput, SetWebhookResult, Share, StoredFile, UpdateOrganisationCeilingInput, UploadInput, UploadReservation, UploadedPart, UsageDay, WebhookConfig } from './types.js';
|
|
3
3
|
export { ApiError, type ClientOptions, type ManagementClientOptions } from './transport.js';
|
|
4
4
|
export declare function createFilesClient(options: ClientOptions): {
|
|
5
5
|
createCollection: (name: string) => Promise<{
|
|
@@ -7,17 +7,34 @@ export declare function createFilesClient(options: ClientOptions): {
|
|
|
7
7
|
name: string;
|
|
8
8
|
}>;
|
|
9
9
|
reserveUpload: (input: UploadInput) => Promise<UploadReservation>;
|
|
10
|
-
|
|
10
|
+
/** A multipart upload passes the ETag of every part, in part order. */
|
|
11
|
+
confirmUpload: (id: string, parts?: UploadedPart[]) => Promise<StoredFile>;
|
|
12
|
+
/** Fresh URLs for up to 100 parts of a multipart upload, starting at
|
|
13
|
+
* part `from`. Call again to resume after a failed or expired part. */
|
|
14
|
+
uploadPartUrls: (id: string, from?: number, count?: number) => Promise<{
|
|
15
|
+
parts: {
|
|
16
|
+
partNumber: number;
|
|
17
|
+
url: string;
|
|
18
|
+
}[];
|
|
19
|
+
expiresAt: string;
|
|
20
|
+
}>;
|
|
11
21
|
getFile: (id: string) => Promise<StoredFile>;
|
|
12
|
-
downloadFile: (id: string
|
|
22
|
+
downloadFile: (id: string, options?: {
|
|
23
|
+
ttlSeconds?: number;
|
|
24
|
+
}) => Promise<{
|
|
13
25
|
url: string;
|
|
14
26
|
expiresIn: number;
|
|
15
27
|
}>;
|
|
16
28
|
deleteFile: (id: string) => Promise<void>;
|
|
29
|
+
/** `path` lists only the files directly in that folder. */
|
|
17
30
|
listFiles: (collection: string, page?: {
|
|
18
31
|
after?: string;
|
|
19
32
|
limit?: number;
|
|
33
|
+
path?: string;
|
|
20
34
|
}) => Promise<StoredFile[]>;
|
|
35
|
+
/** The immediate subfolders of `path` ('' is the root) that hold at least
|
|
36
|
+
* one file the caller can read. */
|
|
37
|
+
listFolders: (collection: string, path?: string) => Promise<string[]>;
|
|
21
38
|
createShare: (fileId: string, expiresAt: string | null) => Promise<CreatedShare>;
|
|
22
39
|
listShares: (fileId: string) => Promise<Share[]>;
|
|
23
40
|
revokeShare: (shareId: string) => Promise<void>;
|
|
@@ -48,4 +65,6 @@ export declare function createFilesManagementClient(options: ManagementClientOpt
|
|
|
48
65
|
listOrganisationShares: () => Promise<{
|
|
49
66
|
shares: OrganisationShare[];
|
|
50
67
|
}>;
|
|
68
|
+
getWebhook: () => Promise<WebhookConfig>;
|
|
69
|
+
setWebhook: (url: string | null) => Promise<SetWebhookResult>;
|
|
51
70
|
};
|
package/dist/client.js
CHANGED
|
@@ -5,14 +5,26 @@ export function createFilesClient(options) {
|
|
|
5
5
|
return {
|
|
6
6
|
createCollection: (name) => call('POST', '/v1/collections', { name }),
|
|
7
7
|
reserveUpload: (input) => call('POST', '/v1/uploads', input),
|
|
8
|
-
|
|
8
|
+
/** A multipart upload passes the ETag of every part, in part order. */
|
|
9
|
+
confirmUpload: (id, parts) => call('POST', `/v1/uploads/${encodeURIComponent(id)}/confirm`, parts === undefined ? undefined : { parts }),
|
|
10
|
+
/** Fresh URLs for up to 100 parts of a multipart upload, starting at
|
|
11
|
+
* part `from`. Call again to resume after a failed or expired part. */
|
|
12
|
+
uploadPartUrls: (id, from = 1, count = 100) => call('GET', `/v1/uploads/${encodeURIComponent(id)}/parts?${new URLSearchParams({
|
|
13
|
+
from: String(from),
|
|
14
|
+
count: String(count),
|
|
15
|
+
})}`),
|
|
9
16
|
getFile: (id) => call('GET', `/v1/files/${encodeURIComponent(id)}`),
|
|
10
|
-
downloadFile: (id) => call('GET', `/v1/files/${encodeURIComponent(id)}/download`),
|
|
17
|
+
downloadFile: (id, options = {}) => call('GET', `/v1/files/${encodeURIComponent(id)}/download${options.ttlSeconds === undefined ? '' : `?ttl=${options.ttlSeconds}`}`),
|
|
11
18
|
deleteFile: (id) => call('DELETE', `/v1/files/${encodeURIComponent(id)}`),
|
|
19
|
+
/** `path` lists only the files directly in that folder. */
|
|
12
20
|
listFiles: (collection, page = {}) => call('GET', `/v1/collections/${encodeURIComponent(collection)}/files?${new URLSearchParams({
|
|
13
21
|
...(page.after ? { after: page.after } : {}),
|
|
14
22
|
...(page.limit !== undefined ? { limit: String(page.limit) } : {}),
|
|
23
|
+
...(page.path !== undefined ? { path: page.path } : {}),
|
|
15
24
|
})}`),
|
|
25
|
+
/** The immediate subfolders of `path` ('' is the root) that hold at least
|
|
26
|
+
* one file the caller can read. */
|
|
27
|
+
listFolders: (collection, path = '') => call('GET', `/v1/collections/${encodeURIComponent(collection)}/folders?${new URLSearchParams({ path })}`).then((result) => result.folders),
|
|
16
28
|
// Public share links (task S6): `files.files:share`/`files.shares:read`,
|
|
17
29
|
// bearer key or signed request -- both reach the same /v1/files/*
|
|
18
30
|
// routes this client already calls.
|
|
@@ -61,5 +73,8 @@ export function createFilesManagementClient(options) {
|
|
|
61
73
|
// Public share links (task S6): every active link in the caller's
|
|
62
74
|
// organisation, with the file's name -- never the token or URL.
|
|
63
75
|
listOrganisationShares: () => call('GET', '/v1/manage/shares'),
|
|
76
|
+
// Outbound event delivery (task gap-36). setWebhook(null) disables it.
|
|
77
|
+
getWebhook: () => call('GET', '/v1/manage/webhook'),
|
|
78
|
+
setWebhook: (url) => call('PUT', '/v1/manage/webhook', { url }),
|
|
64
79
|
};
|
|
65
80
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -9,7 +9,22 @@ export interface UploadInput {
|
|
|
9
9
|
fileId: string;
|
|
10
10
|
version: number;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
/** 'scratch' keeps the service's own fixed default expiry; 'durable' never
|
|
13
|
+
* expires; `{ days }` is a caller-chosen expiry (1-3650 days) for a
|
|
14
|
+
* collection that wants auto-expiry on its own schedule instead of a
|
|
15
|
+
* hand-rolled cron job calling deleteFile(). A replacement upload must
|
|
16
|
+
* keep the same finite-vs-infinite category as the file it replaces, but
|
|
17
|
+
* does not reset or change an already-running expiry. */
|
|
18
|
+
retention: 'scratch' | 'durable' | {
|
|
19
|
+
days: number;
|
|
20
|
+
};
|
|
21
|
+
/** Upload in parts, resumable part by part (task gap-33). The reservation
|
|
22
|
+
* then has `state: 'multipart'`, and the upload stays open for a day. */
|
|
23
|
+
multipart?: boolean;
|
|
24
|
+
/** The folder inside the collection, e.g. `reports/2026`. Omitted or `''`
|
|
25
|
+
* is the root. '/'-separated, with no leading, trailing or empty segment.
|
|
26
|
+
* A replacement keeps the file's path. */
|
|
27
|
+
path?: string;
|
|
13
28
|
}
|
|
14
29
|
export interface StoredFile {
|
|
15
30
|
id: string;
|
|
@@ -20,6 +35,12 @@ export interface StoredFile {
|
|
|
20
35
|
version: number;
|
|
21
36
|
collectionId: string;
|
|
22
37
|
expiresAt: string | null;
|
|
38
|
+
/** `"md5:<hex>"` of the confirmed bytes, or `null` for a file confirmed
|
|
39
|
+
* before this field existed. Derived from the storage backend's own etag
|
|
40
|
+
* for a single-part upload; not present for a multipart one. */
|
|
41
|
+
checksum: string | null;
|
|
42
|
+
/** The folder inside the collection; `''` is the root. */
|
|
43
|
+
path: string;
|
|
23
44
|
}
|
|
24
45
|
export type UploadReservation = {
|
|
25
46
|
uploadId: string;
|
|
@@ -35,7 +56,21 @@ export type UploadReservation = {
|
|
|
35
56
|
'content-length': string;
|
|
36
57
|
};
|
|
37
58
|
expiresAt: string;
|
|
59
|
+
} | {
|
|
60
|
+
/** Reserved with `multipart: true` (task gap-33). Get part URLs from
|
|
61
|
+
* `uploadPartUrls`, PUT part n as bytes [(n-1)*partBytes, n*partBytes),
|
|
62
|
+
* keep each response's ETag header, then `confirmUpload(id, parts)`. */
|
|
63
|
+
uploadId: string;
|
|
64
|
+
fileId: string;
|
|
65
|
+
state: 'multipart';
|
|
66
|
+
partBytes: number;
|
|
67
|
+
partCount: number;
|
|
68
|
+
expiresAt: string;
|
|
38
69
|
};
|
|
70
|
+
export interface UploadedPart {
|
|
71
|
+
partNumber: number;
|
|
72
|
+
etag: string;
|
|
73
|
+
}
|
|
39
74
|
/** Public share links (task S6). `url` is returned only from `createShare`,
|
|
40
75
|
* once, and is never included in a listing -- `Share` (below) is what
|
|
41
76
|
* `listShares` returns instead. */
|
|
@@ -156,6 +191,21 @@ export interface UsageDay {
|
|
|
156
191
|
chargedMicros: string;
|
|
157
192
|
rateMicrosPerGbDay: number;
|
|
158
193
|
}
|
|
194
|
+
/** Outbound event delivery (task gap-36): at most one endpoint per
|
|
195
|
+
* organisation, POSTed a signed `files.upload_confirmed` or
|
|
196
|
+
* `files.quota_near_ceiling` event instead of the caller polling. Delivery
|
|
197
|
+
* is best-effort with no retry queue -- a receiver must be idempotent. */
|
|
198
|
+
export interface WebhookConfig {
|
|
199
|
+
url: string | null;
|
|
200
|
+
}
|
|
201
|
+
export interface SetWebhookResult extends WebhookConfig {
|
|
202
|
+
/** Present only in `setWebhook()`'s own response, and only when a url was
|
|
203
|
+
* set -- `getWebhook()` never returns it. Generated fresh on every
|
|
204
|
+
* successful set; this is the rotation mechanism. Verify a delivery's
|
|
205
|
+
* `x-files-webhook-signature: sha256=<hex hmac-sha256 of the raw body>`
|
|
206
|
+
* header with it. */
|
|
207
|
+
secret: string | null;
|
|
208
|
+
}
|
|
159
209
|
/** Operator audit filters. Omitted dates select the most recent 92 days. */
|
|
160
210
|
export interface AuditQuery {
|
|
161
211
|
organisation?: string;
|