@civitai/app-sdk 0.51.1 → 0.51.2
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 +1 -1
- package/dist/blocks/browsingLevel.d.ts +52 -11
- package/dist/blocks/browsingLevel.js +30 -7
- package/dist/blocks/messages.d.ts +25 -7
- package/dist/blocks/scopes.d.ts +1 -1
- package/dist/blocks/scopes.js +1 -1
- package/dist/safe-storage/index.d.ts +19 -2
- package/dist/safe-storage/index.js +19 -2
- package/package.json +1 -1
- package/schemas/app-block/v1.json +1 -1
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ interface BlockInitPayload {
|
|
|
134
134
|
|
|
135
135
|
| Export | What |
|
|
136
136
|
|---|---|
|
|
137
|
-
| `BLOCK_SCOPES` / `BLOCK_SCOPE_PATTERN` |
|
|
137
|
+
| `BLOCK_SCOPES` / `BLOCK_SCOPE_PATTERN` | Every known block scope string (the authoritative enum the canonical schema validates `scopes` against) + the `domain:verb:target` format-helper regex. Read the current set from `BLOCK_SCOPES` itself — scopes are added and retired, so no count is quoted here. A scope is valid only if it's a member of `BLOCK_SCOPES`, matching the [canonical schema](https://civitai.com/schemas/app-block/v1.json). |
|
|
138
138
|
| `isMessage(data, type)` | Discriminator-only message narrowing (see above). |
|
|
139
139
|
| `isModelSlotContext(ctx)` / `isPageSlotContext(ctx)` | Runtime narrowing for the `slotId`-discriminated `BlockContext` union. Real checks on a value that crossed a `postMessage` boundary — they verify every field they assert, not just `slotId`. |
|
|
140
140
|
| `isSignedIn(viewer)` | **The sign-in gate.** `isSignedIn(useBlockContext().viewer)` — do not open-code it as `viewer !== null` or `viewer?.signedIn === true`. Which of those is correct has already changed once with the host contract, and this is the one place it is decided. It reads neither `viewer.id` nor `viewer.username` (both `@deprecated`), so nothing written through it changes when those are removed. Need the identity rather than the presence? `useViewer()` — scope-gated and audited per call. |
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The host (civitai/civitai) projects an authoritative `maxBrowsingLevel`
|
|
5
5
|
* BITMASK into `BLOCK_INIT` — the max NSFW levels the surrounding color-domain
|
|
6
|
-
* allows (computed server-side from `domainBrowsingCeiling(color)`)
|
|
7
|
-
*
|
|
8
|
-
* affordances
|
|
6
|
+
* allows (computed server-side from `domainBrowsingCeiling(color)`) — and,
|
|
7
|
+
* alongside it, the viewer-narrowed `effectiveBrowsingLevel`. A block decides
|
|
8
|
+
* whether to surface mature affordances from the PAIR, via
|
|
9
|
+
* {@link effectiveBrowsingCeiling}; `@civitai/blocks-react`'s
|
|
10
|
+
* `useDomainMaturity()` is the React wrapper over exactly that. The domain
|
|
11
|
+
* ceiling alone is not the answer — see {@link ColorDomain}.
|
|
9
12
|
*
|
|
10
13
|
* The per-level bit VALUES below mirror civitai/civitai's server `NsfwLevel`
|
|
11
14
|
* enum. They are STABLE wire values (a level's bit never changes), so it is
|
|
@@ -44,7 +47,7 @@ export declare const SFW_LEVELS: number;
|
|
|
44
47
|
*/
|
|
45
48
|
export declare const NSFW_LEVELS: number;
|
|
46
49
|
/**
|
|
47
|
-
* True when the
|
|
50
|
+
* True when the given browsing-level ceiling permits NO NSFW content.
|
|
48
51
|
*
|
|
49
52
|
* Derived purely from the BITMASK: SFW ⇔ the ceiling has no NSFW bits set.
|
|
50
53
|
*
|
|
@@ -54,7 +57,17 @@ export declare const NSFW_LEVELS: number;
|
|
|
54
57
|
* block must therefore treat "unknown" as SFW and hide mature affordances
|
|
55
58
|
* until proven otherwise.
|
|
56
59
|
*
|
|
57
|
-
*
|
|
60
|
+
* ⚠ A NEGATIVE ceiling is none of those three and fails OPEN:
|
|
61
|
+
* `isSfwCeiling(-1)` is `false` and `isLevelAllowed(XXX, -1)` is `true`, because
|
|
62
|
+
* two's complement sets every bit. {@link effectiveBrowsingCeiling} guards a
|
|
63
|
+
* negative VIEWER level but not a negative DOMAIN ceiling, so the recipe does
|
|
64
|
+
* not neutralise it either. Stated rather than fixed: adding the guard is a
|
|
65
|
+
* behaviour change and belongs in its own release.
|
|
66
|
+
*
|
|
67
|
+
* @param maxBrowsingLevel the ceiling to test — WHICHEVER you pass. Named for
|
|
68
|
+
* the domain mask because that was the only ceiling when this shipped; when
|
|
69
|
+
* gating for a viewer pass {@link effectiveBrowsingCeiling}'s result instead,
|
|
70
|
+
* or this answers "is the DOMAIN SFW?" and not "may I show THIS viewer".
|
|
58
71
|
* @example
|
|
59
72
|
* isSfwCeiling(BrowsingLevel.PG | BrowsingLevel.PG13); // true (green/blue)
|
|
60
73
|
* isSfwCeiling(BrowsingLevel.PG | BrowsingLevel.X); // false (mature)
|
|
@@ -62,14 +75,24 @@ export declare const NSFW_LEVELS: number;
|
|
|
62
75
|
*/
|
|
63
76
|
export declare function isSfwCeiling(maxBrowsingLevel?: number | null): boolean;
|
|
64
77
|
/**
|
|
65
|
-
* True when a specific browsing `level` bit is permitted by the
|
|
78
|
+
* True when a specific browsing `level` bit is permitted by the given ceiling.
|
|
66
79
|
*
|
|
67
80
|
* **Fail-closed.** A missing / null / non-finite ceiling permits ONLY SFW
|
|
68
81
|
* levels (PG / PG13) — same fail-closed posture as {@link isSfwCeiling}. A
|
|
69
82
|
* non-finite / non-positive `level` returns `false`.
|
|
70
83
|
*
|
|
84
|
+
* ⚠ A NEGATIVE ceiling is none of those three and fails OPEN:
|
|
85
|
+
* `isSfwCeiling(-1)` is `false` and `isLevelAllowed(XXX, -1)` is `true`, because
|
|
86
|
+
* two's complement sets every bit. {@link effectiveBrowsingCeiling} guards a
|
|
87
|
+
* negative VIEWER level but not a negative DOMAIN ceiling, so the recipe does
|
|
88
|
+
* not neutralise it either. Stated rather than fixed: adding the guard is a
|
|
89
|
+
* behaviour change and belongs in its own release.
|
|
90
|
+
*
|
|
71
91
|
* @param level a single `BrowsingLevel` bit (e.g. `BrowsingLevel.R`).
|
|
72
|
-
* @param maxBrowsingLevel the
|
|
92
|
+
* @param maxBrowsingLevel the ceiling to test — WHICHEVER you pass. Named for
|
|
93
|
+
* the domain mask because that was the only ceiling when this shipped; when
|
|
94
|
+
* gating for a viewer pass {@link effectiveBrowsingCeiling}'s result instead,
|
|
95
|
+
* or this answers what the DOMAIN permits and not what THIS viewer may see.
|
|
73
96
|
* @example
|
|
74
97
|
* isLevelAllowed(BrowsingLevel.R, BrowsingLevel.PG | BrowsingLevel.PG13); // false
|
|
75
98
|
* isLevelAllowed(BrowsingLevel.PG13, undefined); // true
|
|
@@ -126,10 +149,28 @@ export declare function isLevelAllowed(level: number, maxBrowsingLevel?: number
|
|
|
126
149
|
*/
|
|
127
150
|
export declare function effectiveBrowsingCeiling(maxBrowsingLevel?: number | null, effectiveBrowsingLevel?: number | null): number;
|
|
128
151
|
/**
|
|
129
|
-
* The color-domain a block is rendered inside, as projected by the host.
|
|
130
|
-
* SFW policy is
|
|
131
|
-
*
|
|
132
|
-
*
|
|
152
|
+
* The color-domain a block is rendered inside, as projected by the host.
|
|
153
|
+
* Informational ONLY — the SFW policy is server-side, and this string is not
|
|
154
|
+
* it. `null` / absent means the host did not project a domain (treat as
|
|
155
|
+
* unknown ⇒ fail-closed SFW).
|
|
156
|
+
*
|
|
157
|
+
* 🔴 Never gate on this string, and never on `maxBrowsingLevel` alone. Resolve
|
|
158
|
+
* the ceiling with {@link effectiveBrowsingCeiling}, then test it with
|
|
159
|
+
* {@link isSfwCeiling} or {@link isLevelAllowed}:
|
|
160
|
+
*
|
|
161
|
+
* ```ts
|
|
162
|
+
* const eff = effectiveBrowsingCeiling(maxBrowsingLevel, effectiveBrowsingLevel);
|
|
163
|
+
* // 🔴 OPPOSITE POLARITIES — do not copy these two lines as a uniform pair.
|
|
164
|
+
* if (isSfwCeiling(eff)) hideMatureAffordances(); // true = SFW ⇒ HIDE
|
|
165
|
+
* if (isLevelAllowed(BrowsingLevel.R, eff)) showR(); // true = allowed ⇒ SHOW
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* This docblock used to say *"use `isSfwCeiling` on the accompanying
|
|
169
|
+
* `maxBrowsingLevel` mask instead"*. The predicate was never the problem — both
|
|
170
|
+
* predicates take WHATEVER CEILING YOU PASS — the problem was passing the
|
|
171
|
+
* domain one, which is identical for every viewer on `civitai.red` including
|
|
172
|
+
* one whose own NSFW setting is off, so it cannot answer "may I show THIS
|
|
173
|
+
* viewer mature content".
|
|
133
174
|
*/
|
|
134
175
|
export type ColorDomain = 'green' | 'blue' | 'red';
|
|
135
176
|
//# sourceMappingURL=browsingLevel.d.ts.map
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The host (civitai/civitai) projects an authoritative `maxBrowsingLevel`
|
|
5
5
|
* BITMASK into `BLOCK_INIT` — the max NSFW levels the surrounding color-domain
|
|
6
|
-
* allows (computed server-side from `domainBrowsingCeiling(color)`)
|
|
7
|
-
*
|
|
8
|
-
* affordances
|
|
6
|
+
* allows (computed server-side from `domainBrowsingCeiling(color)`) — and,
|
|
7
|
+
* alongside it, the viewer-narrowed `effectiveBrowsingLevel`. A block decides
|
|
8
|
+
* whether to surface mature affordances from the PAIR, via
|
|
9
|
+
* {@link effectiveBrowsingCeiling}; `@civitai/blocks-react`'s
|
|
10
|
+
* `useDomainMaturity()` is the React wrapper over exactly that. The domain
|
|
11
|
+
* ceiling alone is not the answer — see {@link ColorDomain}.
|
|
9
12
|
*
|
|
10
13
|
* The per-level bit VALUES below mirror civitai/civitai's server `NsfwLevel`
|
|
11
14
|
* enum. They are STABLE wire values (a level's bit never changes), so it is
|
|
@@ -42,7 +45,7 @@ export const SFW_LEVELS = BrowsingLevel.PG | BrowsingLevel.PG13;
|
|
|
42
45
|
*/
|
|
43
46
|
export const NSFW_LEVELS = BrowsingLevel.R | BrowsingLevel.X | BrowsingLevel.XXX;
|
|
44
47
|
/**
|
|
45
|
-
* True when the
|
|
48
|
+
* True when the given browsing-level ceiling permits NO NSFW content.
|
|
46
49
|
*
|
|
47
50
|
* Derived purely from the BITMASK: SFW ⇔ the ceiling has no NSFW bits set.
|
|
48
51
|
*
|
|
@@ -52,7 +55,17 @@ export const NSFW_LEVELS = BrowsingLevel.R | BrowsingLevel.X | BrowsingLevel.XXX
|
|
|
52
55
|
* block must therefore treat "unknown" as SFW and hide mature affordances
|
|
53
56
|
* until proven otherwise.
|
|
54
57
|
*
|
|
55
|
-
*
|
|
58
|
+
* ⚠ A NEGATIVE ceiling is none of those three and fails OPEN:
|
|
59
|
+
* `isSfwCeiling(-1)` is `false` and `isLevelAllowed(XXX, -1)` is `true`, because
|
|
60
|
+
* two's complement sets every bit. {@link effectiveBrowsingCeiling} guards a
|
|
61
|
+
* negative VIEWER level but not a negative DOMAIN ceiling, so the recipe does
|
|
62
|
+
* not neutralise it either. Stated rather than fixed: adding the guard is a
|
|
63
|
+
* behaviour change and belongs in its own release.
|
|
64
|
+
*
|
|
65
|
+
* @param maxBrowsingLevel the ceiling to test — WHICHEVER you pass. Named for
|
|
66
|
+
* the domain mask because that was the only ceiling when this shipped; when
|
|
67
|
+
* gating for a viewer pass {@link effectiveBrowsingCeiling}'s result instead,
|
|
68
|
+
* or this answers "is the DOMAIN SFW?" and not "may I show THIS viewer".
|
|
56
69
|
* @example
|
|
57
70
|
* isSfwCeiling(BrowsingLevel.PG | BrowsingLevel.PG13); // true (green/blue)
|
|
58
71
|
* isSfwCeiling(BrowsingLevel.PG | BrowsingLevel.X); // false (mature)
|
|
@@ -65,14 +78,24 @@ export function isSfwCeiling(maxBrowsingLevel) {
|
|
|
65
78
|
return (maxBrowsingLevel & NSFW_LEVELS) === 0;
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
|
-
* True when a specific browsing `level` bit is permitted by the
|
|
81
|
+
* True when a specific browsing `level` bit is permitted by the given ceiling.
|
|
69
82
|
*
|
|
70
83
|
* **Fail-closed.** A missing / null / non-finite ceiling permits ONLY SFW
|
|
71
84
|
* levels (PG / PG13) — same fail-closed posture as {@link isSfwCeiling}. A
|
|
72
85
|
* non-finite / non-positive `level` returns `false`.
|
|
73
86
|
*
|
|
87
|
+
* ⚠ A NEGATIVE ceiling is none of those three and fails OPEN:
|
|
88
|
+
* `isSfwCeiling(-1)` is `false` and `isLevelAllowed(XXX, -1)` is `true`, because
|
|
89
|
+
* two's complement sets every bit. {@link effectiveBrowsingCeiling} guards a
|
|
90
|
+
* negative VIEWER level but not a negative DOMAIN ceiling, so the recipe does
|
|
91
|
+
* not neutralise it either. Stated rather than fixed: adding the guard is a
|
|
92
|
+
* behaviour change and belongs in its own release.
|
|
93
|
+
*
|
|
74
94
|
* @param level a single `BrowsingLevel` bit (e.g. `BrowsingLevel.R`).
|
|
75
|
-
* @param maxBrowsingLevel the
|
|
95
|
+
* @param maxBrowsingLevel the ceiling to test — WHICHEVER you pass. Named for
|
|
96
|
+
* the domain mask because that was the only ceiling when this shipped; when
|
|
97
|
+
* gating for a viewer pass {@link effectiveBrowsingCeiling}'s result instead,
|
|
98
|
+
* or this answers what the DOMAIN permits and not what THIS viewer may see.
|
|
76
99
|
* @example
|
|
77
100
|
* isLevelAllowed(BrowsingLevel.R, BrowsingLevel.PG | BrowsingLevel.PG13); // false
|
|
78
101
|
* isLevelAllowed(BrowsingLevel.PG13, undefined); // true
|
|
@@ -150,8 +150,21 @@ export interface BlockInitPayload {
|
|
|
150
150
|
/**
|
|
151
151
|
* The color-domain the block is rendered inside (`green` | `blue` | `red`),
|
|
152
152
|
* or `null` when the host did not resolve one. Informational ONLY — the SFW
|
|
153
|
-
* policy is server-side;
|
|
154
|
-
*
|
|
153
|
+
* policy is server-side; never gate on this string. Resolve the ceiling
|
|
154
|
+
* first, then test it — in any runtime:
|
|
155
|
+
*
|
|
156
|
+
* ```ts
|
|
157
|
+
* const eff = effectiveBrowsingCeiling(maxBrowsingLevel, effectiveBrowsingLevel);
|
|
158
|
+
* // 🔴 OPPOSITE POLARITIES — do not copy these two lines as a uniform pair.
|
|
159
|
+
* if (isSfwCeiling(eff)) hideMatureAffordances(); // true = SFW ⇒ HIDE
|
|
160
|
+
* if (isLevelAllowed(BrowsingLevel.R, eff)) showR(); // true = allowed ⇒ SHOW
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* 🔴 `effectiveBrowsingCeiling` returns a BITMASK, not a boolean — an SFW
|
|
164
|
+
* ceiling is `3`, which is truthy, so branching on it directly shows mature
|
|
165
|
+
* content to a viewer who may not see it. It needs one of the two predicates
|
|
166
|
+
* above. In React, `@civitai/blocks-react`'s `useDomainMaturity()` returns
|
|
167
|
+
* `isSfw` / `isLevelAllowed` already composed this way.
|
|
155
168
|
*
|
|
156
169
|
* Sent by civitai/civitai PR #2670. A host that predates it omits this field
|
|
157
170
|
* (reads `undefined`).
|
|
@@ -161,8 +174,9 @@ export interface BlockInitPayload {
|
|
|
161
174
|
* Authoritative browsing-level BITMASK = the max NSFW levels the domain
|
|
162
175
|
* allows, computed server-side from `domainBrowsingCeiling(color)` (green/
|
|
163
176
|
* blue → SFW, red → all). Bits mirror the server `NsfwLevel` (see
|
|
164
|
-
* `browsingLevel.ts`).
|
|
165
|
-
*
|
|
177
|
+
* `browsingLevel.ts`). `isSfwCeiling(maxBrowsingLevel)` answers "is this
|
|
178
|
+
* DOMAIN SFW?" — not "may I show THIS viewer mature content"; for that, see
|
|
179
|
+
* the warning below and {@link effectiveBrowsingLevel}.
|
|
166
180
|
*
|
|
167
181
|
* Sent by civitai/civitai PR #2670. A host that predates it omits this field
|
|
168
182
|
* (reads `undefined`); the SDK fail-closes to SFW when it is absent.
|
|
@@ -193,9 +207,13 @@ export interface BlockInitPayload {
|
|
|
193
207
|
* WIDER than the domain permits and is not permission for anything.
|
|
194
208
|
*
|
|
195
209
|
* ADDITIVE + OPTIONAL. A host that predates it omits the field, in which case
|
|
196
|
-
*
|
|
197
|
-
* behaviour that host already had.
|
|
198
|
-
*
|
|
210
|
+
* the ceiling falls back to {@link maxBrowsingLevel} — i.e. exactly the
|
|
211
|
+
* behaviour that host already had. Do not read this field directly: pass it
|
|
212
|
+
* through `effectiveBrowsingCeiling`, which applies that fallback and the
|
|
213
|
+
* fail-closed defaults for you. `@civitai/blocks-react`'s
|
|
214
|
+
* `useDomainMaturity()` is the React wrapper over the same thing — it is one
|
|
215
|
+
* way to get this right, not the only one, since this module is
|
|
216
|
+
* runtime-agnostic and two of the starters ship without React.
|
|
199
217
|
*/
|
|
200
218
|
effectiveBrowsingLevel?: number;
|
|
201
219
|
}
|
package/dist/blocks/scopes.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type BlockScope = (typeof BLOCK_SCOPES)[BlockScopeKey];
|
|
|
33
33
|
*
|
|
34
34
|
* NOTE: this regex is **not** the authoritative validity contract. The
|
|
35
35
|
* canonical manifest schema (https://civitai.com/schemas/app-block/v1.json)
|
|
36
|
-
* validates `scopes` by MEMBERSHIP in a fixed enum — i.e. the
|
|
36
|
+
* validates `scopes` by MEMBERSHIP in a fixed enum — i.e. exactly the values in
|
|
37
37
|
* {@link BLOCK_SCOPES}. `defineBlock` therefore gates on membership in
|
|
38
38
|
* `BLOCK_SCOPES`; this pattern is kept only as a FORMAT HEURISTIC to give a
|
|
39
39
|
* pointed error message (e.g. distinguishing a malformed/PascalCase scope from
|
package/dist/blocks/scopes.js
CHANGED
|
@@ -55,7 +55,7 @@ export const BLOCK_SCOPES = {
|
|
|
55
55
|
*
|
|
56
56
|
* NOTE: this regex is **not** the authoritative validity contract. The
|
|
57
57
|
* canonical manifest schema (https://civitai.com/schemas/app-block/v1.json)
|
|
58
|
-
* validates `scopes` by MEMBERSHIP in a fixed enum — i.e. the
|
|
58
|
+
* validates `scopes` by MEMBERSHIP in a fixed enum — i.e. exactly the values in
|
|
59
59
|
* {@link BLOCK_SCOPES}. `defineBlock` therefore gates on membership in
|
|
60
60
|
* `BLOCK_SCOPES`; this pattern is kept only as a FORMAT HEURISTIC to give a
|
|
61
61
|
* pointed error message (e.g. distinguishing a malformed/PascalCase scope from
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@civitai/app-sdk/safe-storage` — survive an opaque-origin sandbox.
|
|
3
3
|
*
|
|
4
|
+
* 🔴 `@civitai/sdk`'s `src/safe-storage/index.ts` is a deliberate independent
|
|
5
|
+
* COPY of this file (the successor must not depend on its predecessor). The two
|
|
6
|
+
* bodies are held byte-identical modulo comments by
|
|
7
|
+
* `tests/guards/safe-storage-copy-parity.test.mjs`, so a fix here goes red
|
|
8
|
+
* until it is applied there too.
|
|
9
|
+
*
|
|
4
10
|
* Civitai Apps run in an iframe sandboxed as `allow-scripts allow-forms`,
|
|
5
11
|
* deliberately WITHOUT `allow-same-origin`. The document therefore has an
|
|
6
12
|
* **opaque origin**, and there is no origin to key web storage against, so the
|
|
@@ -54,8 +60,19 @@ export declare function createMemoryStorage(): Storage;
|
|
|
54
60
|
* with an in-memory `Storage`, so third-party code that touches it unguarded
|
|
55
61
|
* cannot throw.
|
|
56
62
|
*
|
|
57
|
-
* - **No-op where storage works.** A healthy `Storage` is
|
|
58
|
-
* its contents are
|
|
63
|
+
* - **No-op where storage works — but not a no-TOUCH.** A healthy `Storage` is
|
|
64
|
+
* never replaced and its contents are unchanged, but it *is* written to:
|
|
65
|
+
* classifying it means a real round trip, so each of `localStorage` and
|
|
66
|
+
* `sessionStorage` gets `setItem('__civitai_app_sdk_storage_probe__', '1')`
|
|
67
|
+
* immediately followed by `removeItem`. MEASURED against the built artifact
|
|
68
|
+
* with instrumented healthy stores: exactly those four operations, the store
|
|
69
|
+
* objects not replaced, contents identical before and after. Per the Web
|
|
70
|
+
* Storage spec every one of those writes queues a `storage` event in the
|
|
71
|
+
* *other* documents that share the store, so at a REAL origin a cross-tab
|
|
72
|
+
* listener that does not filter by key sees spurious events (the two
|
|
73
|
+
* `localStorage` writes are the ones another tab receives). At the opaque
|
|
74
|
+
* origin this module exists for there is no other same-origin document, so
|
|
75
|
+
* there is nothing to receive them.
|
|
59
76
|
* - **No-op where storage is absent** (Node/SSR/workers). Nothing is invented.
|
|
60
77
|
* - **Never loses readable data.** When the old store reads but refuses writes
|
|
61
78
|
* (full / disabled), its entries are copied into the fallback first, so the
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@civitai/app-sdk/safe-storage` — survive an opaque-origin sandbox.
|
|
3
3
|
*
|
|
4
|
+
* 🔴 `@civitai/sdk`'s `src/safe-storage/index.ts` is a deliberate independent
|
|
5
|
+
* COPY of this file (the successor must not depend on its predecessor). The two
|
|
6
|
+
* bodies are held byte-identical modulo comments by
|
|
7
|
+
* `tests/guards/safe-storage-copy-parity.test.mjs`, so a fix here goes red
|
|
8
|
+
* until it is applied there too.
|
|
9
|
+
*
|
|
4
10
|
* Civitai Apps run in an iframe sandboxed as `allow-scripts allow-forms`,
|
|
5
11
|
* deliberately WITHOUT `allow-same-origin`. The document therefore has an
|
|
6
12
|
* **opaque origin**, and there is no origin to key web storage against, so the
|
|
@@ -272,8 +278,19 @@ function seedFrom(existing, into) {
|
|
|
272
278
|
* with an in-memory `Storage`, so third-party code that touches it unguarded
|
|
273
279
|
* cannot throw.
|
|
274
280
|
*
|
|
275
|
-
* - **No-op where storage works.** A healthy `Storage` is
|
|
276
|
-
* its contents are
|
|
281
|
+
* - **No-op where storage works — but not a no-TOUCH.** A healthy `Storage` is
|
|
282
|
+
* never replaced and its contents are unchanged, but it *is* written to:
|
|
283
|
+
* classifying it means a real round trip, so each of `localStorage` and
|
|
284
|
+
* `sessionStorage` gets `setItem('__civitai_app_sdk_storage_probe__', '1')`
|
|
285
|
+
* immediately followed by `removeItem`. MEASURED against the built artifact
|
|
286
|
+
* with instrumented healthy stores: exactly those four operations, the store
|
|
287
|
+
* objects not replaced, contents identical before and after. Per the Web
|
|
288
|
+
* Storage spec every one of those writes queues a `storage` event in the
|
|
289
|
+
* *other* documents that share the store, so at a REAL origin a cross-tab
|
|
290
|
+
* listener that does not filter by key sees spurious events (the two
|
|
291
|
+
* `localStorage` writes are the ones another tab receives). At the opaque
|
|
292
|
+
* origin this module exists for there is no other same-origin document, so
|
|
293
|
+
* there is nothing to receive them.
|
|
277
294
|
* - **No-op where storage is absent** (Node/SSR/workers). Nothing is invented.
|
|
278
295
|
* - **Never loses readable data.** When the old store reads but refuses writes
|
|
279
296
|
* (full / disabled), its entries are copied into the fallback first, so the
|
package/package.json
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"auth": {
|
|
74
74
|
"type": "string",
|
|
75
|
-
"description": "Which credential the host hands the block: \"block-token\" (default when omitted) is the block-scoped JWT; \"oauth\"
|
|
75
|
+
"description": "Which credential the host hands the block: \"block-token\" (default when omitted) is the block-scoped JWT; \"oauth\" asks for an opaque OAuth access token for the block's own OauthClient, which ordinary /api/v1 routes accept as a viewer bearer. Minting it is gated on a server flag that is not generally enabled, and also requires a signed-in viewer and a declared \"user:read:self\" scope; when any of those is unmet the host hands back a block JWT instead of failing, so branch on the token kind the host reports rather than on this field. An OAuth token is refused by the postMessage bridge procedures, which verify a block JWT only, and declaring \"oauth\" alongside any \"apps:storage:*\" scope is refused at submit time.",
|
|
76
76
|
"enum": ["block-token", "oauth"]
|
|
77
77
|
},
|
|
78
78
|
"trustTier": {
|