@civitai/blocks-react 0.61.1 → 0.62.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.
|
@@ -599,18 +599,70 @@ export interface MockHostOptions {
|
|
|
599
599
|
* {@link MockHost.setScenario}.
|
|
600
600
|
*/
|
|
601
601
|
disallowedAccountTypes?: BuzzAccountType[];
|
|
602
|
+
/**
|
|
603
|
+
* The scopes the app's `block.manifest.json` DECLARES — the set the real
|
|
604
|
+
* host's token mint draws from.
|
|
605
|
+
*
|
|
606
|
+
* 🔴 **STORAGE IS GATED ON THIS, AND THE DEFAULT IS EMPTY.** A storage op whose
|
|
607
|
+
* scope is not in here is refused exactly as the server refuses it, because the
|
|
608
|
+
* server's test is presence in the block's approved scope set (see
|
|
609
|
+
* `BLOCK_SCOPES` in `@civitai/app-sdk`) and an undeclared scope is never
|
|
610
|
+
* approved. Omit this and every `APP_STORAGE_*` / `SHARED_*` call fails.
|
|
611
|
+
*
|
|
612
|
+
* That is a DELIBERATE BREAKING DEFAULT. Until this existed the mock host
|
|
613
|
+
* served storage unconditionally, so an app that forgot the scopes passed its
|
|
614
|
+
* whole suite and the dev harness and then failed every save in production —
|
|
615
|
+
* the one storage failure mode that actually ships was the only one the mock
|
|
616
|
+
* could not produce. A default of "permissive" would have left that true for
|
|
617
|
+
* every app that did not opt in, i.e. precisely the apps that did not know the
|
|
618
|
+
* scopes existed.
|
|
619
|
+
*
|
|
620
|
+
* Pass what your manifest declares — ideally by importing your own
|
|
621
|
+
* `block.manifest.json` as `manifest`, so the two cannot drift:
|
|
622
|
+
*
|
|
623
|
+
* ```ts
|
|
624
|
+
* createMockHost({ declaredScopes: manifest.scopes });
|
|
625
|
+
* ```
|
|
626
|
+
*
|
|
627
|
+
* ⚠️ The `import` line is described rather than shown ON PURPOSE, and please
|
|
628
|
+
* do not helpfully add it back. `tests/guards/blocks-react-entry-directory-names.test.mjs`
|
|
629
|
+
* extracts import specifiers with a raw regex over the whole file —
|
|
630
|
+
* `/\bfrom\s*['"]([^'"]+)['"]/g`, comments included — so a `from '…'` inside a
|
|
631
|
+
* doc comment is read as a real edge and resolved against THIS file's
|
|
632
|
+
* directory. A relative path to a consumer's manifest does not exist from
|
|
633
|
+
* here, and the guard fails with `unresolvable specifier`. Measured: it went
|
|
634
|
+
* red on all five `Starter (…)` matrix legs.
|
|
635
|
+
*
|
|
636
|
+
* A test that only exercises storage mechanics (quota, caps, row limits) and
|
|
637
|
+
* does not care about authorization should declare the storage scopes
|
|
638
|
+
* explicitly rather than reach for a permissive flag — there is none, on
|
|
639
|
+
* purpose.
|
|
640
|
+
*
|
|
641
|
+
* ⚠️ Scopes OTHER than storage are not read from here yet. `ai:write:budgeted`
|
|
642
|
+
* keeps its own `consentGranted` flag, because `buzzBudget` is conditional on
|
|
643
|
+
* it and `setScenario` can toggle it mid-session; giving one scope two sources
|
|
644
|
+
* of truth is how they drift.
|
|
645
|
+
*/
|
|
646
|
+
declaredScopes?: string[];
|
|
602
647
|
/**
|
|
603
648
|
* STORAGE scenario: in-memory KV backend (seed / quota / failNext). See
|
|
604
649
|
* {@link MockStorageScenario}. When omitted, the store starts EMPTY with the
|
|
605
|
-
* v0 defaults
|
|
606
|
-
*
|
|
650
|
+
* v0 defaults.
|
|
651
|
+
*
|
|
652
|
+
* ⚠️ This governs the BACKEND, not authorization. Storage is additionally
|
|
653
|
+
* gated on {@link MockHostOptions.declaredScopes}, which defaults to empty —
|
|
654
|
+
* so a scenario alone no longer makes storage answer. (It used to: this doc
|
|
655
|
+
* said `APP_STORAGE_*` was "answered either way", and that was the defect.)
|
|
607
656
|
*/
|
|
608
657
|
storage?: MockStorageScenario;
|
|
609
658
|
/**
|
|
610
659
|
* SHARED scenario: in-memory, app-scoped, votable backend (seed / failNext).
|
|
611
|
-
* See {@link MockSharedScenario}. When omitted, the shared store starts EMPTY
|
|
612
|
-
*
|
|
613
|
-
*
|
|
660
|
+
* See {@link MockSharedScenario}. When omitted, the shared store starts EMPTY.
|
|
661
|
+
*
|
|
662
|
+
* ⚠️ As with {@link MockHostOptions.storage}, this governs the BACKEND and not
|
|
663
|
+
* authorization: `SHARED_*` is additionally gated on
|
|
664
|
+
* {@link MockHostOptions.declaredScopes} (`apps:storage:shared:read` /
|
|
665
|
+
* `:write`), which defaults to empty.
|
|
614
666
|
*/
|
|
615
667
|
shared?: MockSharedScenario;
|
|
616
668
|
/** Host theme delivered in `BLOCK_INIT` + context. Default `'dark'`. */
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
import { APP_STORAGE_ERROR_REQUEST_FAILED, APP_STORAGE_ERROR_USER_QUOTA_EXCEEDED, APP_STORAGE_ERROR_USER_ROW_LIMIT, APP_STORAGE_ERROR_VALUE_TOO_LARGE, APP_STORAGE_MAX_BYTES, APP_STORAGE_MAX_ROWS, APP_STORAGE_MAX_VALUE_BYTES, BrowsingLevel, SFW_LEVELS, } from '@civitai/app-sdk/blocks';
|
|
41
41
|
import { consentUnavailablePayload, isKnownBlockScope, resolveUngrantableConsentNotice, } from './consent.js';
|
|
42
|
+
import { requiredStorageScope, storageResultType, storageScopeDeniedMessage, storageScopeDeniedPayload, } from './mockHostScopes.js';
|
|
42
43
|
import { hostContextWithTheme } from '../transport/transport.js';
|
|
43
44
|
import { isRoutableRequestId } from '../transport/requestId.js';
|
|
44
45
|
/**
|
|
@@ -849,6 +850,19 @@ export function createMockHost(options = {}) {
|
|
|
849
850
|
* precisely what `./consent.js`'s header warns about.
|
|
850
851
|
*/
|
|
851
852
|
const extraGrantedScopes = new Set();
|
|
853
|
+
/**
|
|
854
|
+
* What the app's manifest DECLARES — the set the real host's token mint draws
|
|
855
|
+
* from, and the set the storage gate tests presence in.
|
|
856
|
+
*
|
|
857
|
+
* 🔴 Deliberately NOT defaulted to anything permissive. See
|
|
858
|
+
* {@link MockHostOptions.declaredScopes}: a permissive default would leave the
|
|
859
|
+
* pre-gate behaviour in place for every app that did not opt in, which is the
|
|
860
|
+
* population the gate exists for.
|
|
861
|
+
*
|
|
862
|
+
* This is a SNAPSHOT at install time, unlike `consentGranted`, which
|
|
863
|
+
* `setScenario` can toggle. A manifest does not change mid-session.
|
|
864
|
+
*/
|
|
865
|
+
const declaredScopeSet = new Set(options.declaredScopes ?? []);
|
|
852
866
|
/** Everything the CURRENT token carries. One reader, so the two cannot drift. */
|
|
853
867
|
const currentScopes = () => [
|
|
854
868
|
...(consentGranted ? [BUDGETED_SCOPE] : []),
|
|
@@ -911,6 +925,30 @@ export function createMockHost(options = {}) {
|
|
|
911
925
|
const typed = msg;
|
|
912
926
|
options.onOutbound?.({ type: typed.type, payload: typed.payload });
|
|
913
927
|
const requestId = typed.payload?.requestId;
|
|
928
|
+
// ---- THE STORAGE SCOPE GATE — see ./mockHostScopes.ts ----
|
|
929
|
+
//
|
|
930
|
+
// The server's test is PRESENCE in the block's approved scope set, so an
|
|
931
|
+
// UNDECLARED scope is refused before the backend is ever consulted. That
|
|
932
|
+
// ordering is the point: it refuses even when the scenario, the quota and
|
|
933
|
+
// the row budget would all have allowed the op, because production does.
|
|
934
|
+
//
|
|
935
|
+
// 🔴 IT SITS AHEAD OF THE SWITCH SO ONE RULE COVERS EVERY SURFACE. The
|
|
936
|
+
// alternative — a check inside each of the 15 storage handlers — is the
|
|
937
|
+
// shape that regenerates the same omission at every new site: a storage
|
|
938
|
+
// message added later is governed the moment it joins the table in
|
|
939
|
+
// `mockHostScopes.ts`, rather than whenever someone remembers to copy a
|
|
940
|
+
// guard into its handler.
|
|
941
|
+
{
|
|
942
|
+
const needed = requiredStorageScope(typed.type);
|
|
943
|
+
if (needed !== null && !declaredScopeSet.has(needed)) {
|
|
944
|
+
const error = storageScopeDeniedMessage(typed.type, needed);
|
|
945
|
+
dispatchToBlock({
|
|
946
|
+
type: storageResultType(typed.type),
|
|
947
|
+
payload: storageScopeDeniedPayload(typed.type, requestId, error),
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
914
952
|
switch (typed.type) {
|
|
915
953
|
case 'REQUEST_TOKEN':
|
|
916
954
|
dispatchToBlock({
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mockHostScopes.ts — the dev host's STORAGE SCOPE GATE.
|
|
3
|
+
*
|
|
4
|
+
* THE MOTIVATING FAILURE (2026-10-01). An app was built from the documented
|
|
5
|
+
* onboarding prompt, shipped `useAppStorage()` for every save, and declared only
|
|
6
|
+
* `ai:write:budgeted` in its manifest. It passed **198 unit tests, the dev
|
|
7
|
+
* harness, `civitai app validate`, and a full submit** — then every save in
|
|
8
|
+
* production failed:
|
|
9
|
+
*
|
|
10
|
+
* "message": "storage set requires the apps:storage:write scope",
|
|
11
|
+
* "code": -32003,
|
|
12
|
+
* "data": { "code": "FORBIDDEN", "httpStatus": 403, "path": "apps.storage.set" }
|
|
13
|
+
*
|
|
14
|
+
* which the viewer saw as *"Saving failed for an unknown reason… try again"*.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 THE REASON IT GOT THAT FAR IS THIS FILE'S ABSENCE. `createMockHost` served
|
|
17
|
+
* storage unconditionally — its own options doc said so in as many words
|
|
18
|
+
* ("`APP_STORAGE_*` is answered either way (the mock host always serves storage
|
|
19
|
+
* now)") — so the one failure mode that actually ships was the only storage
|
|
20
|
+
* failure mode the mock could not produce. It modelled the per-value cap, both
|
|
21
|
+
* per-viewer budgets, the row limit and an induced transport failure, and not
|
|
22
|
+
* the scope.
|
|
23
|
+
*
|
|
24
|
+
* So this is not a new feature so much as the missing arm of an existing
|
|
25
|
+
* simulation: the dev host already models `ai:write:budgeted` (it has a flag,
|
|
26
|
+
* a consent round-trip and an un-grantable case). Storage had nothing.
|
|
27
|
+
*
|
|
28
|
+
* ---
|
|
29
|
+
*
|
|
30
|
+
* WHAT THE SERVER ACTUALLY DOES, and what is inferred here.
|
|
31
|
+
*
|
|
32
|
+
* `BLOCK_SCOPES` in `@civitai/app-sdk` states the mechanism: the storage scopes
|
|
33
|
+
* "have no OAuth bit … the server gates them by presence in the block's
|
|
34
|
+
* APPROVED SCOPE SET, not a bitmask". Presence is therefore the whole test, and
|
|
35
|
+
* it is what {@link requiredStorageScope} models.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 ONE ROW IS MEASURED; THE REST ARE INFERRED FROM THE SCOPE NAMES. The
|
|
38
|
+
* incident above is direct evidence for exactly one pair —
|
|
39
|
+
* `apps.storage.set` → `apps:storage:write` — because the server named both in
|
|
40
|
+
* its own refusal. Every other row below reads a `:read` scope onto a read op
|
|
41
|
+
* and a `:write` scope onto a write op, which is the only split the names admit
|
|
42
|
+
* but is still an inference about another service. Nothing in this repository
|
|
43
|
+
* can verify it: there is no op→scope table in `@civitai/app-sdk`, none in this
|
|
44
|
+
* package, and the server's own table is not vendored.
|
|
45
|
+
*
|
|
46
|
+
* The consequence matters because enforcement is ON by default: a row that is
|
|
47
|
+
* WRONG fails a CORRECT app's suite. If that happens, the row is the suspect —
|
|
48
|
+
* not the app. Fix the row and say what the server did instead.
|
|
49
|
+
*
|
|
50
|
+
* `SHARED_REPORT` is the row to doubt first. It is mapped to
|
|
51
|
+
* `apps:storage:shared:write` because `useSharedStorage().report()` files a row
|
|
52
|
+
* in the shared store, so it is a write by construction — but a server is also
|
|
53
|
+
* free to treat an abuse report as a moderation path outside the store's own
|
|
54
|
+
* gate, in which case this row over-gates and should be removed rather than
|
|
55
|
+
* weakened.
|
|
56
|
+
*/
|
|
57
|
+
/** Every message type this gate governs. Exported for the ledger test. */
|
|
58
|
+
export declare function gatedStorageMessages(): string[];
|
|
59
|
+
/**
|
|
60
|
+
* The scope `type` needs, or `null` when this gate does not govern it.
|
|
61
|
+
*
|
|
62
|
+
* `null` is the ordinary answer — the overwhelming majority of block→host
|
|
63
|
+
* messages are not storage.
|
|
64
|
+
*/
|
|
65
|
+
export declare function requiredStorageScope(type: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The refusal text. Modelled on the server's own prose for the one case it was
|
|
68
|
+
* observed emitting: *"storage set requires the apps:storage:write scope"*.
|
|
69
|
+
*
|
|
70
|
+
* 🔴 DO NOT LET A BLOCK BRANCH ON THIS STRING, and do not render it to a viewer.
|
|
71
|
+
* `@civitai/app-sdk`'s `appStorageErrors.ts` is explicit that host refusal prose
|
|
72
|
+
* is "not localized, not written for an end user, and free to change", and that
|
|
73
|
+
* an authorization failure deliberately classifies as `null` through
|
|
74
|
+
* `classifyAppStorageError` — which is exactly what this message does, matching
|
|
75
|
+
* production rather than inventing a classification the real host does not send.
|
|
76
|
+
* That `null` arm is also why *"please try again"* is the wrong copy for it: a
|
|
77
|
+
* missing scope is a manifest defect and no number of retries fixes it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function storageScopeDeniedMessage(type: string, scope: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* The reply payload that refuses `type`, type-correct for that reply's own
|
|
82
|
+
* contract.
|
|
83
|
+
*
|
|
84
|
+
* 🔴 EVERY STORAGE REPLY CARRIES `error?`, AND A NON-EMPTY `error` IS THE REJECT
|
|
85
|
+
* SIGNAL — `@civitai/app-sdk`'s messages module calls it "the
|
|
86
|
+
* `APP_STORAGE_GET_RESULT` value-or-error convention: consumers treat a
|
|
87
|
+
* non-empty `error` as the failure signal". So a refusal does not need a new
|
|
88
|
+
* wire field, a new constant, or an `@civitai/app-sdk` change — which also
|
|
89
|
+
* means it needs no peer-range bump, the hazard that once left 27 of 43 test
|
|
90
|
+
* files collecting zero tests in this very package.
|
|
91
|
+
*
|
|
92
|
+
* The DATA fields are still required by each reply's type, so each arm supplies
|
|
93
|
+
* an empty-but-valid one rather than omitting it: a reply that fails its own
|
|
94
|
+
* contract risks being dropped by the host-message validator, which would
|
|
95
|
+
* present as a HANG rather than a refusal.
|
|
96
|
+
*/
|
|
97
|
+
export declare function storageScopeDeniedPayload(type: string, requestId: string | undefined, error: string): Record<string, unknown>;
|
|
98
|
+
/** The reply type for a block→host storage message. Uniform across the family. */
|
|
99
|
+
export declare function storageResultType(type: string): string;
|
|
100
|
+
//# sourceMappingURL=mockHostScopes.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mockHostScopes.ts — the dev host's STORAGE SCOPE GATE.
|
|
3
|
+
*
|
|
4
|
+
* THE MOTIVATING FAILURE (2026-10-01). An app was built from the documented
|
|
5
|
+
* onboarding prompt, shipped `useAppStorage()` for every save, and declared only
|
|
6
|
+
* `ai:write:budgeted` in its manifest. It passed **198 unit tests, the dev
|
|
7
|
+
* harness, `civitai app validate`, and a full submit** — then every save in
|
|
8
|
+
* production failed:
|
|
9
|
+
*
|
|
10
|
+
* "message": "storage set requires the apps:storage:write scope",
|
|
11
|
+
* "code": -32003,
|
|
12
|
+
* "data": { "code": "FORBIDDEN", "httpStatus": 403, "path": "apps.storage.set" }
|
|
13
|
+
*
|
|
14
|
+
* which the viewer saw as *"Saving failed for an unknown reason… try again"*.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 THE REASON IT GOT THAT FAR IS THIS FILE'S ABSENCE. `createMockHost` served
|
|
17
|
+
* storage unconditionally — its own options doc said so in as many words
|
|
18
|
+
* ("`APP_STORAGE_*` is answered either way (the mock host always serves storage
|
|
19
|
+
* now)") — so the one failure mode that actually ships was the only storage
|
|
20
|
+
* failure mode the mock could not produce. It modelled the per-value cap, both
|
|
21
|
+
* per-viewer budgets, the row limit and an induced transport failure, and not
|
|
22
|
+
* the scope.
|
|
23
|
+
*
|
|
24
|
+
* So this is not a new feature so much as the missing arm of an existing
|
|
25
|
+
* simulation: the dev host already models `ai:write:budgeted` (it has a flag,
|
|
26
|
+
* a consent round-trip and an un-grantable case). Storage had nothing.
|
|
27
|
+
*
|
|
28
|
+
* ---
|
|
29
|
+
*
|
|
30
|
+
* WHAT THE SERVER ACTUALLY DOES, and what is inferred here.
|
|
31
|
+
*
|
|
32
|
+
* `BLOCK_SCOPES` in `@civitai/app-sdk` states the mechanism: the storage scopes
|
|
33
|
+
* "have no OAuth bit … the server gates them by presence in the block's
|
|
34
|
+
* APPROVED SCOPE SET, not a bitmask". Presence is therefore the whole test, and
|
|
35
|
+
* it is what {@link requiredStorageScope} models.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 ONE ROW IS MEASURED; THE REST ARE INFERRED FROM THE SCOPE NAMES. The
|
|
38
|
+
* incident above is direct evidence for exactly one pair —
|
|
39
|
+
* `apps.storage.set` → `apps:storage:write` — because the server named both in
|
|
40
|
+
* its own refusal. Every other row below reads a `:read` scope onto a read op
|
|
41
|
+
* and a `:write` scope onto a write op, which is the only split the names admit
|
|
42
|
+
* but is still an inference about another service. Nothing in this repository
|
|
43
|
+
* can verify it: there is no op→scope table in `@civitai/app-sdk`, none in this
|
|
44
|
+
* package, and the server's own table is not vendored.
|
|
45
|
+
*
|
|
46
|
+
* The consequence matters because enforcement is ON by default: a row that is
|
|
47
|
+
* WRONG fails a CORRECT app's suite. If that happens, the row is the suspect —
|
|
48
|
+
* not the app. Fix the row and say what the server did instead.
|
|
49
|
+
*
|
|
50
|
+
* `SHARED_REPORT` is the row to doubt first. It is mapped to
|
|
51
|
+
* `apps:storage:shared:write` because `useSharedStorage().report()` files a row
|
|
52
|
+
* in the shared store, so it is a write by construction — but a server is also
|
|
53
|
+
* free to treat an abuse report as a moderation path outside the store's own
|
|
54
|
+
* gate, in which case this row over-gates and should be removed rather than
|
|
55
|
+
* weakened.
|
|
56
|
+
*/
|
|
57
|
+
import { BLOCK_SCOPES } from '@civitai/app-sdk/blocks';
|
|
58
|
+
/**
|
|
59
|
+
* Block→host message type → the scope the server requires to answer it.
|
|
60
|
+
*
|
|
61
|
+
* Only storage surfaces appear here. The money path keeps its own flag
|
|
62
|
+
* (`consentGranted`) in `mockHost.ts`, deliberately: `buzzBudget` is conditional
|
|
63
|
+
* on it and `setScenario` can toggle it mid-session, so folding it in here would
|
|
64
|
+
* give one scope two sources of truth.
|
|
65
|
+
*/
|
|
66
|
+
const STORAGE_SCOPE_BY_MESSAGE = {
|
|
67
|
+
// Per-app, per-viewer KV store.
|
|
68
|
+
APP_STORAGE_GET: BLOCK_SCOPES.APPS_STORAGE_READ,
|
|
69
|
+
APP_STORAGE_LIST: BLOCK_SCOPES.APPS_STORAGE_READ,
|
|
70
|
+
APP_STORAGE_QUOTA: BLOCK_SCOPES.APPS_STORAGE_READ,
|
|
71
|
+
APP_STORAGE_SET: BLOCK_SCOPES.APPS_STORAGE_WRITE, // ← the MEASURED row
|
|
72
|
+
APP_STORAGE_DELETE: BLOCK_SCOPES.APPS_STORAGE_WRITE,
|
|
73
|
+
// Shared, cross-user store.
|
|
74
|
+
SHARED_LIST: BLOCK_SCOPES.APPS_STORAGE_SHARED_READ,
|
|
75
|
+
SHARED_GET: BLOCK_SCOPES.APPS_STORAGE_SHARED_READ,
|
|
76
|
+
SHARED_GET_COUNT: BLOCK_SCOPES.APPS_STORAGE_SHARED_READ,
|
|
77
|
+
SHARED_GET_COUNTS: BLOCK_SCOPES.APPS_STORAGE_SHARED_READ,
|
|
78
|
+
SHARED_APPEND: BLOCK_SCOPES.APPS_STORAGE_SHARED_WRITE,
|
|
79
|
+
SHARED_VOTE: BLOCK_SCOPES.APPS_STORAGE_SHARED_WRITE,
|
|
80
|
+
SHARED_UNVOTE: BLOCK_SCOPES.APPS_STORAGE_SHARED_WRITE,
|
|
81
|
+
SHARED_WITHDRAW: BLOCK_SCOPES.APPS_STORAGE_SHARED_WRITE,
|
|
82
|
+
SHARED_UPDATE: BLOCK_SCOPES.APPS_STORAGE_SHARED_WRITE,
|
|
83
|
+
SHARED_REPORT: BLOCK_SCOPES.APPS_STORAGE_SHARED_WRITE, // ← doubt this one first
|
|
84
|
+
};
|
|
85
|
+
/** Every message type this gate governs. Exported for the ledger test. */
|
|
86
|
+
export function gatedStorageMessages() {
|
|
87
|
+
return Object.keys(STORAGE_SCOPE_BY_MESSAGE).sort();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The scope `type` needs, or `null` when this gate does not govern it.
|
|
91
|
+
*
|
|
92
|
+
* `null` is the ordinary answer — the overwhelming majority of block→host
|
|
93
|
+
* messages are not storage.
|
|
94
|
+
*/
|
|
95
|
+
export function requiredStorageScope(type) {
|
|
96
|
+
return STORAGE_SCOPE_BY_MESSAGE[type] ?? null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The refusal text. Modelled on the server's own prose for the one case it was
|
|
100
|
+
* observed emitting: *"storage set requires the apps:storage:write scope"*.
|
|
101
|
+
*
|
|
102
|
+
* 🔴 DO NOT LET A BLOCK BRANCH ON THIS STRING, and do not render it to a viewer.
|
|
103
|
+
* `@civitai/app-sdk`'s `appStorageErrors.ts` is explicit that host refusal prose
|
|
104
|
+
* is "not localized, not written for an end user, and free to change", and that
|
|
105
|
+
* an authorization failure deliberately classifies as `null` through
|
|
106
|
+
* `classifyAppStorageError` — which is exactly what this message does, matching
|
|
107
|
+
* production rather than inventing a classification the real host does not send.
|
|
108
|
+
* That `null` arm is also why *"please try again"* is the wrong copy for it: a
|
|
109
|
+
* missing scope is a manifest defect and no number of retries fixes it.
|
|
110
|
+
*/
|
|
111
|
+
export function storageScopeDeniedMessage(type, scope) {
|
|
112
|
+
return `${verbFor(type)} requires the ${scope} scope`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The server names the OPERATION, not the message type, in its refusal
|
|
116
|
+
* (`storage set …`, on path `apps.storage.set`). Mirror that so a developer
|
|
117
|
+
* grepping their logs for the production string finds the dev one too.
|
|
118
|
+
*/
|
|
119
|
+
function verbFor(type) {
|
|
120
|
+
const shared = type.startsWith('SHARED_');
|
|
121
|
+
const op = type
|
|
122
|
+
.replace(/^APP_STORAGE_/, '')
|
|
123
|
+
.replace(/^SHARED_/, '')
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/_/g, ' ');
|
|
126
|
+
return shared ? `shared storage ${op}` : `storage ${op}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The reply payload that refuses `type`, type-correct for that reply's own
|
|
130
|
+
* contract.
|
|
131
|
+
*
|
|
132
|
+
* 🔴 EVERY STORAGE REPLY CARRIES `error?`, AND A NON-EMPTY `error` IS THE REJECT
|
|
133
|
+
* SIGNAL — `@civitai/app-sdk`'s messages module calls it "the
|
|
134
|
+
* `APP_STORAGE_GET_RESULT` value-or-error convention: consumers treat a
|
|
135
|
+
* non-empty `error` as the failure signal". So a refusal does not need a new
|
|
136
|
+
* wire field, a new constant, or an `@civitai/app-sdk` change — which also
|
|
137
|
+
* means it needs no peer-range bump, the hazard that once left 27 of 43 test
|
|
138
|
+
* files collecting zero tests in this very package.
|
|
139
|
+
*
|
|
140
|
+
* The DATA fields are still required by each reply's type, so each arm supplies
|
|
141
|
+
* an empty-but-valid one rather than omitting it: a reply that fails its own
|
|
142
|
+
* contract risks being dropped by the host-message validator, which would
|
|
143
|
+
* present as a HANG rather than a refusal.
|
|
144
|
+
*/
|
|
145
|
+
export function storageScopeDeniedPayload(type, requestId, error) {
|
|
146
|
+
switch (type) {
|
|
147
|
+
// ---- reads: data field + error ----
|
|
148
|
+
case 'APP_STORAGE_GET':
|
|
149
|
+
return { requestId, value: null, error };
|
|
150
|
+
case 'APP_STORAGE_LIST':
|
|
151
|
+
return { requestId, keys: [], error };
|
|
152
|
+
case 'APP_STORAGE_QUOTA':
|
|
153
|
+
return { requestId, usedBytes: 0, rowCount: 0, limitBytes: 0, limitRows: 0, error };
|
|
154
|
+
case 'SHARED_LIST':
|
|
155
|
+
return { requestId, items: [], error };
|
|
156
|
+
case 'SHARED_GET':
|
|
157
|
+
return { requestId, item: null, error };
|
|
158
|
+
case 'SHARED_GET_COUNT':
|
|
159
|
+
return { requestId, count: 0, error };
|
|
160
|
+
case 'SHARED_GET_COUNTS':
|
|
161
|
+
return { requestId, counts: {}, error };
|
|
162
|
+
// ---- writes: ok:false + error ----
|
|
163
|
+
default:
|
|
164
|
+
return { requestId, ok: false, error };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** The reply type for a block→host storage message. Uniform across the family. */
|
|
168
|
+
export function storageResultType(type) {
|
|
169
|
+
return `${type}_RESULT`;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=mockHostScopes.js.map
|