@civitai/sdk 0.15.0 → 0.16.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/README.md +13 -1
- package/dist/doc-store/fake.d.ts +7 -0
- package/dist/doc-store/fake.js +71 -2
- package/dist/doc-store/index.d.ts +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -274,7 +274,12 @@ Anonymous viewers may read `shared` collections and nothing else
|
|
|
274
274
|
(`sign_in_required`). Creating or editing a document in a viewer-written shared
|
|
275
275
|
collection needs the consent-gated `apps:storage:publish`; deleting your own, marks
|
|
276
276
|
and counters need only `apps:storage`. Shared writes pass a minimum-trust gate and
|
|
277
|
-
moderation.
|
|
277
|
+
moderation. A shared `write: "own"` collection whose manifest declares
|
|
278
|
+
`ownedModelVersions` (pointers such as `/workflow/modelVersionId`) refuses a create
|
|
279
|
+
or `put` whose `data` holds, at one of those fields, anything but a model version
|
|
280
|
+
the viewer owns: `resource_not_owned` (403), `details.pointer` naming the field,
|
|
281
|
+
before moderation and before any write budget. An absent field is allowed;
|
|
282
|
+
ownership is checked only when the document is written. Only phase-one operations exist: no batch writes, change feed, TTL or
|
|
278
283
|
owner-written documents yet.
|
|
279
284
|
|
|
280
285
|
The legacy stores below keep working, unchanged, while apps move; their methods are
|
|
@@ -974,6 +979,13 @@ Where it differs from the server on purpose:
|
|
|
974
979
|
not a manifest validator otherwise; `civitai app validate` is.
|
|
975
980
|
- A list `nextCursor` is an opaque token, refused for any other collection, sort or
|
|
976
981
|
filter than the query that issued it. Treat it as opaque against the server too.
|
|
982
|
+
- It has no model versions of its own, so a collection's `ownedModelVersions` rule
|
|
983
|
+
checks writes against the viewer's `modelVersions` — the ids that viewer owns,
|
|
984
|
+
none by default (so every id is refused, as the server refuses one that is not
|
|
985
|
+
the writer's):
|
|
986
|
+
`fake.as({ viewer: { username: 'bob', modelVersions: [4317] } })`. It does not
|
|
987
|
+
model the server's ownership lookup failing (a retryable 503
|
|
988
|
+
`service_unavailable`).
|
|
977
989
|
|
|
978
990
|
`fake.calls` records every request and answer. Options also cover smaller `limits`,
|
|
979
991
|
`consent` (revoked scopes, or `'unavailable'`), `appStatus`, `disabled` and a custom
|
package/dist/doc-store/fake.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface FakeStorageViewer {
|
|
|
25
25
|
trusted?: boolean;
|
|
26
26
|
/** `details.reason` when untrusted. Default `account_too_new`. */
|
|
27
27
|
untrustedReason?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Ids of the model versions whose model this viewer owns — what a collection's
|
|
30
|
+
* `ownedModelVersions` rule checks a write against. Default: none, so every id
|
|
31
|
+
* at a declared field is refused `resource_not_owned`, as the server refuses a
|
|
32
|
+
* version that is not the writer's (or does not exist).
|
|
33
|
+
*/
|
|
34
|
+
modelVersions?: readonly number[];
|
|
28
35
|
}
|
|
29
36
|
/** Who is calling: a viewer (or `null`, anonymous) holding a token of some kind and scopes. */
|
|
30
37
|
export interface FakeStorageCaller {
|
package/dist/doc-store/fake.js
CHANGED
|
@@ -18,10 +18,15 @@ import { createDocumentStorage } from './index.js';
|
|
|
18
18
|
* the fake is CREATED rather than per request; and a list cursor is an opaque token
|
|
19
19
|
* bound to its whole query, not the server's sort-bound keyset string.
|
|
20
20
|
*
|
|
21
|
+
* A collection's `ownedModelVersions` rule is enforced; the fake has no model
|
|
22
|
+
* versions of its own, so which ones a viewer owns is `FakeStorageViewer.modelVersions`
|
|
23
|
+
* (default none).
|
|
24
|
+
*
|
|
21
25
|
* NOT MODELLED: moderator hides (a hidden document reads as not found on the server;
|
|
22
26
|
* nothing here hides one), moderator exemption from the scanner, the review-preview
|
|
23
|
-
* namespace,
|
|
24
|
-
* binding, an approval read that fails)
|
|
27
|
+
* namespace, the token middleware's other refusals (revoked instance, context
|
|
28
|
+
* binding, an approval read that fails), and a failed ownership lookup (the
|
|
29
|
+
* server's retryable 503 `service_unavailable`).
|
|
25
30
|
*/
|
|
26
31
|
// ---------------------------------------------------------------------------
|
|
27
32
|
// Bounds — the server's, one copy here (the contract run pins the ones it reaches)
|
|
@@ -85,6 +90,7 @@ const STATUS = {
|
|
|
85
90
|
not_eligible: 403,
|
|
86
91
|
not_author: 403,
|
|
87
92
|
write_policy: 403,
|
|
93
|
+
resource_not_owned: 403,
|
|
88
94
|
not_found: 404,
|
|
89
95
|
collection_not_declared: 404,
|
|
90
96
|
method_not_allowed: 405,
|
|
@@ -97,6 +103,11 @@ const STATUS = {
|
|
|
97
103
|
moderation_unavailable: 503,
|
|
98
104
|
consent_unavailable: 503,
|
|
99
105
|
};
|
|
106
|
+
/** The manifest's `ownedModelVersions` pointer: 1 to 4 plain identifier segments. */
|
|
107
|
+
const OWNED_POINTER = /^(\/[A-Za-z_][A-Za-z0-9_]{0,63}){1,4}$/u;
|
|
108
|
+
const MAX_OWNED_POINTERS = 4;
|
|
109
|
+
/** Postgres `integer`: the server refuses a larger id before querying for it. */
|
|
110
|
+
const INT4_MAX = 2_147_483_647;
|
|
100
111
|
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
101
112
|
/** Declarations the platform does not serve yet, each with what to do instead. */
|
|
102
113
|
const PHASED = [
|
|
@@ -144,12 +155,30 @@ function parseManifest(manifest) {
|
|
|
144
155
|
else
|
|
145
156
|
errors.push(`${at}.indexes.${path}: type must be one of ${INDEX_TYPES.join(', ')}`);
|
|
146
157
|
}
|
|
158
|
+
const ownedModelVersions = [];
|
|
159
|
+
if (raw.ownedModelVersions !== undefined) {
|
|
160
|
+
const list = raw.ownedModelVersions;
|
|
161
|
+
if (!shared || raw.write !== 'own') {
|
|
162
|
+
errors.push(`${at}.ownedModelVersions: applies only to shared collections with write "own"`);
|
|
163
|
+
}
|
|
164
|
+
else if (!Array.isArray(list) ||
|
|
165
|
+
list.length < 1 ||
|
|
166
|
+
list.length > MAX_OWNED_POINTERS ||
|
|
167
|
+
!list.every((p) => typeof p === 'string' && OWNED_POINTER.test(p)) ||
|
|
168
|
+
new Set(list).size !== list.length) {
|
|
169
|
+
errors.push(`${at}.ownedModelVersions: must be 1 to ${MAX_OWNED_POINTERS} distinct pointers such as "/workflow/modelVersionId"`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
ownedModelVersions.push(...list);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
147
175
|
const decl = {
|
|
148
176
|
visibility: raw.visibility,
|
|
149
177
|
counters: shared ? names(raw.counters) : [],
|
|
150
178
|
marks: shared ? names(raw.marks) : [],
|
|
151
179
|
indexes,
|
|
152
180
|
showAuthor: shared && raw.showAuthor === true,
|
|
181
|
+
ownedModelVersions,
|
|
153
182
|
};
|
|
154
183
|
if (shared && (raw.write === 'own' || raw.write === 'none'))
|
|
155
184
|
decl.write = raw.write;
|
|
@@ -893,6 +922,41 @@ export function createFakeStorage(options) {
|
|
|
893
922
|
});
|
|
894
923
|
}
|
|
895
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* The collection's `ownedModelVersions` rule, as the server's
|
|
927
|
+
* `doc-store-ownership.ts` applies it: a pointer that resolves to nothing is
|
|
928
|
+
* allowed; every value found must be a positive `integer` id — all of them are
|
|
929
|
+
* checked before any ownership — and then the writer's own version. One code
|
|
930
|
+
* and message for every reason, so a refusal reveals nothing about whether a
|
|
931
|
+
* version exists.
|
|
932
|
+
*/
|
|
933
|
+
function assertOwnedModelVersions(decl, data, viewer) {
|
|
934
|
+
if (decl.ownedModelVersions.length === 0)
|
|
935
|
+
return;
|
|
936
|
+
const notOwned = (pointer) => refuse('resource_not_owned', 'the model version at this field is not one the author owns', { pointer });
|
|
937
|
+
const claimed = [];
|
|
938
|
+
for (const pointer of decl.ownedModelVersions) {
|
|
939
|
+
let cur = data;
|
|
940
|
+
let found = true;
|
|
941
|
+
for (const seg of pointer.split('/').slice(1)) {
|
|
942
|
+
if (!isObject(cur) || !Object.prototype.hasOwnProperty.call(cur, seg)) {
|
|
943
|
+
found = false;
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
cur = cur[seg];
|
|
947
|
+
}
|
|
948
|
+
if (!found)
|
|
949
|
+
continue;
|
|
950
|
+
if (typeof cur !== 'number' || !Number.isInteger(cur) || cur < 1 || cur > INT4_MAX)
|
|
951
|
+
throw notOwned(pointer);
|
|
952
|
+
claimed.push({ pointer, id: cur });
|
|
953
|
+
}
|
|
954
|
+
const owned = new Set(viewer.modelVersions ?? []);
|
|
955
|
+
for (const { pointer, id } of claimed) {
|
|
956
|
+
if (!owned.has(id))
|
|
957
|
+
throw notOwned(pointer);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
896
960
|
/** The server's refusals that are ITS fault: refunded without a rejection charge. */
|
|
897
961
|
const isOurFault = (err) => !(err instanceof Refusal) ||
|
|
898
962
|
(err.code === 'rate_limited' ? err.details?.reason === 'contention' : ['storage_disabled', 'moderation_unavailable', 'consent_unavailable'].includes(err.code));
|
|
@@ -1032,6 +1096,11 @@ export function createFakeStorage(options) {
|
|
|
1032
1096
|
throw invalid('ifVersion must be a non-negative integer');
|
|
1033
1097
|
deleteVersion = Number(raw);
|
|
1034
1098
|
}
|
|
1099
|
+
// Every write that sets `data`, before the trust gate, the write budget and the
|
|
1100
|
+
// moderation scan (and so before the store's own checks), as the server orders it.
|
|
1101
|
+
const newData = created?.data ?? putIn?.data;
|
|
1102
|
+
if (newData)
|
|
1103
|
+
assertOwnedModelVersions(decl, newData, viewer);
|
|
1035
1104
|
const write = (moderated, landed) => {
|
|
1036
1105
|
if (created) {
|
|
1037
1106
|
if (moderated)
|
|
@@ -171,7 +171,7 @@ export interface DocumentStorage {
|
|
|
171
171
|
* Every code the server documents for these routes. The type stays open
|
|
172
172
|
* (`string & {}`) so a code a later server adds still reaches the caller.
|
|
173
173
|
*/
|
|
174
|
-
export type StorageErrorCode = 'invalid_request' | 'index_not_declared' | 'unauthenticated' | 'sign_in_required' | 'insufficient_scope' | 'consent_required' | 'not_eligible' | 'not_author' | 'write_policy' | 'not_found' | 'collection_not_declared' | 'method_not_allowed' | 'version_conflict' | 'index_building' | 'cursor_expired' | 'too_large' | 'quota_exceeded' | 'content_rejected' | 'rate_limited' | 'internal_error' | 'storage_disabled' | 'moderation_unavailable' | 'consent_unavailable' | 'service_unavailable' | (string & {});
|
|
174
|
+
export type StorageErrorCode = 'invalid_request' | 'index_not_declared' | 'unauthenticated' | 'sign_in_required' | 'insufficient_scope' | 'consent_required' | 'not_eligible' | 'not_author' | 'write_policy' | 'resource_not_owned' | 'not_found' | 'collection_not_declared' | 'method_not_allowed' | 'version_conflict' | 'index_building' | 'cursor_expired' | 'too_large' | 'quota_exceeded' | 'content_rejected' | 'rate_limited' | 'internal_error' | 'storage_disabled' | 'moderation_unavailable' | 'consent_unavailable' | 'service_unavailable' | (string & {});
|
|
175
175
|
/**
|
|
176
176
|
* A refusal from the document store, in its one envelope `{ code, message, error, details? }`.
|
|
177
177
|
* Branch on `code`; `status` and `body` are {@link ApiError}'s, as sent.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.16.0";
|
package/package.json
CHANGED