@wtfalch/files 0.9.1 → 0.11.1

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 CHANGED
@@ -6,7 +6,7 @@ package dependencies.
6
6
 
7
7
  ## Status
8
8
 
9
- Published: yes, v0.9.1, 2026-09-28.
9
+ Published: yes, v0.11.0, 2026-10-03.
10
10
 
11
11
  ```sh
12
12
  npm install @wtfalch/files
@@ -50,7 +50,8 @@ exact file version. A mismatch against the file's current version throws
50
50
  `ApiError` with `status: 409` rather than serving the current version's
51
51
  bytes -- the backend keeps only the current version's object, so there is no
52
52
  older version to fall back to. Every `StoredFile` also carries `sha256`
53
- (`null` unless the storage backend exposed byte access at confirm time) and
53
+ (`null` unless the storage backend exposed byte access at confirm time),
54
+ `createdAt` (ISO 8601 creation time of the file row) and
54
55
  `principalRef`, the principal whose upload last confirmed it at its current
55
56
  version (`null` if that upload is no longer on record, or the file was never
56
57
  confirmed).
@@ -188,6 +189,11 @@ app key), so lokessmie's and offline.tf's existing app-key integrations keep
188
189
  minting links unchanged. Change it afterward with `setFileAudience`'s own
189
190
  `shareable` field.
190
191
 
192
+ A link's `expiresAt` is `null` for a link that never stops working, or a time
193
+ at most 365 days ahead (a few minutes more are allowed for a caller's clock
194
+ running ahead). That is the range of an app's `FILES_SHARE_LINK_DAYS`
195
+ (1 to 365, or `never`): send `now + days`, or `null` for `never`.
196
+
191
197
  ### Seeing everything: `files.files:read-any`
192
198
 
193
199
  A sensitive permission: read any file in the organisation, whatever its
@@ -251,13 +257,16 @@ is a separate rollout; installing the SDK does not migrate accounts or files.
251
257
 
252
258
  ## Calling from a browser through a same-origin proxy
253
259
 
254
- The files host itself has no CORS headers and no public domain (task #70):
255
260
  `credential()` above must never be a privileged service credential in
256
- browser code, and a browser cannot reach the host directly either way. A
257
- browser app such as Agora instead runs its own same-origin proxy -- a route
258
- on the app's own origin that forwards `Authorization` and
259
- `X-Organisation-Id` to the files host on its private network, stripping any
260
- cookies -- and points the SDK at itself:
261
+ browser code. The host now answers a CORS preflight and allows a direct
262
+ browser call for an origin its operator has allow-listed
263
+ (`FILES_ALLOWED_ORIGINS` -- see `@wtfalch/files-host`'s README, "Cross-origin
264
+ browser calls"); point `createFilesClient` at the host's own URL in that
265
+ case. For any other origin, or before it is allow-listed, the host has no
266
+ public domain for it to reach anyway, so a browser app such as Agora instead
267
+ runs its own same-origin proxy -- a route on the app's own origin that
268
+ forwards `Authorization` and `X-Organisation-Id` to the files host on its
269
+ private network, stripping any cookies -- and points the SDK at itself:
261
270
 
262
271
  ```ts
263
272
  const files = createFilesClient({
@@ -397,6 +406,84 @@ every row and every stored object -- and `restoreOrganisation` then throws
397
406
  `ApiError` with `status: 404` too, the same as for an organisation that was
398
407
  never deleted; the two are not distinguishable from the client, by design.
399
408
 
409
+ ### Quotas and budgets: operator ceiling vs. organisation limit
410
+
411
+ Two different levels cap an organisation's bytes and spend, and only one of
412
+ them is operator-settable. `updateOrganisationCeiling` sets the operator
413
+ **ceiling** (`ceilingBytes`/`ceilingMicros`) -- the platform-wide cap a
414
+ support/operator credential may set for any organisation, passed by
415
+ `organisationId` like the rest of the organisation-management calls above.
416
+ `getQuota`/`setQuota`/`getBudget`/`setBudget` work on the organisation's own
417
+ **limit** (`limitBytes`/`limitMicros`) instead, and are tenant self-service
418
+ only: they take no `organisationId` because the target is always read off
419
+ the signed request's own identity, never a parameter a caller can point
420
+ elsewhere.
421
+
422
+ ```ts
423
+ // Operator-settable, any organisation, by id:
424
+ await management.updateOrganisationCeiling(organisationId, { ceilingBytes: '5000000000' });
425
+
426
+ // Tenant self-service, the caller's own organisation only:
427
+ const tenantManagement = createFilesManagementClient({ url, fetch: tenantFetch });
428
+ await tenantManagement.setQuota('1000000000');
429
+ await tenantManagement.setBudget('250000000');
430
+ ```
431
+
432
+ There is no operator-callable way to set an organisation's own limit on its
433
+ behalf. This was decided deliberately (issue #116): a freshly provisioned
434
+ organisation's limit starts `null` (uncapped at that level), so the operator
435
+ ceiling alone governs it until the organisation sets a tighter limit for
436
+ itself, from its own tenant application (for example Boule). An application
437
+ provisioning an organisation on an operator's behalf (Archon's foundry step,
438
+ for one) should set the ceiling and stop there -- not attempt to also seed
439
+ the organisation's own limit, which no credential it holds can do.
440
+
441
+ ### One app's files (issue #161)
442
+
443
+ A company's admin app (Boule) reads one app's files with a key of the
444
+ company's own organisation: `appFilesTotals`, `appFilesFolders`,
445
+ `appFilesSpace`, `appFiles` and `appFilesLinks`, and `stopSharing` to revoke
446
+ a link. Every call names the app's collections by name and reads only those;
447
+ the organisation is always the key's own. Files knows no apps and no copies:
448
+ a workspace is its own organisation, read with its own key. The key needs
449
+ `files.usage:read` (totals, folders, space), `files.files:read` (the files
450
+ list), `files.shares:read` (the links) and `files.files:share` (Stop sharing),
451
+ each across the organisation.
452
+
453
+ ```ts
454
+ const app = { collections: ['INVOICES', 'PHOTOS'] };
455
+ const totals = await tenantManagement.appFilesTotals(app);
456
+ const { files, more } = await tenantManagement.appFiles({ ...app, search: 'march', limit: 50 });
457
+ await tenantManagement.stopSharing({ ...app, link: linkId });
458
+ ```
459
+
460
+ ### An app's space alert (issue #164)
461
+
462
+ Foundry sets each app's `spaceAlertPercent` with the operator's management
463
+ key (`files.organisations:provision`, the permission that ensures the app's
464
+ collections): `setSpaceAlert(organisationId, app, { percent, name,
465
+ collections, recipients })` (PUT
466
+ `/v1/manage/organisations/:organisationId/apps/:app/space-alert`),
467
+ `getSpaceAlert` (GET) and `removeSpaceAlert` (DELETE, a no-op when there is
468
+ none). `app` is the entry key, `percent` is 1 to 100 and defaults to 80,
469
+ `collections` are the app's collection names (1 to 50) and `recipients` the
470
+ owners' addresses (at most 20); files keeps no address for anyone, so
471
+ foundry sends the setting again when the owners change. Every day, after the
472
+ meter, files mails each recipient once when the bytes of the app's live files
473
+ reach `percent` of the organisation's plan space (the tighter of its limit and
474
+ the operator ceiling, `planBytes` in the totals), and again only after they
475
+ drop below it and reach it again. A send that fails is tried on the next day's
476
+ check; nobody is mailed twice for one crossing.
477
+
478
+ ```ts
479
+ await management.setSpaceAlert(organisationId, 'shop', {
480
+ percent: 80,
481
+ name: 'Shop',
482
+ collections: ['INVOICES', 'PHOTOS'],
483
+ recipients: ['owner@acme.example'],
484
+ });
485
+ ```
486
+
400
487
  ### Membership
401
488
 
402
489
  The product that owns its users (Agora, lokessmie) writes memberships in its
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ClientOptions, type ManagementClientOptions } from './transport.js';
2
- import type { AddMemberInput, AuditPage, AuditQuery, BudgetSummary, CopyFileInput, CreatedShare, FilesPage, FilesRate, Group, GroupMember, Member, MoveFileInput, OrganisationShare, OrganisationSummary, ProvisionOrganisationInput, ProvisionOrganisationResult, QuotaSummary, SetFileAudienceInput, SetRateInput, SetWebhookResult, Share, SharedFilesPage, StoredFile, UpdateOrganisationCeilingInput, UploadInput, UploadReservation, UploadedPart, UsageDay, VariantFormat, VariantWidth, WebhookConfig } from './types.js';
2
+ import type { AddMemberInput, AppFilesEntry, AppFilesFolder, AppFilesLink, AppFilesListQuery, AppFilesQuery, AppFilesSpace, AppFilesTotals, AuditPage, AuditQuery, BudgetSummary, CopyFileInput, CreatedShare, FilesPage, FilesRate, Group, GroupMember, IssueKeyInput, IssueOrganisationKeyInput, IssuedOrganisationKey, Member, MoveFileInput, OrganisationShare, OrganisationSummary, ProvisionOrganisationInput, ProvisionOrganisationResult, QuotaSummary, RevokedOrganisationKey, SetFileAudienceInput, SetRateInput, SetSpaceAlertInput, SetWebhookResult, Share, SharedFilesPage, SpaceAlert, StoredFile, UpdateOrganisationCeilingInput, UploadInput, UploadReservation, UploadedPart, UsageDay, VariantFormat, VariantWidth, 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<{
@@ -122,6 +122,26 @@ export declare function createFilesManagementClient(options: ManagementClientOpt
122
122
  organisations: OrganisationSummary[];
123
123
  }>;
124
124
  provisionOrganisation: (input: ProvisionOrganisationInput) => Promise<ProvisionOrganisationResult>;
125
+ issueOrganisationKey: (organisationId: string, input: IssueOrganisationKeyInput) => Promise<IssuedOrganisationKey>;
126
+ revokeOrganisationKey: (organisationId: string, keyId: string) => Promise<RevokedOrganisationKey>;
127
+ issueKeyBelow: (organisationId: string, minterId: string, input: IssueKeyInput) => Promise<IssuedOrganisationKey>;
128
+ ensureCollection: (organisationId: string, name: string) => Promise<{
129
+ created: boolean;
130
+ id: string;
131
+ name: string;
132
+ }>;
133
+ setSpaceAlert: (organisationId: string, app: string, input: SetSpaceAlertInput) => Promise<{
134
+ created: boolean;
135
+ app: string;
136
+ name: string;
137
+ percent: number;
138
+ collections: string[];
139
+ recipients: string[];
140
+ above: boolean;
141
+ updatedAt: string;
142
+ }>;
143
+ getSpaceAlert: (organisationId: string, app: string) => Promise<SpaceAlert>;
144
+ removeSpaceAlert: (organisationId: string, app: string) => Promise<void>;
125
145
  updateOrganisationCeiling: (organisationId: string, input: UpdateOrganisationCeilingInput) => Promise<OrganisationSummary>;
126
146
  deleteOrganisation: (organisationId: string) => Promise<OrganisationSummary>;
127
147
  restoreOrganisation: (organisationId: string) => Promise<OrganisationSummary>;
@@ -137,8 +157,28 @@ export declare function createFilesManagementClient(options: ManagementClientOpt
137
157
  listOrganisationShares: () => Promise<{
138
158
  shares: OrganisationShare[];
139
159
  }>;
160
+ appFilesTotals: (query: AppFilesQuery) => Promise<AppFilesTotals>;
161
+ appFilesFolders: (query: AppFilesQuery) => Promise<{
162
+ folders: AppFilesFolder[];
163
+ }>;
164
+ appFilesSpace: (query: AppFilesQuery) => Promise<AppFilesSpace>;
165
+ appFiles: (query: AppFilesListQuery) => Promise<{
166
+ files: AppFilesEntry[];
167
+ more: boolean;
168
+ }>;
169
+ appFilesLinks: (query: AppFilesQuery & {
170
+ limit?: number;
171
+ }) => Promise<{
172
+ links: AppFilesLink[];
173
+ total: number;
174
+ }>;
175
+ stopSharing: (query: AppFilesQuery & {
176
+ link: string;
177
+ }) => Promise<void>;
140
178
  getWebhook: () => Promise<WebhookConfig>;
141
179
  setWebhook: (url: string | null) => Promise<SetWebhookResult>;
180
+ issueKey: (input: IssueKeyInput) => Promise<IssuedOrganisationKey>;
181
+ revokeKey: (keyId: string) => Promise<RevokedOrganisationKey>;
142
182
  listMembers: () => Promise<{
143
183
  members: Member[];
144
184
  }>;
package/dist/client.js CHANGED
@@ -139,6 +139,32 @@ export function createFilesManagementClient(options) {
139
139
  const { data, status } = await callWithStatus('POST', '/v1/manage/organisations', input);
140
140
  return { ...data, created: status === 201 };
141
141
  },
142
+ // Mints an organisation's root key; the secret comes back once. Throws
143
+ // `ApiError` 409 for an organisation with no owner, 422 when the issuer
144
+ // is not a member or lacks a requested permission.
145
+ issueOrganisationKey: (organisationId, input) => call('POST', `/v1/manage/organisations/${encodeURIComponent(organisationId)}/keys`, input),
146
+ // Revokes the key and every key it minted; a second call is a no-op.
147
+ revokeOrganisationKey: (organisationId, keyId) => call('DELETE', `/v1/manage/organisations/${encodeURIComponent(organisationId)}/keys/${encodeURIComponent(keyId)}`),
148
+ // Mints a key below `minterId`, a live key of the organisation, without the
149
+ // minter's secret. Same input, result and errors as `issueKey`, plus 404
150
+ // when `minterId` is not a live key of the organisation. Revoking the
151
+ // minter (`revokeOrganisationKey`) revokes the new key too.
152
+ issueKeyBelow: (organisationId, minterId, input) => call('POST', `/v1/manage/organisations/${encodeURIComponent(organisationId)}/keys/${encodeURIComponent(minterId)}/keys`, input),
153
+ // Returns the organisation's collection of that name, creating it when
154
+ // missing; `created` is true only on the call that made it.
155
+ ensureCollection: async (organisationId, name) => {
156
+ const { data, status } = await callWithStatus('POST', `/v1/manage/organisations/${encodeURIComponent(organisationId)}/collections`, { name });
157
+ return { ...data, created: status === 201 };
158
+ },
159
+ // An app's space alert (issue #164). `created` is true only on the call
160
+ // that made it. `getSpaceAlert` throws `ApiError` 404 when there is none;
161
+ // `removeSpaceAlert` is a no-op when there is none.
162
+ setSpaceAlert: async (organisationId, app, input) => {
163
+ const { data, status } = await callWithStatus('PUT', spaceAlertPath(organisationId, app), input);
164
+ return { ...data, created: status === 201 };
165
+ },
166
+ getSpaceAlert: (organisationId, app) => call('GET', spaceAlertPath(organisationId, app)),
167
+ removeSpaceAlert: (organisationId, app) => call('DELETE', spaceAlertPath(organisationId, app)),
142
168
  updateOrganisationCeiling: (organisationId, input) => call('PATCH', `/v1/manage/organisations/${encodeURIComponent(organisationId)}`, input),
143
169
  // Delete an organisation with a 7-day grace period (task O, issue #67).
144
170
  // Idempotent: deleting an already-deleted organisation succeeds again
@@ -153,6 +179,10 @@ export function createFilesManagementClient(options) {
153
179
  restoreOrganisation: (organisationId) => call('POST', `/v1/manage/organisations/${encodeURIComponent(organisationId)}/restore`),
154
180
  // Tenant routes (task S4b): the caller's own organisation, from the
155
181
  // signed request's claim -- there is no organisation id to pass here.
182
+ // Self-service only, by design (issue #116): there is no
183
+ // operator-callable variant that targets another organisation's own
184
+ // limit. An operator sets only the ceiling, updateOrganisationCeiling
185
+ // above. See the SDK README's "Quotas and budgets" section.
156
186
  getQuota: () => call('GET', '/v1/manage/quota'),
157
187
  setQuota: (limitBytes) => call('PUT', '/v1/manage/quota', { limitBytes }),
158
188
  setKeyQuota: (keyId, limitBytes) => call('PUT', `/v1/manage/keys/${encodeURIComponent(keyId)}/quota`, {
@@ -167,9 +197,27 @@ export function createFilesManagementClient(options) {
167
197
  // Public share links (task S6): every active link in the caller's
168
198
  // organisation, with the file's name -- never the token or URL.
169
199
  listOrganisationShares: () => call('GET', '/v1/manage/shares'),
200
+ // One app's files (issue #161): only the named collections of the
201
+ // caller's own organisation.
202
+ appFilesTotals: (query) => call('GET', `/v1/manage/report/totals?${reportParams(query)}`),
203
+ appFilesFolders: (query) => call('GET', `/v1/manage/report/folders?${reportParams(query)}`),
204
+ appFilesSpace: (query) => call('GET', `/v1/manage/report/space?${reportParams(query)}`),
205
+ appFiles: (query) => call('GET', `/v1/manage/report/files?${reportParams(query)}`),
206
+ appFilesLinks: (query) => call('GET', `/v1/manage/report/links?${reportParams(query)}`),
207
+ // Stop sharing: the link stops opening at once. A link outside the named
208
+ // collections is not found.
209
+ stopSharing: (query) => call('DELETE', `/v1/manage/report/links/${encodeURIComponent(query.link)}?${reportParams({ collections: query.collections })}`),
170
210
  // Outbound event delivery (task gap-36). setWebhook(null) disables it.
171
211
  getWebhook: () => call('GET', '/v1/manage/webhook'),
172
212
  setWebhook: (url) => call('PUT', '/v1/manage/webhook', { url }),
213
+ // Mints a key below the calling key (which needs `credentials:create`);
214
+ // the secret comes back once. Throws `ApiError` 422 for a permission the
215
+ // caller lacks, `credentials:create`/`credentials:revoke`, or an expiry
216
+ // or limit past the caller's own; 404 when the caller may not mint.
217
+ issueKey: (input) => call('POST', '/v1/manage/keys', input),
218
+ // Revokes a key of the caller's own subtree (the caller included) and
219
+ // every key it minted; 404 for any other key. A second call is a no-op.
220
+ revokeKey: (keyId) => call('DELETE', `/v1/manage/keys/${encodeURIComponent(keyId)}`),
173
221
  // Membership (task M): the product that owns its users writes
174
222
  // memberships in its own files organisation. `files.members:read`/
175
223
  // `files.members:update`, held by the calling key for its own
@@ -184,3 +232,17 @@ export function createFilesManagementClient(options) {
184
232
  removeMember: (subject) => call('DELETE', `/v1/manage/members/${encodeURIComponent(subject)}`),
185
233
  };
186
234
  }
235
+ /** `collection=A&collection=B&...` plus the list query's own fields. */
236
+ function reportParams(query) {
237
+ const params = new URLSearchParams();
238
+ for (const name of query.collections)
239
+ params.append('collection', name);
240
+ if (query.folder)
241
+ params.set('folder', query.folder);
242
+ if (query.search)
243
+ params.set('search', query.search);
244
+ if (query.limit !== undefined)
245
+ params.set('limit', String(query.limit));
246
+ return params;
247
+ }
248
+ const spaceAlertPath = (organisationId, app) => `/v1/manage/organisations/${encodeURIComponent(organisationId)}/apps/${encodeURIComponent(app)}/space-alert`;
package/dist/types.d.ts CHANGED
@@ -92,6 +92,8 @@ export interface StoredFile {
92
92
  bytes: string;
93
93
  version: number;
94
94
  collectionId: string;
95
+ /** When the file row was created (ISO 8601). */
96
+ createdAt: string;
95
97
  expiresAt: string | null;
96
98
  /** `"md5:<hex>"` of the confirmed bytes, or `null` for a file confirmed
97
99
  * before this field existed. Derived from the storage backend's own etag
@@ -269,6 +271,46 @@ export interface ProvisionOrganisationResult extends OrganisationSummary {
269
271
  secret: string;
270
272
  };
271
273
  }
274
+ export interface IssueOrganisationKeyInput {
275
+ name: string;
276
+ kind: 'api_key' | 'agent' | 'service';
277
+ /** Subject id of a live human member of the organisation; the key holds
278
+ * only permissions that person holds. */
279
+ issuer: string;
280
+ permissions: string[];
281
+ /** 1 to 365. */
282
+ expiresDays: number;
283
+ app?: string;
284
+ }
285
+ export interface IssuedOrganisationKey {
286
+ id: string;
287
+ /** Shown once; files keeps no copy. */
288
+ secret: string;
289
+ kind: 'api_key' | 'agent' | 'service';
290
+ expiresAt: string;
291
+ permissions: string[];
292
+ }
293
+ export interface RevokedOrganisationKey {
294
+ id: string;
295
+ /** The key and every key it minted; empty when it was already revoked. */
296
+ revoked: string[];
297
+ }
298
+ export interface IssueKeyInput {
299
+ name: string;
300
+ kind: 'api_key' | 'agent' | 'service';
301
+ /** Each must be held by the calling key; never `credentials:create` or `credentials:revoke`. */
302
+ permissions: string[];
303
+ /** 1 to 365, and not past the calling key's own expiry. */
304
+ expiresDays: number;
305
+ /** Defaults to the calling key's own app. */
306
+ app?: string;
307
+ /** Byte quota for the new key, at most the organisation's limit; `null` is uncapped. */
308
+ limitBytes?: number | null;
309
+ /** Spend limit in micros for the new key, at most the organisation's limit; `null` is uncapped. */
310
+ limitMicros?: number | null;
311
+ }
312
+ export type IssuedKey = IssuedOrganisationKey;
313
+ export type RevokedKey = RevokedOrganisationKey;
272
314
  export interface UpdateOrganisationCeilingInput {
273
315
  ceilingBytes?: string | null;
274
316
  ceilingMicros?: string | null;
@@ -400,3 +442,103 @@ export interface AuditPage {
400
442
  events: AuditEvent[];
401
443
  next: string | null;
402
444
  }
445
+ /** One app's files, read by the company's own admin app (issue #161): every
446
+ * call names the app's collections by name (the `collections` of foundry's
447
+ * `files` module) and reads only those, inside the one organisation the key
448
+ * belongs to. Files has no notion of an app or of a copy (live or
449
+ * workspace): a copy is its own organisation, read with that organisation's
450
+ * key. Bytes and counts are JSON numbers; times are ISO, days `YYYY-MM-DD`
451
+ * in UTC. */
452
+ export interface AppFilesQuery {
453
+ /** The app's collections, by name. 1 to 50. */
454
+ collections: string[];
455
+ }
456
+ export interface AppFilesTotals {
457
+ /** Active files in the app's collections. */
458
+ files: number;
459
+ bytes: number;
460
+ /** The organisation's byte cap: the tighter of its own limit and the
461
+ * operator ceiling. Organisation-wide, not the app's alone. `null`: none set. */
462
+ planBytes: number | null;
463
+ /** Files added since the start of this UTC month (still active). */
464
+ addedThisMonth: number;
465
+ /** Confirmed uploads in the last 7 UTC days, today included. */
466
+ uploadedThisWeek: number;
467
+ /** Downloads and share-link opens in the last 7 UTC days, today included. */
468
+ openedThisWeek: number;
469
+ /** Share links that still open. */
470
+ openLinks: number;
471
+ }
472
+ export interface AppFilesFolder {
473
+ /** The collection's name. Files keeps no other label. */
474
+ name: string;
475
+ files: number;
476
+ bytes: number;
477
+ lastUploadAt: string | null;
478
+ /** Share links that still open. */
479
+ links: number;
480
+ }
481
+ export interface AppFilesSpace {
482
+ /** Oldest first, at most 90. A past day appears once the daily meter has
483
+ * recorded it; today is always last, read live. */
484
+ days: {
485
+ day: string;
486
+ bytes: number;
487
+ uploads: number;
488
+ }[];
489
+ /** The space used at the start of `month` (`YYYY-MM`, three months on) at
490
+ * the average daily growth of the recorded days; `null` with fewer than
491
+ * 7 recorded days or no growth. */
492
+ forecast: {
493
+ bytes: number;
494
+ month: string;
495
+ } | null;
496
+ }
497
+ export interface AppFilesEntry {
498
+ id: string;
499
+ name: string;
500
+ /** The collection's name. */
501
+ folder: string;
502
+ bytes: number;
503
+ /** When it was added. */
504
+ at: string;
505
+ }
506
+ export interface AppFilesListQuery extends AppFilesQuery {
507
+ /** One of `collections`, or every one of them when absent. */
508
+ folder?: string | null;
509
+ /** Matches the file's name or its folder, case-insensitively. */
510
+ search?: string;
511
+ /** 1 to 100; 50 when absent. */
512
+ limit?: number;
513
+ }
514
+ export interface AppFilesLink {
515
+ id: string;
516
+ /** The file's name. */
517
+ file: string;
518
+ opened: number;
519
+ expiresAt: string | null;
520
+ }
521
+ /** An app's space alert (issue #164): mail `recipients` once when the
522
+ * bytes of the app's `collections` reach `percent` of the organisation's
523
+ * plan space, and again only after they drop below it and reach it again.
524
+ * `app` is foundry's entry key. */
525
+ export interface SetSpaceAlertInput {
526
+ /** 1 to 100; absent is 80. */
527
+ percent?: number;
528
+ /** Shown in the mail; absent is the app key. */
529
+ name?: string;
530
+ /** The app's collections by name, 1 to 50. */
531
+ collections: string[];
532
+ /** The owners' addresses, at most 20. */
533
+ recipients: string[];
534
+ }
535
+ export interface SpaceAlert {
536
+ app: string;
537
+ name: string;
538
+ percent: number;
539
+ collections: string[];
540
+ recipients: string[];
541
+ /** True while the app is at or over its share, as of the last daily check. */
542
+ above: boolean;
543
+ updatedAt: string;
544
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/files",
3
- "version": "0.9.1",
3
+ "version": "0.11.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Public client SDK and API types for the Files service.",