@wtfalch/files 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/client.d.ts +51 -0
- package/dist/client.js +65 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/references.d.ts +3 -0
- package/dist/references.js +10 -0
- package/dist/transport.d.ts +31 -0
- package/dist/transport.js +99 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Falch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @wtfalch/files
|
|
2
|
+
|
|
3
|
+
Public client SDK and API types for the Files service. Requires Node 22+ or a
|
|
4
|
+
browser with Fetch and AbortSignal.timeout support. The client has no runtime
|
|
5
|
+
package dependencies.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @wtfalch/files
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createFilesClient, ApiError } from '@wtfalch/files';
|
|
13
|
+
|
|
14
|
+
const files = createFilesClient({
|
|
15
|
+
url: serviceOrigin,
|
|
16
|
+
credential: () => serviceKey,
|
|
17
|
+
organisationId,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const files = await files.listFiles(collectionId, { limit: 50 });
|
|
21
|
+
const download = await files.downloadFile(fileId);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use the deployed service's HTTPS origin and a scoped service credential. The
|
|
25
|
+
credential callback runs for every request, so rotation does not require a new
|
|
26
|
+
client. The client refuses redirects and omits browser cookies. An HTTP origin
|
|
27
|
+
is accepted only for localhost development. Do not embed a privileged service
|
|
28
|
+
credential in public browser code.
|
|
29
|
+
|
|
30
|
+
Requests require current service permissions and configured budgets. A retry
|
|
31
|
+
of the same operation should retain its request ID. API failures throw
|
|
32
|
+
`ApiError` with `status` and `code`; the SDK does not expose raw upstream error
|
|
33
|
+
bodies.
|
|
34
|
+
|
|
35
|
+
The root export provides the client and public request/response types.
|
|
36
|
+
`@wtfalch/files/client` is also supported. There are no server, database,
|
|
37
|
+
migration, provider-adapter or authorization-engine exports. The service
|
|
38
|
+
implementation is maintained in a private repository.
|
|
39
|
+
|
|
40
|
+
The first 0.1.0 SDK targets the new service API. Production migration from Valet
|
|
41
|
+
is a separate rollout; installing the SDK does not migrate accounts or files.
|
|
42
|
+
|
|
43
|
+
## Management integration
|
|
44
|
+
|
|
45
|
+
`createFilesManagementClient` covers operator rates, organisations and audit,
|
|
46
|
+
plus tenant quotas, budgets, usage and share listings. Supply an authenticated
|
|
47
|
+
`fetch` that signs a human management proof or attaches an issued management key.
|
|
48
|
+
The calling application's transport must check the current user's permissions
|
|
49
|
+
before using a service key on their behalf. The SDK adds no authentication itself.
|
|
50
|
+
HTTPS is required by default, with HTTP allowed for localhost development.
|
|
51
|
+
For a server-side management client on a trusted private network, explicitly set
|
|
52
|
+
`allowInsecureHttp: true` to use an origin such as `http://files:3000`.
|
|
53
|
+
This does not detect or secure private networks; the host application owns that
|
|
54
|
+
boundary. Redirects remain disabled and cookies omitted. The file client still
|
|
55
|
+
requires HTTPS outside localhost.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createFilesManagementClient } from '@wtfalch/files';
|
|
59
|
+
|
|
60
|
+
const management = createFilesManagementClient({
|
|
61
|
+
url: serviceOrigin,
|
|
62
|
+
fetch: authenticatedManagementFetch,
|
|
63
|
+
});
|
|
64
|
+
const query = {
|
|
65
|
+
from: '2026-09-01T00:00:00Z',
|
|
66
|
+
to: '2026-09-02T00:00:00Z',
|
|
67
|
+
limit: 200,
|
|
68
|
+
};
|
|
69
|
+
const page = await management.listAudit(query);
|
|
70
|
+
if (page.next) {
|
|
71
|
+
const nextPage = await management.listAudit({ ...query, cursor: page.next });
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Audit reads require `platform.audit:read`. Date ranges are capped at 92 days;
|
|
76
|
+
omitting both dates selects the most recent 92 days. Reuse the same filters and
|
|
77
|
+
pass `next` unchanged: converting its timestamp to a JavaScript `Date` loses
|
|
78
|
+
the microseconds needed for pagination. Audit types are exported as `AuditQuery`,
|
|
79
|
+
`AuditEvent` and `AuditPage`. CSV export remains available through
|
|
80
|
+
`GET /v1/manage/audit?format=csv`; its `x-next-cursor` header supplies continuation.
|
|
81
|
+
|
|
82
|
+
`provisionOrganisation()` returns the organisation fields plus `created`:
|
|
83
|
+
`true` for a new organisation (HTTP 201), `false` when it already existed
|
|
84
|
+
(HTTP 200). A repeat provisioning call preserves the existing ceilings and share
|
|
85
|
+
host; use `updateOrganisationCeiling()` to change them. Byte and money totals
|
|
86
|
+
remain decimal strings, including values above JavaScript's safe integer range.
|
|
87
|
+
|
|
88
|
+
For the operator application's existing transport, compose the SDK on the server:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const management = createFilesManagementClient({
|
|
92
|
+
url: filesOrigin,
|
|
93
|
+
allowInsecureHttp: privateNetworkHttpEnabled,
|
|
94
|
+
fetch: filesManagementFetch(access, operatorOrganisationId),
|
|
95
|
+
});
|
|
96
|
+
const result = await management.provisionOrganisation(input);
|
|
97
|
+
if (!result.created) {
|
|
98
|
+
// Show the retained values; provisioning did not apply the requested ceilings.
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`allowInsecureHttp` should come from trusted deployment configuration, never a
|
|
103
|
+
request parameter. Keep application permission checks inside the supplied fetch
|
|
104
|
+
and retain the UI's error mapping when replacing a custom client with the SDK.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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';
|
|
3
|
+
export { ApiError, type ClientOptions, type ManagementClientOptions } from './transport.js';
|
|
4
|
+
export declare function createFilesClient(options: ClientOptions): {
|
|
5
|
+
createCollection: (name: string) => Promise<{
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
}>;
|
|
9
|
+
reserveUpload: (input: UploadInput) => Promise<UploadReservation>;
|
|
10
|
+
confirmUpload: (id: string) => Promise<StoredFile>;
|
|
11
|
+
getFile: (id: string) => Promise<StoredFile>;
|
|
12
|
+
downloadFile: (id: string) => Promise<{
|
|
13
|
+
url: string;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
}>;
|
|
16
|
+
deleteFile: (id: string) => Promise<void>;
|
|
17
|
+
listFiles: (collection: string, page?: {
|
|
18
|
+
after?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
}) => Promise<StoredFile[]>;
|
|
21
|
+
createShare: (fileId: string, expiresAt: string | null) => Promise<CreatedShare>;
|
|
22
|
+
listShares: (fileId: string) => Promise<Share[]>;
|
|
23
|
+
revokeShare: (shareId: string) => Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
/** Operator and tenant management routes. `options.fetch` authenticates
|
|
26
|
+
* each request with a signed human proof or an issued management key;
|
|
27
|
+
* this client sends no bearer credential or organisation header of its own. */
|
|
28
|
+
export declare function createFilesManagementClient(options: ManagementClientOptions): {
|
|
29
|
+
listAudit: (query?: AuditQuery) => Promise<AuditPage>;
|
|
30
|
+
listRates: () => Promise<{
|
|
31
|
+
rates: FilesRate[];
|
|
32
|
+
}>;
|
|
33
|
+
setRate: (input: SetRateInput) => Promise<FilesRate>;
|
|
34
|
+
listOrganisations: () => Promise<{
|
|
35
|
+
organisations: OrganisationSummary[];
|
|
36
|
+
}>;
|
|
37
|
+
provisionOrganisation: (input: ProvisionOrganisationInput) => Promise<ProvisionOrganisationResult>;
|
|
38
|
+
updateOrganisationCeiling: (organisationId: string, input: UpdateOrganisationCeilingInput) => Promise<OrganisationSummary>;
|
|
39
|
+
getQuota: () => Promise<QuotaSummary>;
|
|
40
|
+
setQuota: (limitBytes: string | null) => Promise<QuotaSummary>;
|
|
41
|
+
setKeyQuota: (keyId: string, limitBytes: string | null) => Promise<QuotaSummary>;
|
|
42
|
+
getBudget: () => Promise<BudgetSummary>;
|
|
43
|
+
setBudget: (limitMicros: string | null) => Promise<BudgetSummary>;
|
|
44
|
+
setKeyBudget: (keyId: string, limitMicros: string | null) => Promise<BudgetSummary>;
|
|
45
|
+
getUsage: (from: string, to: string) => Promise<{
|
|
46
|
+
days: UsageDay[];
|
|
47
|
+
}>;
|
|
48
|
+
listOrganisationShares: () => Promise<{
|
|
49
|
+
shares: OrganisationShare[];
|
|
50
|
+
}>;
|
|
51
|
+
};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { clientCall, managementCall, managementCallWithStatus, } from './transport.js';
|
|
2
|
+
export { ApiError } from './transport.js';
|
|
3
|
+
export function createFilesClient(options) {
|
|
4
|
+
const call = clientCall(options);
|
|
5
|
+
return {
|
|
6
|
+
createCollection: (name) => call('POST', '/v1/collections', { name }),
|
|
7
|
+
reserveUpload: (input) => call('POST', '/v1/uploads', input),
|
|
8
|
+
confirmUpload: (id) => call('POST', `/v1/uploads/${encodeURIComponent(id)}/confirm`),
|
|
9
|
+
getFile: (id) => call('GET', `/v1/files/${encodeURIComponent(id)}`),
|
|
10
|
+
downloadFile: (id) => call('GET', `/v1/files/${encodeURIComponent(id)}/download`),
|
|
11
|
+
deleteFile: (id) => call('DELETE', `/v1/files/${encodeURIComponent(id)}`),
|
|
12
|
+
listFiles: (collection, page = {}) => call('GET', `/v1/collections/${encodeURIComponent(collection)}/files?${new URLSearchParams({
|
|
13
|
+
...(page.after ? { after: page.after } : {}),
|
|
14
|
+
...(page.limit !== undefined ? { limit: String(page.limit) } : {}),
|
|
15
|
+
})}`),
|
|
16
|
+
// Public share links (task S6): `files.files:share`/`files.shares:read`,
|
|
17
|
+
// bearer key or signed request -- both reach the same /v1/files/*
|
|
18
|
+
// routes this client already calls.
|
|
19
|
+
createShare: (fileId, expiresAt) => call('POST', `/v1/files/${encodeURIComponent(fileId)}/shares`, { expiresAt }),
|
|
20
|
+
listShares: (fileId) => call('GET', `/v1/files/${encodeURIComponent(fileId)}/shares`).then((r) => r.shares),
|
|
21
|
+
revokeShare: (shareId) => call('DELETE', `/v1/shares/${encodeURIComponent(shareId)}`),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Operator and tenant management routes. `options.fetch` authenticates
|
|
25
|
+
* each request with a signed human proof or an issued management key;
|
|
26
|
+
* this client sends no bearer credential or organisation header of its own. */
|
|
27
|
+
export function createFilesManagementClient(options) {
|
|
28
|
+
const call = managementCall(options);
|
|
29
|
+
const callWithStatus = managementCallWithStatus(options);
|
|
30
|
+
return {
|
|
31
|
+
listAudit: (query = {}) => {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
for (const key of ['organisation', 'from', 'to', 'limit', 'cursor']) {
|
|
34
|
+
const value = query[key];
|
|
35
|
+
if (value !== undefined)
|
|
36
|
+
params.set(key, String(value));
|
|
37
|
+
}
|
|
38
|
+
return call('GET', `/v1/manage/audit?${params}`);
|
|
39
|
+
},
|
|
40
|
+
listRates: () => call('GET', '/v1/manage/rates'),
|
|
41
|
+
setRate: (input) => call('POST', '/v1/manage/rates', input),
|
|
42
|
+
listOrganisations: () => call('GET', '/v1/manage/organisations'),
|
|
43
|
+
provisionOrganisation: async (input) => {
|
|
44
|
+
const { data, status } = await callWithStatus('POST', '/v1/manage/organisations', input);
|
|
45
|
+
return { ...data, created: status === 201 };
|
|
46
|
+
},
|
|
47
|
+
updateOrganisationCeiling: (organisationId, input) => call('PATCH', `/v1/manage/organisations/${encodeURIComponent(organisationId)}`, input),
|
|
48
|
+
// Tenant routes (task S4b): the caller's own organisation, from the
|
|
49
|
+
// signed request's claim -- there is no organisation id to pass here.
|
|
50
|
+
getQuota: () => call('GET', '/v1/manage/quota'),
|
|
51
|
+
setQuota: (limitBytes) => call('PUT', '/v1/manage/quota', { limitBytes }),
|
|
52
|
+
setKeyQuota: (keyId, limitBytes) => call('PUT', `/v1/manage/keys/${encodeURIComponent(keyId)}/quota`, {
|
|
53
|
+
limitBytes,
|
|
54
|
+
}),
|
|
55
|
+
getBudget: () => call('GET', '/v1/manage/budget'),
|
|
56
|
+
setBudget: (limitMicros) => call('PUT', '/v1/manage/budget', { limitMicros }),
|
|
57
|
+
setKeyBudget: (keyId, limitMicros) => call('PUT', `/v1/manage/keys/${encodeURIComponent(keyId)}/budget`, {
|
|
58
|
+
limitMicros,
|
|
59
|
+
}),
|
|
60
|
+
getUsage: (from, to) => call('GET', `/v1/manage/usage?${new URLSearchParams({ from, to })}`),
|
|
61
|
+
// Public share links (task S6): every active link in the caller's
|
|
62
|
+
// organisation, with the file's name -- never the token or URL.
|
|
63
|
+
listOrganisationShares: () => call('GET', '/v1/manage/shares'),
|
|
64
|
+
};
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const ref = /^(?:files:file|storage:file|valet:artifact)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
2
|
+
export function fileReference(id) {
|
|
3
|
+
if (!ref.test(`files:file/${id}`))
|
|
4
|
+
throw new Error('Invalid file id');
|
|
5
|
+
return `files:file/${id.toLowerCase()}`;
|
|
6
|
+
}
|
|
7
|
+
/** Parsing a legacy reference supplies an identifier, never authority. */
|
|
8
|
+
export function fileIdFromReference(value) {
|
|
9
|
+
return ref.exec(value)?.[1]?.toLowerCase() ?? null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ClientOptions {
|
|
2
|
+
url: string;
|
|
3
|
+
credential: () => string | Promise<string>;
|
|
4
|
+
/** Organisation selection; the service verifies credential access to it. */
|
|
5
|
+
organisationId?: string;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare class ApiError extends Error {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly code: string;
|
|
11
|
+
constructor(status: number, code: string, message?: string);
|
|
12
|
+
}
|
|
13
|
+
export declare function clientCall(options: ClientOptions): <T>(method: string, path: string, input?: unknown) => Promise<T>;
|
|
14
|
+
export interface ManagementClientOptions {
|
|
15
|
+
url: string;
|
|
16
|
+
/** Explicit opt-in for server-to-server HTTP on a trusted private network.
|
|
17
|
+
* Defaults to false. The caller is responsible for protecting the connection. */
|
|
18
|
+
allowInsecureHttp?: boolean;
|
|
19
|
+
/** Authenticates each request with a signed human proof or issued key;
|
|
20
|
+
* this transport carries no credential of its own. */
|
|
21
|
+
fetch: typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
/** Same wire shape as `clientCall`, except the response body itself is the
|
|
24
|
+
* data (no `{ data }` envelope) and no bearer/organisation header is sent --
|
|
25
|
+
* the caller-supplied `fetch` is the whole authentication story. */
|
|
26
|
+
export declare function managementCall(options: ManagementClientOptions): <T>(method: string, path: string, input?: unknown) => Promise<T>;
|
|
27
|
+
/** Retain response status for idempotent provisioning (201 created, 200 existing). */
|
|
28
|
+
export declare function managementCallWithStatus(options: ManagementClientOptions): <T>(method: string, path: string, input?: unknown) => Promise<{
|
|
29
|
+
data: T;
|
|
30
|
+
status: number;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
constructor(status, code, message) {
|
|
5
|
+
super(message ?? code);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function requireServiceOrigin(url, allowInsecureHttp = false) {
|
|
11
|
+
const base = new URL(url);
|
|
12
|
+
if (base.username ||
|
|
13
|
+
base.password ||
|
|
14
|
+
base.search ||
|
|
15
|
+
base.hash ||
|
|
16
|
+
base.pathname !== '/' ||
|
|
17
|
+
(base.protocol !== 'https:' &&
|
|
18
|
+
!(base.protocol === 'http:' &&
|
|
19
|
+
(allowInsecureHttp || ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname)))))
|
|
20
|
+
throw new Error('Use an HTTPS service origin (HTTP is allowed only for localhost)');
|
|
21
|
+
return base;
|
|
22
|
+
}
|
|
23
|
+
/** `{ error: { code, message } }`, the same shape both response envelopes use. */
|
|
24
|
+
function errorFrom(value) {
|
|
25
|
+
const error = typeof value === 'object' && value !== null && 'error' in value ? value.error : null;
|
|
26
|
+
const code = typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string'
|
|
27
|
+
? error.code
|
|
28
|
+
: 'request_failed';
|
|
29
|
+
const message = typeof error === 'object' && error !== null && 'message' in error ? error.message : undefined;
|
|
30
|
+
return { code, message: typeof message === 'string' ? message : undefined };
|
|
31
|
+
}
|
|
32
|
+
export function clientCall(options) {
|
|
33
|
+
const base = requireServiceOrigin(options.url);
|
|
34
|
+
return async (method, path, input) => {
|
|
35
|
+
const response = await (options.fetch ?? fetch)(new URL(path, base), {
|
|
36
|
+
method,
|
|
37
|
+
redirect: 'error',
|
|
38
|
+
credentials: 'omit',
|
|
39
|
+
headers: {
|
|
40
|
+
authorization: `Bearer ${await options.credential()}`,
|
|
41
|
+
...(options.organisationId ? { 'x-organisation-id': options.organisationId } : {}),
|
|
42
|
+
...(input === undefined ? {} : { 'content-type': 'application/json' }),
|
|
43
|
+
},
|
|
44
|
+
body: input === undefined ? undefined : JSON.stringify(input),
|
|
45
|
+
signal: AbortSignal.timeout(30000),
|
|
46
|
+
});
|
|
47
|
+
if (response.status === 204)
|
|
48
|
+
return undefined;
|
|
49
|
+
let value;
|
|
50
|
+
try {
|
|
51
|
+
value = await response.json();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new ApiError(response.status, 'invalid_response');
|
|
55
|
+
}
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const { code, message } = errorFrom(value);
|
|
58
|
+
throw new ApiError(response.status, code, message);
|
|
59
|
+
}
|
|
60
|
+
if (!value || typeof value !== 'object' || !('data' in value))
|
|
61
|
+
throw new ApiError(response.status, 'invalid_response');
|
|
62
|
+
return value.data;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Same wire shape as `clientCall`, except the response body itself is the
|
|
66
|
+
* data (no `{ data }` envelope) and no bearer/organisation header is sent --
|
|
67
|
+
* the caller-supplied `fetch` is the whole authentication story. */
|
|
68
|
+
export function managementCall(options) {
|
|
69
|
+
const call = managementCallWithStatus(options);
|
|
70
|
+
return async (method, path, input) => (await call(method, path, input)).data;
|
|
71
|
+
}
|
|
72
|
+
/** Retain response status for idempotent provisioning (201 created, 200 existing). */
|
|
73
|
+
export function managementCallWithStatus(options) {
|
|
74
|
+
const base = requireServiceOrigin(options.url, options.allowInsecureHttp);
|
|
75
|
+
return async (method, path, input) => {
|
|
76
|
+
const response = await options.fetch(new URL(path, base), {
|
|
77
|
+
method,
|
|
78
|
+
redirect: 'error',
|
|
79
|
+
credentials: 'omit',
|
|
80
|
+
headers: input === undefined ? {} : { 'content-type': 'application/json' },
|
|
81
|
+
body: input === undefined ? undefined : JSON.stringify(input),
|
|
82
|
+
signal: AbortSignal.timeout(30000),
|
|
83
|
+
});
|
|
84
|
+
if (response.status === 204)
|
|
85
|
+
return { data: undefined, status: response.status };
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = await response.json();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new ApiError(response.status, 'invalid_response');
|
|
92
|
+
}
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const { code, message } = errorFrom(value);
|
|
95
|
+
throw new ApiError(response.status, code, message);
|
|
96
|
+
}
|
|
97
|
+
return { data: value, status: response.status };
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** Public HTTP contracts. Internal object keys and quota rows are never returned. */
|
|
2
|
+
export interface UploadInput {
|
|
3
|
+
collectionId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
contentType: string;
|
|
6
|
+
bytes: number;
|
|
7
|
+
requestId: string;
|
|
8
|
+
replace?: {
|
|
9
|
+
fileId: string;
|
|
10
|
+
version: number;
|
|
11
|
+
};
|
|
12
|
+
retention: 'scratch' | 'durable';
|
|
13
|
+
}
|
|
14
|
+
export interface StoredFile {
|
|
15
|
+
id: string;
|
|
16
|
+
ref: string;
|
|
17
|
+
name: string;
|
|
18
|
+
contentType: string;
|
|
19
|
+
bytes: string;
|
|
20
|
+
version: number;
|
|
21
|
+
collectionId: string;
|
|
22
|
+
expiresAt: string | null;
|
|
23
|
+
}
|
|
24
|
+
export type UploadReservation = {
|
|
25
|
+
uploadId: string;
|
|
26
|
+
fileId: string;
|
|
27
|
+
state: 'confirmed';
|
|
28
|
+
} | {
|
|
29
|
+
uploadId: string;
|
|
30
|
+
fileId: string;
|
|
31
|
+
state: 'pending';
|
|
32
|
+
url: string;
|
|
33
|
+
headers: {
|
|
34
|
+
'content-type': string;
|
|
35
|
+
'content-length': string;
|
|
36
|
+
};
|
|
37
|
+
expiresAt: string;
|
|
38
|
+
};
|
|
39
|
+
/** Public share links (task S6). `url` is returned only from `createShare`,
|
|
40
|
+
* once, and is never included in a listing -- `Share` (below) is what
|
|
41
|
+
* `listShares` returns instead. */
|
|
42
|
+
export interface CreatedShare {
|
|
43
|
+
shareId: string;
|
|
44
|
+
url: string;
|
|
45
|
+
expiresAt: string | null;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
}
|
|
48
|
+
export interface Share {
|
|
49
|
+
shareId: string;
|
|
50
|
+
expiresAt: string | null;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
createdBy: string;
|
|
53
|
+
revokedAt: string | null;
|
|
54
|
+
}
|
|
55
|
+
/** The organisation's active links (task S6, signed-request-only client). */
|
|
56
|
+
export interface OrganisationShare {
|
|
57
|
+
shareId: string;
|
|
58
|
+
fileId: string;
|
|
59
|
+
fileName: string;
|
|
60
|
+
expiresAt: string | null;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
}
|
|
63
|
+
/** Operator management contracts (ADR 0021 signed requests only). A GB-day
|
|
64
|
+
* rate is a small price and travels as a JSON number; ceilings, usage and
|
|
65
|
+
* spend are cumulative totals and travel as bigint-safe strings, `null`
|
|
66
|
+
* meaning uncapped. */
|
|
67
|
+
export interface FilesRate {
|
|
68
|
+
microsPerGbDay: number;
|
|
69
|
+
/** The per-GB upload price, reserved when an upload is reserved and
|
|
70
|
+
* settled when it is confirmed -- the same small-price convention as
|
|
71
|
+
* `microsPerGbDay`. */
|
|
72
|
+
microsPerGbUpload: number;
|
|
73
|
+
effectiveFrom: string;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
createdBy: string;
|
|
76
|
+
}
|
|
77
|
+
export interface SetRateInput {
|
|
78
|
+
microsPerGbDay: number;
|
|
79
|
+
/** Omitted: the new rate carries forward whatever upload price is
|
|
80
|
+
* already in force, rather than zeroing it. */
|
|
81
|
+
microsPerGbUpload?: number;
|
|
82
|
+
/** ISO calendar date; must be tomorrow UTC or later. */
|
|
83
|
+
effectiveFrom: string;
|
|
84
|
+
}
|
|
85
|
+
export interface OrganisationSummary {
|
|
86
|
+
organisationId: string;
|
|
87
|
+
ceilingBytes: string | null;
|
|
88
|
+
ceilingMicros: string | null;
|
|
89
|
+
usedBytes: string;
|
|
90
|
+
reservedBytes: string;
|
|
91
|
+
spentMicros: string;
|
|
92
|
+
suspended: boolean;
|
|
93
|
+
/** The organisation's own public share domain, a bare lowercase host name
|
|
94
|
+
* such as `share.lokessmie.no` (task S7). `null` means it has none, and
|
|
95
|
+
* `createShare` answers 503 `not_configured` for it. Unique across
|
|
96
|
+
* organisations: a host another organisation already owns is refused with
|
|
97
|
+
* 409 `conflict`. */
|
|
98
|
+
shareHost: string | null;
|
|
99
|
+
}
|
|
100
|
+
export interface ProvisionOrganisationInput {
|
|
101
|
+
organisationId: string;
|
|
102
|
+
ceilingBytes: string | null;
|
|
103
|
+
ceilingMicros: string | null;
|
|
104
|
+
/** Omitted or `null`: provisioned with no share domain. */
|
|
105
|
+
shareHost?: string | null;
|
|
106
|
+
}
|
|
107
|
+
export interface ProvisionOrganisationResult extends OrganisationSummary {
|
|
108
|
+
/** False when already provisioned: existing ceilings and share host were retained. */
|
|
109
|
+
created: boolean;
|
|
110
|
+
}
|
|
111
|
+
export interface UpdateOrganisationCeilingInput {
|
|
112
|
+
ceilingBytes?: string | null;
|
|
113
|
+
ceilingMicros?: string | null;
|
|
114
|
+
suspended?: boolean;
|
|
115
|
+
/** `undefined` leaves it untouched; `null` clears it, which retires every
|
|
116
|
+
* existing link for that organisation immediately (task S7). */
|
|
117
|
+
shareHost?: string | null;
|
|
118
|
+
}
|
|
119
|
+
/** Tenant management contracts (task S4b), same signed-request-only client.
|
|
120
|
+
* Byte and money totals travel as bigint-safe strings, `null` uncapped. */
|
|
121
|
+
export interface QuotaSummary {
|
|
122
|
+
organisation: {
|
|
123
|
+
limitBytes: string | null;
|
|
124
|
+
ceilingBytes: string | null;
|
|
125
|
+
usedBytes: string;
|
|
126
|
+
reservedBytes: string;
|
|
127
|
+
};
|
|
128
|
+
keys: Array<{
|
|
129
|
+
keyId: string;
|
|
130
|
+
limitBytes: string | null;
|
|
131
|
+
usedBytes: string;
|
|
132
|
+
reservedBytes: string;
|
|
133
|
+
}>;
|
|
134
|
+
}
|
|
135
|
+
export interface BudgetSummary {
|
|
136
|
+
organisation: {
|
|
137
|
+
limitMicros: string | null;
|
|
138
|
+
ceilingMicros: string | null;
|
|
139
|
+
spentMicros: string;
|
|
140
|
+
reservedMicros: string;
|
|
141
|
+
};
|
|
142
|
+
keys: Array<{
|
|
143
|
+
keyId: string;
|
|
144
|
+
limitMicros: string | null;
|
|
145
|
+
spentMicros: string;
|
|
146
|
+
reservedMicros: string;
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
/** `gbDays`/`rateMicrosPerGbDay` are small numbers and travel as JSON
|
|
150
|
+
* numbers, matching `FilesRate.microsPerGbDay`; `chargedMicros` is a
|
|
151
|
+
* cumulative amount and travels as a bigint-safe string. */
|
|
152
|
+
export interface UsageDay {
|
|
153
|
+
day: string;
|
|
154
|
+
keyId: string;
|
|
155
|
+
gbDays: number;
|
|
156
|
+
chargedMicros: string;
|
|
157
|
+
rateMicrosPerGbDay: number;
|
|
158
|
+
}
|
|
159
|
+
/** Operator audit filters. Omitted dates select the most recent 92 days. */
|
|
160
|
+
export interface AuditQuery {
|
|
161
|
+
organisation?: string;
|
|
162
|
+
from?: string;
|
|
163
|
+
to?: string;
|
|
164
|
+
limit?: number;
|
|
165
|
+
/** Pass the previous page's `next` unchanged to preserve timestamp precision. */
|
|
166
|
+
cursor?: string;
|
|
167
|
+
}
|
|
168
|
+
export interface AuditEvent {
|
|
169
|
+
id: number;
|
|
170
|
+
occurredAt: string;
|
|
171
|
+
organisationId: string | null;
|
|
172
|
+
action: string;
|
|
173
|
+
actorDisplay: string;
|
|
174
|
+
targetType: string;
|
|
175
|
+
targetId: string | null;
|
|
176
|
+
outcome: string;
|
|
177
|
+
context: string;
|
|
178
|
+
reason: string | null;
|
|
179
|
+
reference: string | null;
|
|
180
|
+
}
|
|
181
|
+
export interface AuditPage {
|
|
182
|
+
events: AuditEvent[];
|
|
183
|
+
next: string | null;
|
|
184
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wtfalch/files",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Public client SDK and API types for the Files service.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/wtfalch/files",
|
|
10
|
+
"directory": "packages/sdk"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/index.js",
|
|
14
|
+
"dist/index.d.ts",
|
|
15
|
+
"dist/client.js",
|
|
16
|
+
"dist/client.d.ts",
|
|
17
|
+
"dist/transport.js",
|
|
18
|
+
"dist/transport.d.ts",
|
|
19
|
+
"dist/types.js",
|
|
20
|
+
"dist/types.d.ts",
|
|
21
|
+
"dist/references.js",
|
|
22
|
+
"dist/references.d.ts"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./client": {
|
|
30
|
+
"types": "./dist/client.d.ts",
|
|
31
|
+
"default": "./dist/client.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"prepublishOnly": "pnpm build && node ../../scripts/verify-sdk.mjs"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22",
|
|
49
|
+
"typescript": "^5.9.0",
|
|
50
|
+
"vitest": "^4.1.6"
|
|
51
|
+
}
|
|
52
|
+
}
|