@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 +95 -8
- package/dist/client.d.ts +41 -1
- package/dist/client.js +62 -0
- package/dist/types.d.ts +142 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ package dependencies.
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
Published: yes, v0.
|
|
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)
|
|
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
|
|
257
|
-
browser
|
|
258
|
-
|
|
259
|
-
`
|
|
260
|
-
|
|
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
|
+
}
|