@civitai/sdk 0.13.1 → 0.14.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/BREAKING.md +2 -2
- package/README.md +146 -7
- package/dist/app/index.d.ts +18 -6
- package/dist/app/index.js +11 -6
- package/dist/doc-store/fake.d.ts +94 -0
- package/dist/doc-store/fake.js +1141 -0
- package/dist/doc-store/index.d.ts +187 -0
- package/dist/doc-store/index.js +260 -0
- package/dist/http/index.js +18 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/shared-storage/index.d.ts +13 -3
- package/dist/storage/index.d.ts +29 -3
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +1 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/BREAKING.md
CHANGED
|
@@ -204,8 +204,8 @@ bridge already called, so behaviour is identical on both transports.
|
|
|
204
204
|
- **Anonymous viewers may NEVER write or vote.** An anon write is rejected by the write scope's own binding in
|
|
205
205
|
`withBlockScope`, before the handler runs, so it surfaces as **`403`** rather than the handler's own `401`.
|
|
206
206
|
- **Authenticated writers must clear a minimum-trust gate.** After the anon check, writes call
|
|
207
|
-
`assertSharedWriteTrust` — account age,
|
|
208
|
-
viewer is not automatically a permitted writer.
|
|
207
|
+
`assertSharedWriteTrust` — account age, completed onboarding, not banned or muted, and verified email *or* a
|
|
208
|
+
linked OAuth account. A signed-in viewer is not automatically a permitted writer.
|
|
209
209
|
- **Tenancy is structural, not checked.** Every query interpolates a schema name derived from the **verified
|
|
210
210
|
token**, never from client input. An app cannot address another app's shared storage because it cannot name it.
|
|
211
211
|
- **Rate limits are per-operation and per-namespace**, so one operation cannot starve another — browsing a feed
|
package/README.md
CHANGED
|
@@ -185,15 +185,106 @@ Routes are addressed by path, so a route `/api/v1` gains needs no release here:
|
|
|
185
185
|
const images = await app.site.get('images', { query: { limit: 20, username: 'civitai' } });
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
A `401` is retried once with a fresh token
|
|
188
|
+
A `401` is retried once with a fresh token — except one whose body's `code` is
|
|
189
|
+
`sign_in_required`, which is about the viewer, not the token. Any failure the server answered is an
|
|
189
190
|
`ApiError` carrying `status`, the parsed `body`, and the server's own message —
|
|
190
191
|
while a request the browser never delivered (CORS, DNS, offline) surfaces as a
|
|
191
192
|
fetch `TypeError` with no `status`, so test `error instanceof ApiError` before
|
|
192
193
|
reading one.
|
|
193
194
|
|
|
195
|
+
## Document storage
|
|
196
|
+
|
|
197
|
+
An app declares its **collections** in its manifest, and `app.storage.collection(name)`
|
|
198
|
+
reads and writes their JSON documents. `private` collections hold one key space per
|
|
199
|
+
viewer; `shared` ones one key space every viewer reads, written under the policy the
|
|
200
|
+
collection declares (`own`: viewers create, only the author edits or deletes;
|
|
201
|
+
`none`: nobody writes documents, viewers only increment counters and set marks).
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
"scopes": ["apps:storage", "apps:storage:publish"],
|
|
205
|
+
"storage": { "collections": {
|
|
206
|
+
"prefs": { "visibility": "private" },
|
|
207
|
+
"requests": { "visibility": "shared", "write": "own", "marks": ["vote"],
|
|
208
|
+
"indexes": { "status": "string" }, "showAuthor": true },
|
|
209
|
+
"plays": { "visibility": "shared", "write": "none", "counters": ["plays"] }
|
|
210
|
+
}}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
interface Request {
|
|
215
|
+
title: string;
|
|
216
|
+
status: 'open' | 'done';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const requests = app.storage.collection<Request>('requests');
|
|
220
|
+
|
|
221
|
+
const created = await requests.create({ title: 'More lighting presets', status: 'open' });
|
|
222
|
+
const doc = await requests.get(created.key); // null when there is none
|
|
223
|
+
await requests.put(created.key, { title: 'More lighting presets', status: 'done' }, { ifVersion: doc?.version });
|
|
224
|
+
await requests.mark(created.key, 'vote'); // { marked: true, count }
|
|
225
|
+
|
|
226
|
+
const page = await requests.list({ where: { status: 'open' }, sort: '-mark:vote', limit: 20 });
|
|
227
|
+
for (const item of page.items) {
|
|
228
|
+
console.log(item.data.title, item.marks.vote, item.author, item.viewer?.marks);
|
|
229
|
+
}
|
|
230
|
+
const mine = await requests.list({ author: 'me' });
|
|
231
|
+
const voted = await requests.list({ markedBy: 'me', mark: 'vote' });
|
|
232
|
+
|
|
233
|
+
await app.storage.collection('plays').increment('model-123', 'plays'); // creates the doc in a `none` collection
|
|
234
|
+
const { limits, usage } = await app.storage.info();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
- **Documents** are `{ key, data, version, createdAt, updatedAt, author, counters,
|
|
238
|
+
marks, viewer }`. `createdAt`/`updatedAt` are `Date`s. `author` is
|
|
239
|
+
`{ kind: 'user', isViewer, username? }` (the username only in a `showAuthor`
|
|
240
|
+
collection), `{ kind: 'counter' }` for a document a counter created, or
|
|
241
|
+
`{ kind: 'app' }` — never a numeric user id. `viewer` is `null` for an anonymous
|
|
242
|
+
viewer.
|
|
243
|
+
- **Conditional writes:** `put(key, data, { ifVersion })` and
|
|
244
|
+
`delete(key, { ifVersion })` write only at that version (`0` = only if absent);
|
|
245
|
+
a mismatch rejects `version_conflict` with `details.currentVersion`.
|
|
246
|
+
- **Lists** take ONE equality on a declared index (`where`), one `sort`
|
|
247
|
+
(`[-]key|createdAt|updatedAt|<index>|counter:<n>|mark:<n>`), `author: 'me'`,
|
|
248
|
+
`markedBy: 'me'` with `mark`, `keyPrefix`, `limit` and `cursor`. `nextCursor` is
|
|
249
|
+
`null` exactly on the last page; pass it back with the same query.
|
|
250
|
+
- **A refusal in the storage envelope is a `StorageError`** (an `ApiError`) with
|
|
251
|
+
the server's `code`, `status` and `details`; `retryAfter` is set when the server
|
|
252
|
+
named a wait. So is a name or key the client refuses before sending (an invalid
|
|
253
|
+
collection name is `collection_not_declared`; a key of `''`, `.` or `..` is
|
|
254
|
+
`invalid_request`). Anything else arrives as it would from any route: a response
|
|
255
|
+
without the envelope (a proxy error page, a site without these routes) is a plain
|
|
256
|
+
`ApiError` with its `status`, a request that never got an answer (offline, CORS,
|
|
257
|
+
DNS) is the `fetch` `TypeError`, and a 2xx the client cannot read is a
|
|
258
|
+
`CivitaiError`. `get` reads only `not_found` as "no document". Branch on `code`:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { StorageError } from '@civitai/sdk';
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await requests.put('r1', { title: 'Renamed', status: 'open' }, { ifVersion: 3 });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error instanceof StorageError && error.code === 'version_conflict') reloadAndRetry();
|
|
267
|
+
else if (error instanceof StorageError && error.code === 'consent_required') askForPublishConsent();
|
|
268
|
+
else throw error;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Anonymous viewers may read `shared` collections and nothing else
|
|
273
|
+
(`sign_in_required`). Creating or editing a document in a viewer-written shared
|
|
274
|
+
collection needs the consent-gated `apps:storage:publish`; deleting your own, marks
|
|
275
|
+
and counters need only `apps:storage`. Shared writes pass a minimum-trust gate and
|
|
276
|
+
moderation. Only phase-one operations exist: no batch writes, change feed, TTL or
|
|
277
|
+
owner-written documents yet.
|
|
278
|
+
|
|
279
|
+
The legacy stores below keep working, unchanged, while apps move; their methods are
|
|
280
|
+
`@deprecated`. Nothing is copied from them into collections.
|
|
281
|
+
|
|
194
282
|
## App storage
|
|
195
283
|
|
|
196
|
-
|
|
284
|
+
> **Deprecated** in favour of [document storage](#document-storage): declare a
|
|
285
|
+
> `private` collection and use `app.storage.collection(name)`.
|
|
286
|
+
|
|
287
|
+
`app.storage`'s `get`/`set`/`delete`/`list`/`getQuota` are the viewer's own key/value store, scoped to this app and this
|
|
197
288
|
block instance. It needs the block token the host mints — an app holding an
|
|
198
289
|
OAuth access token has no per-viewer app storage — and an anonymous viewer is
|
|
199
290
|
refused, so gate on `app.viewer` rather than reading an empty page as "nothing
|
|
@@ -237,12 +328,15 @@ try {
|
|
|
237
328
|
|
|
238
329
|
## Shared storage
|
|
239
330
|
|
|
331
|
+
> **Deprecated** in favour of [document storage](#document-storage): declare a
|
|
332
|
+
> `shared` collection, which has authorship, counters and marks built in.
|
|
333
|
+
|
|
240
334
|
`app.sharedStorage` is this app's **cross-user** key/value store — one namespace
|
|
241
335
|
every viewer reads and writes, unlike `app.storage`, which is private per viewer.
|
|
242
336
|
Five methods: `list`, `get`, `append`, `update`, `withdraw`.
|
|
243
337
|
|
|
244
338
|
```ts
|
|
245
|
-
const { items, nextCursor } = await app.sharedStorage.list({
|
|
339
|
+
const { items, nextCursor } = await app.sharedStorage.list({ limit: 20 }); // newest first
|
|
246
340
|
const { key } = await app.sharedStorage.append({ title: 'My deck', data: { cards } });
|
|
247
341
|
const row = await app.sharedStorage.get(key); // null when hidden or absent
|
|
248
342
|
await app.sharedStorage.update(key, { title: 'Renamed' });
|
|
@@ -268,9 +362,9 @@ other.** This is the opposite of `app.storage`, so do not carry that gate over:
|
|
|
268
362
|
supported path — and may **never** write. An anon write is a `403` from the
|
|
269
363
|
scope binding, not the `401` a missing token gives.
|
|
270
364
|
- **A signed-in viewer is not automatically a permitted writer.** Writes clear a
|
|
271
|
-
server-side minimum-trust gate (account age,
|
|
272
|
-
linked OAuth account). Treat a write refusal
|
|
273
|
-
message in the UI, not an unexpected error.
|
|
365
|
+
server-side minimum-trust gate (account age, completed onboarding, verified
|
|
366
|
+
email or a linked OAuth account, not banned or muted). Treat a write refusal
|
|
367
|
+
as a normal outcome with a real message in the UI, not an unexpected error.
|
|
274
368
|
|
|
275
369
|
So gate the write affordances on `app.viewer`, and render the reads regardless.
|
|
276
370
|
|
|
@@ -285,7 +379,9 @@ that type-checks:
|
|
|
285
379
|
`app.storage`'s, absence proves the scan completed.
|
|
286
380
|
- **`append` takes no key.** The server mints a ULID and returns it, so one
|
|
287
381
|
viewer cannot overwrite another's row. Use `update` for an in-place edit of a
|
|
288
|
-
row this viewer authored.
|
|
382
|
+
row this viewer authored. It also means `list`'s `prefix` is no namespace of
|
|
383
|
+
yours: a prefix you choose (`'deck:'`) matches none of the rows `append`
|
|
384
|
+
creates. Put what you filter by in `data` and filter the page yourself.
|
|
289
385
|
- **`value` is `unknown` on the way out.** The row was written by some other
|
|
290
386
|
viewer's copy of your app, possibly an older version, so its shape is a fact
|
|
291
387
|
about the data rather than a promise the client can make. Narrow it yourself.
|
|
@@ -788,6 +884,49 @@ entry.
|
|
|
788
884
|
`@civitai/sdk/testing` has `createFakeTransport()`: pass it as
|
|
789
885
|
`initialize({ transport })` and script the host's answers.
|
|
790
886
|
|
|
887
|
+
### A fake document store
|
|
888
|
+
|
|
889
|
+
`createFakeStorage({ manifest })` is an in-memory document store behind the same
|
|
890
|
+
`Http` seam the client uses, enforcing the server's rules: scopes and consent, write
|
|
891
|
+
policies, the minimum-trust gate, anonymous readers, keys, limits and quotas,
|
|
892
|
+
moderation, error codes and statuses. This package runs the platform's own storage
|
|
893
|
+
contract against it, so where the contract speaks the fake and the server agree.
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
import { createFakeStorage } from '@civitai/sdk/testing';
|
|
897
|
+
|
|
898
|
+
const fake = createFakeStorage({ manifest, viewer: { username: 'alice' } });
|
|
899
|
+
const prefs = fake.storage.collection('prefs'); // the real client, against the fake
|
|
900
|
+
await prefs.put('theme', { mode: 'dark' });
|
|
901
|
+
|
|
902
|
+
const bob = fake.as({ viewer: { username: 'bob', trusted: false } }).storage;
|
|
903
|
+
const anon = fake.as({ viewer: null }).storage;
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
**Rate limits are OFF by default**, so your own tests are never throttled. Pass
|
|
907
|
+
`enforceRateLimits: true` to apply the server's — the per-minute and per-day windows
|
|
908
|
+
for reads, private writes and deletes, marks and counters, and reports, plus the
|
|
909
|
+
shared write budget — when you want to test how the app handles `rate_limited`
|
|
910
|
+
(drive the clock with `now`).
|
|
911
|
+
|
|
912
|
+
Where it differs from the server on purpose:
|
|
913
|
+
|
|
914
|
+
- A declaration using a feature the platform has not shipped — `write: "owner"` or
|
|
915
|
+
`"anyone"`, `kind: "log"`/`maxEntries`, `ttlSeconds`, `dailyWritesPerAuthor` —
|
|
916
|
+
makes `createFakeStorage` throw, naming it, instead of failing per request. It is
|
|
917
|
+
not a manifest validator otherwise; `civitai app validate` is.
|
|
918
|
+
- A list `nextCursor` is an opaque token, refused for any other collection, sort or
|
|
919
|
+
filter than the query that issued it. Treat it as opaque against the server too.
|
|
920
|
+
|
|
921
|
+
`fake.calls` records every request and answer. Options also cover smaller `limits`,
|
|
922
|
+
`consent` (revoked scopes, or `'unavailable'`), `appStatus`, `disabled` and a custom
|
|
923
|
+
`moderate`; by default the scanner refuses the words in `FAKE_STORAGE_MODERATION`. It
|
|
924
|
+
does not model moderator hides or the review-preview namespace, and at the `Http`
|
|
925
|
+
seam there is no URL resolution — the client refuses the segments a URL would
|
|
926
|
+
rewrite (`''`, `.`, `..`) before sending.
|
|
927
|
+
|
|
928
|
+
### The legacy key/value stores
|
|
929
|
+
|
|
791
930
|
There is no published fake for app storage, on purpose. The seam is `fetch`, so
|
|
792
931
|
pass your own `fetch` to `initialize({ token, fetch })` and the client's URLs,
|
|
793
932
|
bodies, statuses and date revival are all real — which a fake replacing the
|
package/dist/app/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type Host } from '../host/index.js';
|
|
|
4
4
|
import { type OrchestrationClient } from '../orchestration/index.js';
|
|
5
5
|
import { type GrantOptions, type Scope, type TokenOptions, type TokenSessionOptions } from '../session/index.js';
|
|
6
6
|
import { type SharedStorageClient } from '../shared-storage/index.js';
|
|
7
|
+
import { type DocumentStorage } from '../doc-store/index.js';
|
|
7
8
|
import { type SiteClient } from '../site/index.js';
|
|
8
9
|
import { type StorageClient } from '../storage/index.js';
|
|
9
10
|
import { type WorkflowsClient } from '../workflows/index.js';
|
|
@@ -20,15 +21,26 @@ export interface AppClient {
|
|
|
20
21
|
*/
|
|
21
22
|
readonly site: SiteClient;
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
+
* App storage. Two stores share this property while apps migrate:
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* - **The document store** — `collection(name)` and `info()`: the collections
|
|
27
|
+
* the app declares under `storage.collections` in its manifest, private or
|
|
28
|
+
* shared, with counters, marks, declared indexes and conditional writes.
|
|
29
|
+
* Use this for anything new.
|
|
30
|
+
* - **The legacy per-viewer key/value store** — `get`, `set`, `delete`, `list`,
|
|
31
|
+
* `getQuota`, each `@deprecated`. It keeps working, unchanged, until the apps
|
|
32
|
+
* that use it have moved; nothing is copied between the two.
|
|
33
|
+
*
|
|
34
|
+
* 🔴 Both require the block token the host mints: an app that authenticated
|
|
35
|
+
* with an OAuth access token is refused. The legacy store refuses an anonymous
|
|
36
|
+
* viewer outright; the document store lets one read SHARED collections only.
|
|
29
37
|
*/
|
|
30
|
-
readonly storage: StorageClient;
|
|
38
|
+
readonly storage: StorageClient & DocumentStorage;
|
|
31
39
|
/**
|
|
40
|
+
* @deprecated Declare a `shared` collection under `storage.collections` and use
|
|
41
|
+
* `app.storage.collection(name)`. This store keeps working, unchanged, until the
|
|
42
|
+
* apps that use it have moved.
|
|
43
|
+
*
|
|
32
44
|
* This app's CROSS-USER store — one namespace every viewer of this app reads
|
|
33
45
|
* and writes, which is what makes a shared feed or a "popular" rail possible.
|
|
34
46
|
*
|
package/dist/app/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ApiError, createHttp } from '../http/index.js';
|
|
|
5
5
|
import { createOrchestrationClient, DEFAULT_ORCHESTRATION_URL, } from '../orchestration/index.js';
|
|
6
6
|
import { createTokenSession, } from '../session/index.js';
|
|
7
7
|
import { createSharedStorageClient, } from '../shared-storage/index.js';
|
|
8
|
+
import { createDocumentStorage } from '../doc-store/index.js';
|
|
8
9
|
import { createSiteClient, DEFAULT_SITE_URL } from '../site/index.js';
|
|
9
10
|
import { createStorageClient } from '../storage/index.js';
|
|
10
11
|
import { createWorkflowsClient } from '../workflows/index.js';
|
|
@@ -85,15 +86,19 @@ function createAppClient(session, options, holdsBlockTokenNow = () => false, hol
|
|
|
85
86
|
// clients' routes live under the same base URL, so a `siteUrl` override
|
|
86
87
|
// redirects them together.
|
|
87
88
|
// Wrapped once, so `BLOCK_NAMESPACE` is the single place that decides where the
|
|
88
|
-
// diagnosis applies. None of the
|
|
89
|
-
// of its own: their routes are `blocks/app-storage/*`, `blocks/shared-storage
|
|
90
|
-
// and `blocks/workflows/*`, which that namespace already excludes, and
|
|
91
|
-
// `test/storage/seam.test.ts`, `test/shared-storage/seam.test.ts
|
|
92
|
-
// `test/workflows/seam.test.ts` pin the
|
|
89
|
+
// diagnosis applies. None of the four `blocks/*` clients needs an exemption
|
|
90
|
+
// of its own: their routes are `blocks/app-storage/*`, `blocks/shared-storage/*`,
|
|
91
|
+
// `blocks/storage/*` and `blocks/workflows/*`, which that namespace already excludes, and
|
|
92
|
+
// `test/storage/seam.test.ts`, `test/shared-storage/seam.test.ts`,
|
|
93
|
+
// `test/doc-store/client.test.ts` and `test/workflows/seam.test.ts` pin the four
|
|
94
|
+
// `BASE` values there textually.
|
|
93
95
|
const siteHttp = explainApiRefusal(http(options.siteUrl ?? DEFAULT_SITE_URL), holdsBlockTokenNow);
|
|
94
96
|
return {
|
|
95
97
|
site: createSiteClient(siteHttp),
|
|
96
|
-
|
|
98
|
+
// One object, two stores, no shared names: the legacy five and the document
|
|
99
|
+
// store's `collection`/`info`. Spread rather than chained so neither keeps a
|
|
100
|
+
// reference to the other.
|
|
101
|
+
storage: { ...createStorageClient(siteHttp), ...createDocumentStorage(siteHttp) },
|
|
97
102
|
sharedStorage: createSharedStorageClient(siteHttp),
|
|
98
103
|
workflows: createWorkflowsClient(explainWorkflowRefusal(siteHttp, holdsOAuthTokenNow)),
|
|
99
104
|
orchestration: createOrchestrationClient(refuseBlockToken(http(options.orchestrationUrl ?? DEFAULT_ORCHESTRATION_URL), holdsBlockTokenNow)),
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type Http } from '../http/index.js';
|
|
2
|
+
import { type DocumentStorage } from './index.js';
|
|
3
|
+
declare const DEFAULT_LIMITS: {
|
|
4
|
+
appBytes: number;
|
|
5
|
+
appDocs: number;
|
|
6
|
+
viewerBytes: number;
|
|
7
|
+
viewerDocs: number;
|
|
8
|
+
};
|
|
9
|
+
/** The default scanner's words: the contract's `conventions`, so a test can reach each refusal. */
|
|
10
|
+
export declare const FAKE_STORAGE_MODERATION: Readonly<{
|
|
11
|
+
blockedText: "contract-blocked-term";
|
|
12
|
+
blockedLink: "https://contract-blocked.example/x";
|
|
13
|
+
scannerDown: "contract-scanner-down";
|
|
14
|
+
}>;
|
|
15
|
+
export interface FakeStorageViewer {
|
|
16
|
+
/** The viewer's identity in this fake, and what `showAuthor` collections display. */
|
|
17
|
+
username: string;
|
|
18
|
+
/**
|
|
19
|
+
* The viewer's numeric user id, as the platform keys it. Internal: no response
|
|
20
|
+
* carries it, which a test can check by choosing a distinctive one. Default: a
|
|
21
|
+
* small number assigned on first use.
|
|
22
|
+
*/
|
|
23
|
+
id?: number;
|
|
24
|
+
/** Passes the minimum-trust gate on shared writes. Default `true`. */
|
|
25
|
+
trusted?: boolean;
|
|
26
|
+
/** `details.reason` when untrusted. Default `account_too_new`. */
|
|
27
|
+
untrustedReason?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Who is calling: a viewer (or `null`, anonymous) holding a token of some kind and scopes. */
|
|
30
|
+
export interface FakeStorageCaller {
|
|
31
|
+
/** Default: the fake's `viewer`. `null` is an anonymous viewer with a token. */
|
|
32
|
+
viewer?: FakeStorageViewer | null;
|
|
33
|
+
/** `block` (default), `dev` (the developer's own namespace) or `none` (no token at all). */
|
|
34
|
+
token?: 'block' | 'dev' | 'none';
|
|
35
|
+
/** The token's scopes. Default: the manifest's `scopes`. */
|
|
36
|
+
scopes?: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
export type FakeModerationVerdict = 'blocked' | 'link' | 'unavailable' | null;
|
|
39
|
+
export interface FakeStorageOptions {
|
|
40
|
+
/** The app's manifest; `storage.collections` is what the fake enforces. */
|
|
41
|
+
manifest: {
|
|
42
|
+
scopes?: readonly string[];
|
|
43
|
+
storage?: unknown;
|
|
44
|
+
};
|
|
45
|
+
/** The default caller's viewer. Default `{ username: 'viewer' }`; `null` for anonymous. */
|
|
46
|
+
viewer?: FakeStorageViewer | null;
|
|
47
|
+
/** Default `approved`; anything else refuses every call `not_eligible`. */
|
|
48
|
+
appStatus?: string;
|
|
49
|
+
/** Consent state: scopes the viewer revoked, or `unavailable` (the marker could not be read). */
|
|
50
|
+
consent?: {
|
|
51
|
+
revoked: readonly string[];
|
|
52
|
+
} | 'unavailable';
|
|
53
|
+
/** `true` answers every data operation `storage_disabled`, as a server with no database does. */
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
/** Override the per-app and per-viewer ceilings, e.g. a small `viewerDocs` to reach the quota. */
|
|
56
|
+
limits?: Partial<typeof DEFAULT_LIMITS>;
|
|
57
|
+
/**
|
|
58
|
+
* Apply the server's rate limits: the per-minute and per-day windows (reads, private
|
|
59
|
+
* writes and deletes, marks and counters, reports) and the shared write budget.
|
|
60
|
+
* Default `false`, so an app's own tests are never throttled; turn it on to test how
|
|
61
|
+
* the app handles `rate_limited`.
|
|
62
|
+
*/
|
|
63
|
+
enforceRateLimits?: boolean;
|
|
64
|
+
/** The clock rate limits and timestamps read. Default `Date.now`. */
|
|
65
|
+
now?: () => number;
|
|
66
|
+
/**
|
|
67
|
+
* Classify one string (a value, an object key or a document key) of a shared write.
|
|
68
|
+
* Default: {@link FAKE_STORAGE_MODERATION}'s words.
|
|
69
|
+
*/
|
|
70
|
+
moderate?: (text: string) => FakeModerationVerdict;
|
|
71
|
+
}
|
|
72
|
+
/** One request as the fake received it, and what it answered. */
|
|
73
|
+
export interface FakeStorageCall {
|
|
74
|
+
method: string;
|
|
75
|
+
path: string;
|
|
76
|
+
query: Record<string, string | string[]>;
|
|
77
|
+
body: unknown;
|
|
78
|
+
status: number;
|
|
79
|
+
response: unknown;
|
|
80
|
+
}
|
|
81
|
+
export interface FakeStorageSession {
|
|
82
|
+
/** The `Http` seam, as this caller. */
|
|
83
|
+
readonly http: Http;
|
|
84
|
+
/** The real client over that seam. */
|
|
85
|
+
readonly storage: DocumentStorage;
|
|
86
|
+
}
|
|
87
|
+
export interface FakeStorage extends FakeStorageSession {
|
|
88
|
+
/** The same store, as a different caller. */
|
|
89
|
+
as(caller: FakeStorageCaller): FakeStorageSession;
|
|
90
|
+
/** Every request received, in order, by any caller. */
|
|
91
|
+
readonly calls: FakeStorageCall[];
|
|
92
|
+
}
|
|
93
|
+
export declare function createFakeStorage(options: FakeStorageOptions): FakeStorage;
|
|
94
|
+
export {};
|