@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.
- package/README.md +30 -4
- package/dist/blocks/appStorageErrors.d.ts +390 -0
- package/dist/blocks/appStorageErrors.js +429 -0
- package/dist/blocks/index.d.ts +33 -0
- package/dist/blocks/index.js +32 -0
- package/dist/blocks/messages.d.ts +2 -2
- package/dist/index.d.ts +56 -1
- package/dist/index.js +56 -0
- package/dist/oauth/index.d.ts +23 -0
- package/dist/orchestrator/steps.d.ts +38 -9
- package/dist/orchestrator/steps.js +37 -8
- package/package.json +3 -1
- package/dist/blocks/appStorageLimits.d.ts.map +0 -1
- package/dist/blocks/appStorageLimits.js.map +0 -1
- package/dist/blocks/browsingLevel.d.ts.map +0 -1
- package/dist/blocks/browsingLevel.js.map +0 -1
- package/dist/blocks/index.d.ts.map +0 -1
- package/dist/blocks/index.js.map +0 -1
- package/dist/blocks/initFragment.d.ts.map +0 -1
- package/dist/blocks/initFragment.js.map +0 -1
- package/dist/blocks/manifestError.d.ts.map +0 -1
- package/dist/blocks/manifestError.js.map +0 -1
- package/dist/blocks/messages.d.ts.map +0 -1
- package/dist/blocks/messages.js.map +0 -1
- package/dist/blocks/scopes.d.ts.map +0 -1
- package/dist/blocks/scopes.js.map +0 -1
- package/dist/blocks/types.d.ts.map +0 -1
- package/dist/blocks/types.js.map +0 -1
- package/dist/cookies/index.d.ts.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/manifest/defineBlock.d.ts.map +0 -1
- package/dist/manifest/defineBlock.js.map +0 -1
- package/dist/manifest/index.d.ts.map +0 -1
- package/dist/manifest/index.js.map +0 -1
- package/dist/oauth/authorize.d.ts.map +0 -1
- package/dist/oauth/authorize.js.map +0 -1
- package/dist/oauth/index.d.ts.map +0 -1
- package/dist/oauth/index.js.map +0 -1
- package/dist/oauth/pkce.d.ts.map +0 -1
- package/dist/oauth/pkce.js.map +0 -1
- package/dist/oauth/token.d.ts.map +0 -1
- package/dist/oauth/token.js.map +0 -1
- package/dist/orchestrator/index.d.ts.map +0 -1
- package/dist/orchestrator/index.js.map +0 -1
- package/dist/orchestrator/steps.d.ts.map +0 -1
- package/dist/orchestrator/steps.js.map +0 -1
- package/dist/safe-storage/index.d.ts.map +0 -1
- package/dist/safe-storage/index.js.map +0 -1
- package/dist/scopes/index.d.ts.map +0 -1
- package/dist/scopes/index.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
- 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
|
|
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
|
|
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
|
|
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
|