@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 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.
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './client.js';
2
+ export type * from './types.js';
3
+ export { fileReference, fileIdFromReference } from './references.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './client.js';
2
+ export { fileReference, fileIdFromReference } from './references.js';
@@ -0,0 +1,3 @@
1
+ export declare function fileReference(id: string): string;
2
+ /** Parsing a legacy reference supplies an identifier, never authority. */
3
+ export declare function fileIdFromReference(value: string): string | null;
@@ -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
+ }
@@ -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
+ }