@civitai/app-sdk 0.48.0 → 0.50.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.
Files changed (56) hide show
  1. package/README.md +30 -4
  2. package/dist/blocks/appStorageErrors.d.ts +390 -0
  3. package/dist/blocks/appStorageErrors.js +429 -0
  4. package/dist/blocks/index.d.ts +33 -0
  5. package/dist/blocks/index.js +32 -0
  6. package/dist/blocks/messages.d.ts +2 -2
  7. package/dist/index.d.ts +56 -1
  8. package/dist/index.js +56 -0
  9. package/dist/oauth/index.d.ts +23 -0
  10. package/dist/orchestrator/steps.d.ts +38 -9
  11. package/dist/orchestrator/steps.js +37 -8
  12. package/package.json +3 -1
  13. package/dist/blocks/appStorageLimits.d.ts.map +0 -1
  14. package/dist/blocks/appStorageLimits.js.map +0 -1
  15. package/dist/blocks/browsingLevel.d.ts.map +0 -1
  16. package/dist/blocks/browsingLevel.js.map +0 -1
  17. package/dist/blocks/index.d.ts.map +0 -1
  18. package/dist/blocks/index.js.map +0 -1
  19. package/dist/blocks/initFragment.d.ts.map +0 -1
  20. package/dist/blocks/initFragment.js.map +0 -1
  21. package/dist/blocks/manifestError.d.ts.map +0 -1
  22. package/dist/blocks/manifestError.js.map +0 -1
  23. package/dist/blocks/messages.d.ts.map +0 -1
  24. package/dist/blocks/messages.js.map +0 -1
  25. package/dist/blocks/scopes.d.ts.map +0 -1
  26. package/dist/blocks/scopes.js.map +0 -1
  27. package/dist/blocks/types.d.ts.map +0 -1
  28. package/dist/blocks/types.js.map +0 -1
  29. package/dist/cookies/index.d.ts.map +0 -1
  30. package/dist/cookies/index.js.map +0 -1
  31. package/dist/index.d.ts.map +0 -1
  32. package/dist/index.js.map +0 -1
  33. package/dist/manifest/defineBlock.d.ts.map +0 -1
  34. package/dist/manifest/defineBlock.js.map +0 -1
  35. package/dist/manifest/index.d.ts.map +0 -1
  36. package/dist/manifest/index.js.map +0 -1
  37. package/dist/oauth/authorize.d.ts.map +0 -1
  38. package/dist/oauth/authorize.js.map +0 -1
  39. package/dist/oauth/index.d.ts.map +0 -1
  40. package/dist/oauth/index.js.map +0 -1
  41. package/dist/oauth/pkce.d.ts.map +0 -1
  42. package/dist/oauth/pkce.js.map +0 -1
  43. package/dist/oauth/token.d.ts.map +0 -1
  44. package/dist/oauth/token.js.map +0 -1
  45. package/dist/orchestrator/index.d.ts.map +0 -1
  46. package/dist/orchestrator/index.js.map +0 -1
  47. package/dist/orchestrator/steps.d.ts.map +0 -1
  48. package/dist/orchestrator/steps.js.map +0 -1
  49. package/dist/safe-storage/index.d.ts.map +0 -1
  50. package/dist/safe-storage/index.js.map +0 -1
  51. package/dist/scopes/index.d.ts.map +0 -1
  52. package/dist/scopes/index.js.map +0 -1
  53. package/dist/types.d.ts.map +0 -1
  54. package/dist/types.js.map +0 -1
  55. package/dist/vite/index.d.ts.map +0 -1
  56. package/dist/vite/index.js.map +0 -1
package/README.md CHANGED
@@ -23,9 +23,31 @@ pnpm add @civitai/app-sdk
23
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
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). |
25
25
 
26
+ ## Entry points, and what the root barrel is
27
+
28
+ The root — `import … from '@civitai/app-sdk'` — is **exactly the union of four subpaths**: `@civitai/app-sdk/oauth`, `/scopes`, `/cookies` and `/orchestrator`. All of each, and nothing else. Import from the root when you want the OAuth-app surface in one specifier; import a subpath when you want only that slice. The two are the same symbols either way, which is asserted in both directions by `test/export-surface.test.ts` — the root cannot quietly gain a symbol none of those four exports, or miss one they do.
29
+
30
+ The other five subpaths are **deliberately not on the root**, each for a concrete cost it would push onto every consumer:
31
+
32
+ | Subpath | Why it is not on the root |
33
+ |---|---|
34
+ | `@civitai/app-sdk/blocks` | Importing it runs `/safe-storage` for its side effect, and it is the Civitai-Apps contract — a disjoint audience from OAuth apps. |
35
+ | `@civitai/app-sdk/safe-storage` | Its purpose *is* the module side effect. A side effect on the root barrel is not something a consumer can opt out of. |
36
+ | `@civitai/app-sdk/orchestrator/steps` | Type-only, and its declarations name `@civitai/client` — an **optional** peer that the root would make mandatory for everyone. |
37
+ | `@civitai/app-sdk/manifest` | **Node only** — needs `node:fs` and the optional peer `ajv`. |
38
+ | `@civitai/app-sdk/vite` | **Node only** — optional peers `ajv` (runtime) and `vite` (types). |
39
+
40
+ > One name is declared twice on purpose: `BuzzAccountType` is the full set of Civitai Buzz pools on the root and `/oauth`, and a narrower three-pool union on `/blocks` — a block can neither prefer nor read the others. Same name, two subpaths, two types; the divergence is pinned rather than merged.
41
+
26
42
  ## Subpath imports
27
43
 
28
44
  ```ts
45
+ // The OAuth-app surface, also available whole from the root specifier:
46
+ import { buildAuthorizeUrl, exchangeCode, type OAuthTokens } from '@civitai/app-sdk/oauth';
47
+ import { TokenScope, bitmaskFromScopes } from '@civitai/app-sdk/scopes';
48
+ import { sealCookie, unsealCookie } from '@civitai/app-sdk/cookies';
49
+ import { submitWorkflow, WORKFLOW_STEP_TYPES } from '@civitai/app-sdk/orchestrator';
50
+ // The Civitai Apps contract (a different audience — see above):
29
51
  import { BLOCK_SCOPES, isSignedIn } from '@civitai/app-sdk/blocks';
30
52
  // Build-time manifest validation (NODE ONLY — needs the optional peer `ajv`).
31
53
  // Most projects want the Vite plugin below rather than calling this directly:
@@ -420,7 +442,7 @@ The starters in `civitai/civitai-app-starters` wire this into framework-specific
420
442
 
421
443
  ## Choosing a workflow step type
422
444
 
423
- The orchestrator is a workflow API: each request submits a list of typed steps. `WORKFLOW_STEP_TYPES` is the in-code catalog of every step `$type` it accepts, with a one-line description for each — `textToImage`, `imageGen`, `videoGen`, `comfy`, `customComfy`, `textToSpeech`, `aceStepAudio`, `transcription`, `imageUpscaler`, and 38 more (47 in total).
445
+ The orchestrator is a workflow API: each request submits a list of typed steps. `WORKFLOW_STEP_TYPES` is the in-code catalog of every step `$type` it accepts, with a one-line description for each — `textToImage`, `imageGen`, `videoGen`, `comfy`, `customComfy`, `textToSpeech`, `aceStepAudio`, `transcription`, `imageUpscaler` among them (50 in total).
424
446
 
425
447
  The catalog is pinned to the orchestrator's published OpenAPI spec two ways — an offline unit test against a transcribed copy of the spec's `WorkflowStepTemplate` discriminator mapping, and a CI job (`pnpm check:catalogs`) that re-fetches the live spec and diffs it. If a `$type` is listed here, the orchestrator accepts it.
426
448
 
@@ -495,12 +517,16 @@ What it exports:
495
517
 
496
518
  | Export | What |
497
519
  |---|---|
498
- | `WorkflowStepTemplates` | `$type` → template type, for all 47 step types. Keyed by the WIRE name, which the generated type names don't always match (`model3DPreview` → `Model3dPreviewStepTemplate`). |
520
+ | `WorkflowStepTemplates` | `$type` → template type, for 47 of the catalog's 50 step types. Keyed by the WIRE name, which the generated type names don't always match (`model3DPreview` → `Model3dPreviewStepTemplate`). |
499
521
  | `WorkflowStepTemplateFor<'videoGen'>` | One step's template. |
500
522
  | `WorkflowStepInputFor<'videoGen'>` | One step's `input` shape, without needing the generated `*Input` name. |
501
- | `AnyWorkflowStepTemplate` | Discriminated union of all 47 — `Extract<…, { $type: 'comfy' }>` and exhaustive `switch` work. `@civitai/client`'s base `WorkflowStepTemplate` has `$type` as a bare `string`, so it narrows nothing. |
523
+ | `AnyWorkflowStepTemplate` | Discriminated union of all 47 mapped templates — `Extract<…, { $type: 'comfy' }>` and exhaustive `switch` work. `@civitai/client`'s base `WorkflowStepTemplate` has `$type` as a bare `string`, so it narrows nothing. |
502
524
  | `TypedWorkflowTemplate` | The submit envelope with `steps` narrowed to that union. Pass it straight to `submitWorkflow` / `estimateWorkflow`. |
503
525
 
526
+ > 🔴 **The map is not total over the catalog, and that is the expected state.** `WORKFLOW_STEP_TYPES` documents 50 `$type`s; this map covers 47. The 3 with no generated template in the pinned `@civitai/client` are `imageScanning`, `preprocessVideo`, `yuE2`, and `WorkflowStepTemplateFor<…>` is a compile error for each of them.
527
+ >
528
+ > The two surfaces move independently on purpose: the catalog tracks the **live** orchestrator spec (a daily job syncs it), while these types track whatever `@civitai/client` was last published from. So the catalog runs ahead and the client catches up. The gap is never silent — `test/orchestrator/step-templates.test-d.ts` carries it as a `never` ledger plus one `@ts-expect-error` per gap `$type`, and `test/orchestrator/step-count-prose.test.ts` derives all four numbers (50, 47, 3, and the names) from `WORKFLOW_STEP_TYPES` and the map's own AST, then fails if this paragraph or its twin in `src/orchestrator/steps.ts` disagrees by one character.
529
+
504
530
  The generated `*StepTemplate` and `*Input` types are **not** re-exported individually. Using this subpath already requires `@civitai/client` installed, so if you want one by name, import it straight from there — `import type { TextToImageStepTemplate } from '@civitai/client'`.
505
531
 
506
532
  In practice `WorkflowStepTemplateFor<T>` and `WorkflowStepInputFor<T>` are the better route: they take the wire `$type` you already have, rather than the generated name you would otherwise have to go look up (`model3DPreview` → `Model3dPreviewStepTemplate`).
@@ -536,7 +562,7 @@ A `$type` having a type here says nothing about whether you may submit it.
536
562
 
537
563
  Several of the 47 exist to serve Civitai's own pipelines rather than third-party apps — `modelPickleScan`, `xGuardModeration`, `training`, `comfyNodepackSnapshot`, `qwenImageBench`, the `model*`/`media*` hashing and classification steps. They're in the consumer spec, so they're typed here. They are not an invitation.
538
564
 
539
- Note that `WORKFLOW_STEP_TYPES` does **not** mark most of them: of its 47 entries exactly two — `comfyNodepackSnapshot` and `qwenImageBench` — sit under its "Platform internals" heading, and the rest are ordinary documented entries (`webScrape` even carries usage notes). The reason all 47 are typed is not that the catalog flags the internals; it's that the catalog *documents* all 47 and a `$type`-keyed lookup is only sound as a lookup if it's total — a partial map would make `WorkflowStepTemplateFor<'training'>` a compile error for a step type the SDK documents, and would make the key-parity assertion impossible.
565
+ Note that `WORKFLOW_STEP_TYPES` does **not** mark most of them: of its 50 entries exactly two — `comfyNodepackSnapshot` and `qwenImageBench` — sit under its "Platform internals" heading, and the rest are ordinary documented entries (`webScrape` even carries usage notes). The reason the platform steps are typed anyway is not that the catalog flags them as internal; it's that the catalog *documents* them, so skipping them would make `WorkflowStepTemplateFor<'training'>` a compile error for a step type the SDK documents — which is exactly what is live today for the 3 `$type`s the pinned client cannot type, and is why that gap is spelled out above rather than left to be discovered.
540
566
 
541
567
  ## Public vs. confidential clients
542
568
 
@@ -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