@civitai/app-sdk 0.47.0 → 0.49.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 +76 -32
- package/dist/blocks/appStorageErrors.d.ts +390 -0
- package/dist/blocks/appStorageErrors.d.ts.map +1 -0
- package/dist/blocks/appStorageErrors.js +429 -0
- package/dist/blocks/appStorageErrors.js.map +1 -0
- package/dist/blocks/index.d.ts +52 -7
- package/dist/blocks/index.d.ts.map +1 -1
- package/dist/blocks/index.js +50 -5
- package/dist/blocks/index.js.map +1 -1
- package/dist/blocks/manifestError.d.ts +20 -0
- package/dist/blocks/manifestError.d.ts.map +1 -0
- package/dist/blocks/manifestError.js +22 -0
- package/dist/blocks/manifestError.js.map +1 -0
- package/dist/blocks/messages.d.ts.map +1 -1
- package/dist/blocks/messages.js.map +1 -1
- package/dist/blocks/scopes.d.ts +7 -3
- package/dist/blocks/scopes.d.ts.map +1 -1
- package/dist/blocks/scopes.js +7 -3
- package/dist/blocks/scopes.js.map +1 -1
- package/dist/blocks/types.d.ts +110 -22
- package/dist/blocks/types.d.ts.map +1 -1
- package/dist/manifest/defineBlock.d.ts +145 -0
- package/dist/manifest/defineBlock.d.ts.map +1 -0
- package/dist/manifest/defineBlock.js +390 -0
- package/dist/manifest/defineBlock.js.map +1 -0
- package/dist/manifest/index.d.ts +19 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +17 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +61 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +42 -2
- package/dist/blocks/defineBlock.d.ts +0 -53
- package/dist/blocks/defineBlock.d.ts.map +0 -1
- package/dist/blocks/defineBlock.js +0 -338
- package/dist/blocks/defineBlock.js.map +0 -1
package/README.md
CHANGED
|
@@ -19,12 +19,19 @@ pnpm add @civitai/app-sdk
|
|
|
19
19
|
| `cookies/*` — `sealCookie`, `unsealCookie`, `buildSetCookieHeader`, `readCookie` | AES-256-GCM authenticated cookie crypto. Use to seal a session blob (refresh token, expiry, scope) into an `httpOnly` cookie with zero external session store. |
|
|
20
20
|
| `orchestrator/*` — `createOrchestratorClient`, `estimateWorkflow`, `submitWorkflow`, `getWorkflow`, `pollWorkflow`, `buildTextToImageBody`, `buildImageGenBody`, `buildWorkflowBody`, `WORKFLOW_STEP_TYPES`, `IMAGE_GEN_ENGINES`, `isTerminal`, `extractImageUrls`, `OrchestratorError`, `WorkflowSnapshot`, `GenerateInput`, `ImageGenInput`, `WorkflowStepType`, `ImageGenEngine`, `DEFAULT_MODEL_AIR` | Orchestrator workflow glue — types, body builders, raw HTTP, and long-poll helper. Client + server safe (fetch-only). `estimateWorkflow` calls `?whatif=true` to preview Buzz cost without spending. `pollWorkflow` long-polls to terminal status. `WORKFLOW_STEP_TYPES` is the catalog of every step `$type` the orchestrator accepts. |
|
|
21
21
|
| `orchestrator/steps` — `WorkflowStepTemplates`, `WorkflowStepTemplateFor`, `WorkflowStepInputFor`, `AnyWorkflowStepTemplate`, `TypedWorkflowTemplate` | **Type-only** subpath (0 runtime bytes) keying the orchestrator's generated workflow-step shapes from `@civitai/client` by wire `$type`, so apps compose real step bodies against types that track the spec instead of hand-maintained copies. Requires the optional peer `@civitai/client@beta`. Type surface only — it grants no submit permission; see "Typed step shapes" below. |
|
|
22
|
-
| `blocks/*` — `
|
|
22
|
+
| `blocks/*` — `BlockManifestError`, `BLOCK_SCOPES`, `BLOCK_SCOPE_PATTERN`, `isMessage`, types (`BlockManifestV1`, `BlockContext`, `BlockToken`, `BlockSettings`, `ViewerInfo`, `ThemeInfo`, `BlockWorkflowSnapshot`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`, …) | Framework-agnostic contract for [Civitai Apps](https://github.com/civitai/civitai-app-starters/blob/main/docs/build-your-first-app-block.md). Ships a byte-identical copy of the server-published canonical JSON Schema (draft 2020-12, https://civitai.com/schemas/app-block/v1.json) at the `./schemas/app-block/v1.json` subpath for offline validation; a CI drift-check keeps it in sync. Runtime-agnostic and **zero runtime dependencies** — no React, no DOM types, nothing in your install graph. Hooks and the iframe transport live in a separate package. |
|
|
23
|
+
| `manifest/*` — `defineBlock`, `SCHEMA_DIVERGENCES`, `KNOWN_GAPS` (**Node only**) | Build-time manifest validation. `defineBlock(config)` compiles the vendored canonical schema with [Ajv](https://ajv.js.org) and validates a `BlockManifestV1` against it, so authoring mistakes surface in `pnpm dev` instead of at `civitai app validate`/submit. It is **derived from the schema, not a hand-written mirror of it** — that mirror is what produced [#330](https://github.com/civitai/civitai-app-starters/issues/330). Needs `node:fs` and the optional peer `ajv`, which is why it is not on the browser-facing `./blocks` surface. |
|
|
24
|
+
| `vite/*` — `blockManifestPlugin` (**Node only**) | A Vite plugin wrapping `defineBlock`, firing from `configResolved` — the one hook Vite calls on both the dev-server and the build path. Every block scaffold registers it, so `pnpm dev`, `pnpm dev:harness` and `pnpm build` all fail on a bad manifest with the offending field path. Optional peers: `ajv` (runtime) and `vite` (types only). |
|
|
23
25
|
|
|
24
26
|
## Subpath imports
|
|
25
27
|
|
|
26
28
|
```ts
|
|
27
|
-
import {
|
|
29
|
+
import { BLOCK_SCOPES, isSignedIn } from '@civitai/app-sdk/blocks';
|
|
30
|
+
// Build-time manifest validation (NODE ONLY — needs the optional peer `ajv`).
|
|
31
|
+
// Most projects want the Vite plugin below rather than calling this directly:
|
|
32
|
+
import { defineBlock } from '@civitai/app-sdk/manifest';
|
|
33
|
+
// The same gate as a Vite plugin (NODE ONLY — optional peers `ajv` + `vite`):
|
|
34
|
+
import { blockManifestPlugin } from '@civitai/app-sdk/vite';
|
|
28
35
|
// Type-only: the orchestrator's generated per-step shapes. Needs the optional
|
|
29
36
|
// peer `@civitai/client@beta` — see "Typed step shapes" below:
|
|
30
37
|
import type { TypedWorkflowTemplate } from '@civitai/app-sdk/orchestrator/steps';
|
|
@@ -37,9 +44,11 @@ import manifestSchema from '@civitai/app-sdk/schemas/app-block/v1.json' with { t
|
|
|
37
44
|
## Civitai Apps contract (`@civitai/app-sdk/blocks`)
|
|
38
45
|
|
|
39
46
|
> Building a **Civitai App** (an iframe-embedded UI on a civitai.com page)? This
|
|
40
|
-
> subpath is the framework-agnostic contract — manifest types, scope strings
|
|
41
|
-
> the `postMessage` protocol
|
|
42
|
-
>
|
|
47
|
+
> subpath is the framework-agnostic contract — manifest types, scope strings and
|
|
48
|
+
> the `postMessage` protocol. (The `defineBlock` validator moved to the node-only
|
|
49
|
+
> [`@civitai/app-sdk/manifest`](#defineblock-validator-rules) subpath; `./blocks`
|
|
50
|
+
> keeps zero runtime dependencies.) The React hooks + transport that consume it
|
|
51
|
+
> live in
|
|
43
52
|
> [`@civitai/blocks-react`](https://www.npmjs.com/package/@civitai/blocks-react).
|
|
44
53
|
> Start from the runnable [examples](https://github.com/civitai/civitai-app-starters/tree/main/starters/examples).
|
|
45
54
|
>
|
|
@@ -103,8 +112,7 @@ interface BlockInitPayload {
|
|
|
103
112
|
|
|
104
113
|
| Export | What |
|
|
105
114
|
|---|---|
|
|
106
|
-
| `
|
|
107
|
-
| `BLOCK_SCOPES` / `BLOCK_SCOPE_PATTERN` | The 15 known block scope strings (the authoritative enum `defineBlock` validates against) + the `domain:verb:target` format-helper regex. 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). |
|
|
115
|
+
| `BLOCK_SCOPES` / `BLOCK_SCOPE_PATTERN` | The 15 known block scope strings (the authoritative enum the canonical schema validates `scopes` against) + the `domain:verb:target` format-helper regex. 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). |
|
|
108
116
|
| `isMessage(data, type)` | Discriminator-only message narrowing (see above). |
|
|
109
117
|
| `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`. |
|
|
110
118
|
| `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. |
|
|
@@ -154,31 +162,67 @@ const body: WorkflowBody = {
|
|
|
154
162
|
|
|
155
163
|
### `defineBlock` validator rules
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
165
|
+
**There is no rule list here, and that is the point.** `defineBlock` does not
|
|
166
|
+
maintain one: it compiles the vendored copy of the
|
|
167
|
+
[canonical schema](https://civitai.com/schemas/app-block/v1.json) with
|
|
168
|
+
[Ajv](https://ajv.js.org) and validates against **that**. Every `required`,
|
|
169
|
+
`enum`, `pattern`, bound, `additionalProperties` and `allOf` the canonical
|
|
170
|
+
expresses is enforced, and moves when the canonical moves. The schema is the
|
|
171
|
+
rule list; read it, or read the errors.
|
|
172
|
+
|
|
173
|
+
That is [#330](https://github.com/civitai/civitai-app-starters/issues/330)'s own
|
|
174
|
+
proposed fix. The previous implementation hand-mirrored the schema, and the
|
|
175
|
+
mirror diverged exactly as the issue predicted: it required 11 fields where the
|
|
176
|
+
canonical requires **five** (`blockId`, `version`, `name`, `contentRating`,
|
|
177
|
+
`scopes`), required `appId` — not a canonical property at all — and *required*
|
|
178
|
+
the SERVER-OWNED `iframe.src` that `civitai app submit` refuses, so it rejected
|
|
179
|
+
every `block.manifest.json` this repo ships.
|
|
180
|
+
|
|
181
|
+
On top of the schema, `defineBlock` applies exactly five extra rules, each
|
|
182
|
+
mirroring a server rejection the canonical states only in **prose**:
|
|
183
|
+
|
|
184
|
+
| Rule | The canonical prose it mirrors |
|
|
185
|
+
|---|---|
|
|
186
|
+
| a dev-set **`iframe.src`** is rejected | *"SERVER-OWNED. Do NOT set `iframe.src` — the platform assigns it."* The JSON-Schema `not` is deliberately absent; the top-level `allOf` `$comment` says the platform validator and the Go CLI reject it instead. |
|
|
187
|
+
| a dev-set **`trustTier`** is rejected | *"SERVER-OWNED … the platform assigns the trust tier during review."* Same shape. |
|
|
188
|
+
| **`iframe.sandbox`** rejects `allow-same-origin` and every `allow-top-navigation*` token | *"Never combine allow-same-origin with allow-scripts."* Top-navigation is refused because a block must route navigation through the `NAVIGATE` postMessage. |
|
|
189
|
+
| every **`scopeJustifications`** key must be a scope in `scopes` | *"The requirement is enforced imperatively by the manifest validator (not expressed as JSON-Schema conditionals here)."* |
|
|
190
|
+
| **`settings`** is validated against the W3 settings meta-schema | Not in the app-block schema at all — settings are validated server-side by `manifest-settings.meta.schema.ts`. |
|
|
191
|
+
|
|
192
|
+
Each is **strictly additive**: it can only reject a manifest Ajv accepted, never
|
|
193
|
+
relax a canonical rule. `test/manifest/divergences.test.ts` asserts exactly that
|
|
194
|
+
per entry — the fixture must be schema-VALID and `defineBlock`-REJECTED — and the
|
|
195
|
+
table and the fixture set must be the same set, so a sixth hand-written rule with
|
|
196
|
+
no entry fails the suite.
|
|
197
|
+
|
|
198
|
+
> **Passing is necessary, not sufficient**, and it is **not** a replacement for
|
|
199
|
+
> `civitai app validate` (the Go CLI's local pre-check against the same
|
|
200
|
+
> canonical). Run that before `civitai app submit`. `KNOWN_GAPS` in
|
|
201
|
+
> `src/manifest/defineBlock.ts` names each thing only the server can check —
|
|
202
|
+
> including one that cuts the *other* way: the canonical's sandbox description is
|
|
203
|
+
> an **allowlist** (*"Unverified tier allows only: allow-scripts, allow-forms"*)
|
|
204
|
+
> while the rule above is a denylist, so `allow-popups`, `allow-modals` and
|
|
205
|
+
> `allow-downloads` **pass here and may be refused at review**. The tier is
|
|
206
|
+
> assigned server-side, so it cannot be enforced locally.
|
|
207
|
+
|
|
208
|
+
**Where it runs.** Every scaffold that ships a `block.manifest.json`
|
|
209
|
+
(`starters/civitai-block-starter` and all six `starters/examples/*`) registers
|
|
210
|
+
`blockManifestPlugin` from `@civitai/app-sdk/vite` in its `vite.config.ts`. It
|
|
211
|
+
fires from Vite's `configResolved`, the one hook called on both the dev-server
|
|
212
|
+
and the build path — so `pnpm dev`, `pnpm dev:harness` and `pnpm build` all fail
|
|
213
|
+
loudly on a bad manifest, with the offending field path in the message. Add
|
|
214
|
+
`ajv` to your devDependencies (it is an optional peer):
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pnpm add -D ajv
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
// vite.config.ts — drop this into your existing `defineConfig({ plugins: [...] })`.
|
|
222
|
+
import { blockManifestPlugin } from '@civitai/app-sdk/vite';
|
|
223
|
+
|
|
224
|
+
const plugins = [blockManifestPlugin()];
|
|
225
|
+
```
|
|
182
226
|
|
|
183
227
|
### Web storage in a block (`@civitai/app-sdk/safe-storage`)
|
|
184
228
|
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The App Storage rejection MESSAGES the host puts on the wire — the only site
|
|
3
|
+
* in this repository that spells them **in executable code**, sibling of
|
|
4
|
+
* `appStorageLimits.ts`, which owns the numbers.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 **"IN EXECUTABLE CODE" IS THE WHOLE CLAIM — do not read it wider.** What
|
|
7
|
+
* `tests/guards/app-storage-error-strings.test.mjs` actually enforces is
|
|
8
|
+
* narrower still: every `error:` value inside an `APP_STORAGE_*_RESULT` payload
|
|
9
|
+
* in the scanned mocks must NAME a constant from this module. It sees nothing
|
|
10
|
+
* else. Hand-typed copies of these six strings live today in
|
|
11
|
+
* `mockHost.ts`'s option docblocks (which ship in the published `.d.ts`),
|
|
12
|
+
* in `blocks-react/test/validate.test.ts`'s fixture, in this guard's own
|
|
13
|
+
* control assertions, and in both READMEs — and every one of them is invisible
|
|
14
|
+
* to the guard. If the host rewords a message they go stale silently. Grep the
|
|
15
|
+
* literal, not just the identifier, when a message moves.
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **THE WIRE CARRIES A MESSAGE, NOT A CODE.** The host's router throws a
|
|
18
|
+
* `TRPCError` that has BOTH: `code: 'PAYLOAD_TOO_LARGE'` and a per-site
|
|
19
|
+
* `message`. The bridge that answers `APP_STORAGE_SET` forwards
|
|
20
|
+
* **`err.message`** and never the code, so `PAYLOAD_TOO_LARGE` is a string a
|
|
21
|
+
* block CANNOT receive. `createMockHost` emitted it anyway for three releases:
|
|
22
|
+
* a block that branched on it took the actionable branch under `dev:mock` and
|
|
23
|
+
* the generic one in production, and nothing local could see the difference.
|
|
24
|
+
* That is civitai/civitai-app-starters#343, and this module is its fix — one
|
|
25
|
+
* spelling, shared by the mock, the example harness and any block matcher.
|
|
26
|
+
*
|
|
27
|
+
* ## 🔴 There is NO list here of "every string a block can receive" — on purpose
|
|
28
|
+
*
|
|
29
|
+
* **This is the THIRD attempt at this section, and the absence of that list is
|
|
30
|
+
* what the first two attempts cost. It is not an omission to be helpfully
|
|
31
|
+
* filled in.** Both earlier drafts shipped, and each was wrong by more than its
|
|
32
|
+
* author had checked:
|
|
33
|
+
*
|
|
34
|
+
* 1. **Draft 1 (RETRACTED)** presented six strings as "every storage-rejection
|
|
35
|
+
* string the host can put on the wire". It had read only the
|
|
36
|
+
* `PAYLOAD_TOO_LARGE` sites, so it missed the entire authorization family.
|
|
37
|
+
* 2. **Draft 2 (RETRACTED)** kept the six, added a table of eight
|
|
38
|
+
* authorization messages "measured in the same read", and presented THAT
|
|
39
|
+
* as the surface. Also wrong. It missed, in the same file: `Apps are not
|
|
40
|
+
* enabled`, thrown from TWO different gates with one spelling — the
|
|
41
|
+
* `enforceAppBlocksFlag` middleware (defined `:249`, throws at `:255`
|
|
42
|
+
* query / `:257` mutation), which is `.use()`d on all five storage
|
|
43
|
+
* procedures at `:470`, `:509`, `:938`, `:1022`, `:1106` BEFORE their
|
|
44
|
+
* `.input()`, so it fires before anything else on every call; and
|
|
45
|
+
* `assertAppBlocksEnabledForTokenUser` (`:139-155`, throwing at `:153`),
|
|
46
|
+
* reached from `resolveStorageContext`. Grepping `enforceAppBlocksFlag`
|
|
47
|
+
* will NOT find `:153`. Also missed: `block token subject could
|
|
48
|
+
* not be resolved` (`:148`, called unconditionally from
|
|
49
|
+
* `resolveStorageContext`), `review token subject could not be resolved`
|
|
50
|
+
* (`:82`) and `Apps authoring is not enabled for this account` (`:89`).
|
|
51
|
+
* `Apps are not enabled` is a feature-flag kill switch — neither a ceiling
|
|
52
|
+
* nor an authorization failure, so it fit neither published table.
|
|
53
|
+
*
|
|
54
|
+
* Each enumeration was wider than the last and each was still short; the
|
|
55
|
+
* router carries **21** `throw new TRPCError` sites and **17** distinct
|
|
56
|
+
* messages, against draft 2's 14. A third list would be the same defect a third
|
|
57
|
+
* time.
|
|
58
|
+
*
|
|
59
|
+
* 🔴 **The completeness claim is the wrong SHAPE, not merely a stale list.** The
|
|
60
|
+
* failure is not "this might go out of date" — it is that "the set of strings a
|
|
61
|
+
* block can receive" is the host's whole error surface, which this repository
|
|
62
|
+
* does not own, cannot observe from here, and has now mis-measured twice while
|
|
63
|
+
* believing otherwise. Do not soften this into "the list below may be
|
|
64
|
+
* incomplete" and restore one. What replaces it needs no completeness at all:
|
|
65
|
+
*
|
|
66
|
+
* - **This module classifies the `PAYLOAD_TOO_LARGE` family plus the bridge's
|
|
67
|
+
* fallback — a set THIS REPOSITORY CHOSE, not a set the host guarantees
|
|
68
|
+
* closed.** It is the five `PAYLOAD_TOO_LARGE` throws in `apps.router.ts`
|
|
69
|
+
* (`:568`, `:783`, `:791`, `:845`, `:853`) and the `'storage request failed'`
|
|
70
|
+
* literal at `IframeHost.tsx:287`. Naming what it classifies is a decision
|
|
71
|
+
* about scope; it needs no completeness claim to be useful, and it must not
|
|
72
|
+
* be dressed as one. 🔴 **A ceiling the host enforces OUTSIDE that family
|
|
73
|
+
* already exists** — see "Ceilings outside this set" below.
|
|
74
|
+
* - **Every OTHER host rejection reaches a block on the same `error` field and
|
|
75
|
+
* classifies `null`.** This is the whole of what a block author needs, it
|
|
76
|
+
* requires no enumeration, and it stays true when the host adds, rewords or
|
|
77
|
+
* deletes a message. It holds STRUCTURALLY, from two facts rather than from a
|
|
78
|
+
* survey: the bridge's catch arms are **blanket** `catch (err)` with no code
|
|
79
|
+
* filter (`IframeHost.tsx:2395` GET, `:2427` SET, `:2458` DELETE, `:2508`
|
|
80
|
+
* LIST, `:2535` QUOTA), each forwarding `err.message` verbatim via
|
|
81
|
+
* `storageErrorMessage()`; and {@link classifyAppStorageError} answers `null`
|
|
82
|
+
* for everything outside the ceiling patterns, which is checkable in this
|
|
83
|
+
* file without consulting the host at all.
|
|
84
|
+
* - **The RECIPE beats any prose in this repository — and is still NOT a
|
|
85
|
+
* completeness check.** It is strictly wider than every list anyone has
|
|
86
|
+
* written: it found all 21 `TRPCError` sites, including the four draft 2
|
|
87
|
+
* missed. It is NECESSARY, NOT SUFFICIENT — see "What the recipe cannot
|
|
88
|
+
* see" under "Re-deriving it".
|
|
89
|
+
*
|
|
90
|
+
* ## Ceilings outside this set
|
|
91
|
+
*
|
|
92
|
+
* 🔴 **The host enforces size ceilings that are NOT `TRPCError` throws and are
|
|
93
|
+
* therefore invisible to both the recipe and this module.** They are zod
|
|
94
|
+
* `.max()` bounds on the procedures' `.input()` schemas, so tRPC refuses the
|
|
95
|
+
* call with `BAD_REQUEST` before any handler runs. Measured in
|
|
96
|
+
* `apps.router.ts` on `civitai/civitai` `main`, 2026-09-21:
|
|
97
|
+
*
|
|
98
|
+
* - `const keyInput = z.string().min(1).max(200)` (`:460`) — the `key`
|
|
99
|
+
* argument of `get` (`:471`), `set` (`:511`) and `delete` (`:939`);
|
|
100
|
+
* - `list` (`:1025-1027`): `prefix: z.string().max(200)`,
|
|
101
|
+
* `cursor: z.string().max(400)`, `limit: …int().min(1).max(200)`.
|
|
102
|
+
*
|
|
103
|
+
* 🔴 **The 200-character KEY cap is the one a real block hits with no local
|
|
104
|
+
* warning.** Derive a key from a URL, a model name or a title and 201
|
|
105
|
+
* characters is ordinary. Nothing in this repository caps it: `useAppStorage`
|
|
106
|
+
* and `createMockHost` forward the key verbatim and the mock enforces no length
|
|
107
|
+
* gate (civitai/civitai-app-starters#370), so the write succeeds under
|
|
108
|
+
* `dev:mock` and fails forever in production. The zod refusal arrives on the
|
|
109
|
+
* same `error` field as everything else, so {@link classifyAppStorageError}
|
|
110
|
+
* answers `null` — and the recommended `null` copy ("try reloading the page")
|
|
111
|
+
* is WRONG for it: reloading re-mints the token and the write fails
|
|
112
|
+
* identically. If a block builds keys from untrusted-length input, cap or hash
|
|
113
|
+
* them at the block, and do not rely on a local run to tell you.
|
|
114
|
+
*
|
|
115
|
+
* 🔴 **So: wherever a list of host strings still appears — in this repo's
|
|
116
|
+
* READMEs, in the changeset, in `messages.ts`, in `@civitai/blocks-react`'s
|
|
117
|
+
* `src/hooks/useAppStorage.ts`, in the `kv-storage` example's `src/App.tsx`
|
|
118
|
+
* (which is where the "try reloading" copy lives), or in
|
|
119
|
+
* `test/blocks/appStorageErrors.test.ts` — it is ILLUSTRATIVE, NOT EXHAUSTIVE,
|
|
120
|
+
* and must be labelled as such.** Its job is to show a reader what the `null`
|
|
121
|
+
* bucket typically contains, never to bound it.
|
|
122
|
+
*
|
|
123
|
+
* ## Re-deriving it
|
|
124
|
+
*
|
|
125
|
+
* 🔴 **Not a `PAYLOAD_TOO_LARGE`-only grep** — that is structurally incapable of
|
|
126
|
+
* seeing anything but the ceiling family, and is how draft 1 came to claim a
|
|
127
|
+
* completeness it had not measured.
|
|
128
|
+
*
|
|
129
|
+
* ```sh
|
|
130
|
+
* # EVERY throw, not one code. Read the whole output; do not count from memory.
|
|
131
|
+
* gh api repos/civitai/civitai/contents/src/server/routers/apps.router.ts \
|
|
132
|
+
* --jq '.content' | base64 -d | grep -n "new TRPCError" -A4
|
|
133
|
+
* # …and confirm the catch arms are still blanket (no code filter), which is
|
|
134
|
+
* # what makes "everything else classifies null" true:
|
|
135
|
+
* gh api repos/civitai/civitai/contents/src/components/AppBlocks/IframeHost.tsx \
|
|
136
|
+
* --jq '.content' | base64 -d | grep -n "storageErrorMessage" -B12
|
|
137
|
+
* # …and the zod caps, which throw no TRPCError and so appear in NEITHER of the
|
|
138
|
+
* # two commands above:
|
|
139
|
+
* gh api repos/civitai/civitai/contents/src/server/routers/apps.router.ts \
|
|
140
|
+
* --jq '.content' | base64 -d | grep -n "z\.string()\|z\.number()\|\.max("
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* ### What the recipe cannot see
|
|
144
|
+
*
|
|
145
|
+
* 🔴 **A clean run of the three commands above is NOT proof of completeness —
|
|
146
|
+
* do not treat it as one.** Measured blind spots, both of which bit this file:
|
|
147
|
+
*
|
|
148
|
+
* - **zod-enforced caps.** They are `.input()` bounds, not throws, so
|
|
149
|
+
* `grep "new TRPCError"` cannot see them at all. That is why the third
|
|
150
|
+
* command exists — and why "a new ceiling can only arrive as a new
|
|
151
|
+
* `PAYLOAD_TOO_LARGE` throw" was deleted from this header: it was false
|
|
152
|
+
* when it was written. See "Ceilings outside this set" above.
|
|
153
|
+
* - **the bridge's own literal.** `'storage request failed'` is at
|
|
154
|
+
* `IframeHost.tsx:287`, and `grep … -B12` prints the twelve lines BEFORE
|
|
155
|
+
* each match — for the definition at `:282` that is 270–282, so line 287
|
|
156
|
+
* never appears in the output. The recipe enumerates the CALL SITES of
|
|
157
|
+
* `storageErrorMessage`, not its fallback. Read the function body.
|
|
158
|
+
*
|
|
159
|
+
* 🔴 **DO NOT re-derive the ceiling strings from this comment** — re-read the
|
|
160
|
+
* host. These are prose a server engineer wrote, not a published contract: they
|
|
161
|
+
* can be reworded in any deploy, and nothing will tell you. Which is also why
|
|
162
|
+
* {@link classifyAppStorageError} answers `null` rather than guessing, and why
|
|
163
|
+
* a block must branch on the CLASSIFICATION and render its OWN copy — see the
|
|
164
|
+
* warning on that function.
|
|
165
|
+
*
|
|
166
|
+
* One consequence worth stating plainly: `null` is a **BUSY** bucket, and its
|
|
167
|
+
* dominant real-world occupant is an expired or revoked token, not a rare
|
|
168
|
+
* unknown. Do not write a `default:` arm that assumes `null` means "transient,
|
|
169
|
+
* retry" — see the warning on {@link classifyAppStorageError}.
|
|
170
|
+
*
|
|
171
|
+
* ## Which ceiling each message names
|
|
172
|
+
*
|
|
173
|
+
* Two scopes, and the remedies differ:
|
|
174
|
+
*
|
|
175
|
+
* - **per-user** (`'per-user storage quota exceeded'`, `'per-user row limit
|
|
176
|
+
* exceeded'`) — this viewer has filled their own budget for this app. The
|
|
177
|
+
* ceilings are `APP_STORAGE_MAX_BYTES` / `APP_STORAGE_MAX_ROWS`;
|
|
178
|
+
* the viewer can free space by deleting their own rows.
|
|
179
|
+
* - **app-wide** (`'app quota exceeded'`, `'app row limit exceeded'`) — the
|
|
180
|
+
* app has filled a far larger umbrella shared across every viewer. One
|
|
181
|
+
* viewer's delete will not reliably clear it; this is the developer's
|
|
182
|
+
* problem, and `appStorageLimits.ts` deliberately does not export the
|
|
183
|
+
* umbrella's value because nothing reports usage against it.
|
|
184
|
+
*
|
|
185
|
+
* A block will normally hit the per-user pair. Both are reachable, so a
|
|
186
|
+
* matcher that handles only one is a matcher with a silent hole.
|
|
187
|
+
*/
|
|
188
|
+
/**
|
|
189
|
+
* The host's per-value-cap message, as a function of the cap — a mirror of the
|
|
190
|
+
* template literal at `apps.router.ts:569`, which divides by 1024 and appends
|
|
191
|
+
* `KB cap`.
|
|
192
|
+
*
|
|
193
|
+
* 🔴 **THIS IS A TEMPLATE ON THE HOST, SO IT IS A TEMPLATE HERE.** Writing
|
|
194
|
+
* `'value exceeds 64KB cap'` as a literal would be true only while the cap is
|
|
195
|
+
* 64KB, and a string that silently stops matching the host the day the cap
|
|
196
|
+
* moves is the exact defect #343 is about — one spelling that drifts, with
|
|
197
|
+
* nothing able to notice. Derived from {@link APP_STORAGE_MAX_VALUE_BYTES} so
|
|
198
|
+
* the two move together.
|
|
199
|
+
*
|
|
200
|
+
* Exported from this module (not from `@civitai/app-sdk/blocks`) so a test can
|
|
201
|
+
* feed it a cap the constant cannot equal and watch the output move. Blocks
|
|
202
|
+
* want {@link APP_STORAGE_ERROR_VALUE_TOO_LARGE}.
|
|
203
|
+
*/
|
|
204
|
+
export declare function appStorageValueTooLargeMessage(capBytes?: number): string;
|
|
205
|
+
/**
|
|
206
|
+
* A single value exceeded the per-value wire cap
|
|
207
|
+
* ({@link APP_STORAGE_MAX_VALUE_BYTES}). `apps.router.ts:568-569`.
|
|
208
|
+
*
|
|
209
|
+
* The only one of the six ceiling messages that is not a fixed string on the
|
|
210
|
+
* host — see {@link appStorageValueTooLargeMessage}.
|
|
211
|
+
*/
|
|
212
|
+
export declare const APP_STORAGE_ERROR_VALUE_TOO_LARGE: string;
|
|
213
|
+
/** The APP-wide byte umbrella is full. `apps.router.ts:782-785`. */
|
|
214
|
+
export declare const APP_STORAGE_ERROR_APP_QUOTA_EXCEEDED = "app quota exceeded";
|
|
215
|
+
/** The APP-wide row umbrella is full. `apps.router.ts:790-793`. */
|
|
216
|
+
export declare const APP_STORAGE_ERROR_APP_ROW_LIMIT = "app row limit exceeded";
|
|
217
|
+
/**
|
|
218
|
+
* This viewer's byte budget for this app (`APP_STORAGE_MAX_BYTES`) is
|
|
219
|
+
* full. `apps.router.ts:844-847`.
|
|
220
|
+
*/
|
|
221
|
+
export declare const APP_STORAGE_ERROR_USER_QUOTA_EXCEEDED = "per-user storage quota exceeded";
|
|
222
|
+
/**
|
|
223
|
+
* This viewer's row budget for this app (`APP_STORAGE_MAX_ROWS`) is
|
|
224
|
+
* full. `apps.router.ts:852-855`.
|
|
225
|
+
*
|
|
226
|
+
* The ceiling a block reaches first in practice: rows run out long before
|
|
227
|
+
* bytes do.
|
|
228
|
+
*/
|
|
229
|
+
export declare const APP_STORAGE_ERROR_USER_ROW_LIMIT = "per-user row limit exceeded";
|
|
230
|
+
/**
|
|
231
|
+
* The bridge's fallback, used for any storage failure whose error carries no
|
|
232
|
+
* usable message — a transport fault, a non-`Error` throw, an upstream 5xx.
|
|
233
|
+
* `IframeHost.tsx:287`.
|
|
234
|
+
*
|
|
235
|
+
* 🔴 It is NOT specific to writes. Every `APP_STORAGE_*` catch arm goes
|
|
236
|
+
* through the same helper, so a `get`, `delete`, `list` or `getQuota` can
|
|
237
|
+
* reject with it too. Retryable, unlike the five ceilings.
|
|
238
|
+
*/
|
|
239
|
+
export declare const APP_STORAGE_ERROR_REQUEST_FAILED = "storage request failed";
|
|
240
|
+
/**
|
|
241
|
+
* The **`PAYLOAD_TOO_LARGE` family** — one string per `PAYLOAD_TOO_LARGE` site
|
|
242
|
+
* in `apps.router.ts` — plus the bridge's fallback, as of the measurement in
|
|
243
|
+
* this file's header.
|
|
244
|
+
*
|
|
245
|
+
* 🔴 **NOT "every ceiling string the host can put on the wire", and NOT "every
|
|
246
|
+
* string a block can receive".** Two separate reasons, and both are load-
|
|
247
|
+
* bearing:
|
|
248
|
+
*
|
|
249
|
+
* - The bridge's catch arms are blanket, so every other rejection the host
|
|
250
|
+
* raises arrives on the SAME field and is absent here on purpose. `invalid
|
|
251
|
+
* block token`, `block instance revoked`, `Apps are not enabled` and the
|
|
252
|
+
* `storage … scope` template are examples of what that covers —
|
|
253
|
+
* **illustrations, not a bound.** See this file's header for why no list
|
|
254
|
+
* of them lives in this repository.
|
|
255
|
+
* - 🔴 The host also enforces **size ceilings zod-side** (`key` capped at 200
|
|
256
|
+
* characters on the `.input()` schema, and three more on `list`). Those
|
|
257
|
+
* throw no `TRPCError`, so they are invisible to the re-derivation recipe
|
|
258
|
+
* AND absent from this array, while being ceilings in every sense a block
|
|
259
|
+
* cares about. See "Ceilings outside this set" in this file's header.
|
|
260
|
+
*
|
|
261
|
+
* This is the set `createMockHost` and the starter harnesses must draw from —
|
|
262
|
+
* enforced by `tests/guards/app-storage-error-strings.test.mjs`, so a
|
|
263
|
+
* hand-typed string cannot reappear at a mock rejection site.
|
|
264
|
+
*
|
|
265
|
+
* 🔴 **NOT RE-EXPORTED FROM `@civitai/app-sdk/blocks`, ON PURPOSE.** Publishing
|
|
266
|
+
* this array invites `APP_STORAGE_HOST_ERROR_MESSAGES.includes(err.message)`,
|
|
267
|
+
* which is EQUALITY against a frozen snapshot — narrower than
|
|
268
|
+
* {@link isAppStorageHostErrorMessage}, and narrower than
|
|
269
|
+
* {@link classifyAppStorageError}, by exactly the per-value message, whose cap
|
|
270
|
+
* the host is free to move. That is the matcher shape #343 exists to
|
|
271
|
+
* eliminate, so it must not become public API. Reach it by file path from a
|
|
272
|
+
* test or a guard; a block branches on the classifier's reason.
|
|
273
|
+
*
|
|
274
|
+
* 🔴 **A CLOSED SET IS A CLAIM ABOUT A MEASUREMENT, NOT A CONTRACT — and this
|
|
275
|
+
* measurement is narrow by construction, not merely stale.** Three separate
|
|
276
|
+
* gaps, and only the first is about the future:
|
|
277
|
+
*
|
|
278
|
+
* 1. The host can add a `PAYLOAD_TOO_LARGE` site or reword an existing one
|
|
279
|
+
* in any deploy, and nothing here will notice.
|
|
280
|
+
* 2. **Today, already**, every non-ceiling rejection the host raises reaches
|
|
281
|
+
* the block on the same field and is not in this array. 🔴 This is not a
|
|
282
|
+
* hole to be filled — two drafts tried and both came up short (see this
|
|
283
|
+
* file's header). Those strings are the host's session, approval and
|
|
284
|
+
* feature-flag prose, not ceiling vocabulary, and enumerating them here
|
|
285
|
+
* would invite exactly the equality matching the rest of this comment
|
|
286
|
+
* argues against.
|
|
287
|
+
* 3. 🔴 **Today, already, a SIZE ceiling the host enforces is missing from
|
|
288
|
+
* here too** — the zod caps, which are not `TRPCError` throws at all.
|
|
289
|
+
* Unlike gap 2 this one IS ceiling vocabulary, which is why the array's
|
|
290
|
+
* scope is spelled as "the `PAYLOAD_TOO_LARGE` family" above rather than
|
|
291
|
+
* as "the ceilings". See "Ceilings outside this set" in this file's
|
|
292
|
+
* header.
|
|
293
|
+
*
|
|
294
|
+
* So an unrecognised string is "some storage failure, unknown which" — NOT
|
|
295
|
+
* "some ceiling", and never "impossible".
|
|
296
|
+
*/
|
|
297
|
+
export declare const APP_STORAGE_HOST_ERROR_MESSAGES: readonly [string, "app quota exceeded", "app row limit exceeded", "per-user storage quota exceeded", "per-user row limit exceeded", "storage request failed"];
|
|
298
|
+
/**
|
|
299
|
+
* Which rejection site a storage failure came from, as a closed set. `null`
|
|
300
|
+
* means the string matched nothing known — see {@link classifyAppStorageError}.
|
|
301
|
+
*/
|
|
302
|
+
export type AppStorageRejectionReason = 'value-too-large' | 'app-quota-exceeded' | 'app-row-limit' | 'user-quota-exceeded' | 'user-row-limit' | 'request-failed';
|
|
303
|
+
/**
|
|
304
|
+
* Turn a rejection — the `Error` `useAppStorage().set()` throws, or its raw
|
|
305
|
+
* message — into the rejection site it came from, or `null` when the string
|
|
306
|
+
* matches nothing this SDK version knows about.
|
|
307
|
+
*
|
|
308
|
+
* Branch on the RESULT; render your OWN copy.
|
|
309
|
+
*
|
|
310
|
+
* 🔴 **NEVER RENDER THE MESSAGE ITSELF TO A VIEWER.** It is host-authored
|
|
311
|
+
* server prose in the same class as a workflow `snapshot.error`: not
|
|
312
|
+
* localized, not written for an end user, and free to change. Log it for
|
|
313
|
+
* yourself (`console.warn`) and show copy your app owns.
|
|
314
|
+
*
|
|
315
|
+
* 🔴 **`null` IS A REAL OUTCOME, NOT AN ERROR IN YOUR CODE — AND IT DOES NOT
|
|
316
|
+
* MEAN "TRANSIENT".** THREE different things land here, and only one is a
|
|
317
|
+
* retry:
|
|
318
|
+
*
|
|
319
|
+
* - a ceiling message this SDK version has not seen (the host reworded one,
|
|
320
|
+
* or added a site, and your block compiled against an older SDK);
|
|
321
|
+
* - 🔴 **and, far more often in production, an authorization or kill-switch
|
|
322
|
+
* failure.** The bridge's catch arms are blanket, so every other rejection
|
|
323
|
+
* the host raises arrives on the same field and classifies `null` — for
|
|
324
|
+
* example `invalid block token` (an expired token mid-session), `block
|
|
325
|
+
* instance revoked`, `storage set requires the apps:storage:write scope`,
|
|
326
|
+
* or `Apps are not enabled` (the feature flag, which fires before anything
|
|
327
|
+
* else on every storage call). 🔴 **Those are ILLUSTRATIONS, not the set**:
|
|
328
|
+
* see this module's header for why no list of them lives here.
|
|
329
|
+
* - 🔴 **and a zod INPUT-VALIDATION refusal, which never reaches a handler at
|
|
330
|
+
* all.** tRPC parses `.input()` before the procedure body, so a bound
|
|
331
|
+
* violated there is a `BAD_REQUEST` with a zod-generated message — no
|
|
332
|
+
* `TRPCError` anywhere in the router, and nothing this module classifies.
|
|
333
|
+
* The one a real block hits: the host caps `key` at **200 characters**
|
|
334
|
+
* (`z.string().min(1).max(200)`), and neither `useAppStorage` nor
|
|
335
|
+
* `createMockHost` caps it locally, so a key derived from a URL or a model
|
|
336
|
+
* name can save fine under `dev:mock` and fail forever in production. 🔴
|
|
337
|
+
* **A RELOAD DOES NOT FIX THAT ONE** — see "Ceilings outside this set" in
|
|
338
|
+
* this module's header.
|
|
339
|
+
*
|
|
340
|
+
* So **"Please try again" is the wrong copy for the `null` arm.** Retrying an
|
|
341
|
+
* expired token forever is the failure this warning exists to prevent. Write a
|
|
342
|
+
* generic arm that offers a RELOAD (which re-mints the token, and also covers
|
|
343
|
+
* a genuine transport blip) and concedes that saving may be unavailable — and
|
|
344
|
+
* keep `'request-failed'` separate if you want honest retry copy, since that
|
|
345
|
+
* reason really is the transport one.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* try {
|
|
349
|
+
* await storage.set(key, note);
|
|
350
|
+
* } catch (err) {
|
|
351
|
+
* console.warn('[my-app] save failed:', err);
|
|
352
|
+
* switch (classifyAppStorageError(err)) {
|
|
353
|
+
* case 'value-too-large':
|
|
354
|
+
* return 'That note is too long to save. Try shortening it.';
|
|
355
|
+
* case 'user-row-limit':
|
|
356
|
+
* case 'app-row-limit':
|
|
357
|
+
* return 'You have no note slots left. Delete one to make room.';
|
|
358
|
+
* case 'request-failed':
|
|
359
|
+
* // The bridge's fallback: genuinely a transport fault. Retry is honest.
|
|
360
|
+
* return 'Could not save that note. Please try again.';
|
|
361
|
+
* default:
|
|
362
|
+
* // `null`: an unknown ceiling OR — usually — an expired/revoked token.
|
|
363
|
+
* return 'Could not save that note. Try reloading the page; if it keeps ' +
|
|
364
|
+
* 'happening, saving may be unavailable for this app right now.';
|
|
365
|
+
* }
|
|
366
|
+
* }
|
|
367
|
+
*/
|
|
368
|
+
export declare function classifyAppStorageError(error: unknown): AppStorageRejectionReason | null;
|
|
369
|
+
/**
|
|
370
|
+
* Is `message` one of the `PAYLOAD_TOO_LARGE`-family strings above (or the
|
|
371
|
+
* bridge's fallback)?
|
|
372
|
+
*
|
|
373
|
+
* 🔴 Not "a string the host can produce", and not "a ceiling" either — it
|
|
374
|
+
* answers `false` for every non-ceiling rejection the host raises, all of which
|
|
375
|
+
* the host produces and the bridge forwards on the same field, AND for the
|
|
376
|
+
* host's zod-enforced size caps, which are ceilings it does not know about (see
|
|
377
|
+
* "Ceilings outside this set" in this file's header). It is a membership test
|
|
378
|
+
* over {@link APP_STORAGE_HOST_ERROR_MESSAGES}, nothing wider.
|
|
379
|
+
*
|
|
380
|
+
* Wider than `APP_STORAGE_HOST_ERROR_MESSAGES.includes(…)` by exactly one
|
|
381
|
+
* case: the per-value message is a template on the host, so any cap spelling
|
|
382
|
+
* is admitted — see {@link classifyAppStorageError}.
|
|
383
|
+
*
|
|
384
|
+
* Module-internal, like the array: it is a `classifyAppStorageError(…) !== null`
|
|
385
|
+
* convenience for `tests/guards/app-storage-error-strings.test.mjs`, which
|
|
386
|
+
* imports this file by PATH. A block wants the classifier's reason, not a
|
|
387
|
+
* yes/no on host prose.
|
|
388
|
+
*/
|
|
389
|
+
export declare function isAppStorageHostErrorMessage(message: unknown): message is string;
|
|
390
|
+
//# sourceMappingURL=appStorageErrors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appStorageErrors.d.ts","sourceRoot":"","sources":["../../src/blocks/appStorageErrors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0LG;AAIH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,GAAE,MAAoC,GAC7C,MAAM,CAER;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,QAAmC,CAAC;AAElF,oEAAoE;AACpE,eAAO,MAAM,oCAAoC,uBAAuB,CAAC;AAEzE,mEAAmE;AACnE,eAAO,MAAM,+BAA+B,2BAA2B,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,qCAAqC,oCAAoC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,gCAAgC,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,2BAA2B,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,+BAA+B,+JAOlC,CAAC;AAEX;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GACjC,iBAAiB,GACjB,oBAAoB,GACpB,eAAe,GACf,qBAAqB,GACrB,gBAAgB,GAChB,gBAAgB,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,yBAAyB,GAAG,IAAI,CA4BxF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,MAAM,CAEhF"}
|