@civitai/app-sdk 0.54.0 → 0.55.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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The IDEMPOTENCY-KEY FORMAT the host enforces on every money POST — **the only
3
+ * site in this repository that spells this rule**.
4
+ *
5
+ * Sibling of `./appStorageLimits.ts`, for the same reason: the rule lives in the
6
+ * host, the SDK cannot change it, and a hand-copied regex at each call site is
7
+ * how the copies come to agree with each other and disagree with the host.
8
+ *
9
+ * ## THE MOTIVATING FAILURE (2026-10-02)
10
+ *
11
+ * A character-sheet block composed its key as `sheetId:panelId:nonce` — a
12
+ * perfectly reasonable-looking composite id. It passed **201 local tests**, the
13
+ * dev harness and review. Every save in production failed:
14
+ *
15
+ * ```json
16
+ * { "code": "invalid_format", "format": "regex",
17
+ * "pattern": "/^[A-Za-z0-9_-]{1,64}$/",
18
+ * "path": ["idempotencyKey"],
19
+ * "message": "Invalid string: must match pattern /^[A-Za-z0-9_-]{1,64}$/" }
20
+ * ```
21
+ *
22
+ * `BAD_REQUEST` / httpStatus 400 on path `blocks.submitWorkflow`.
23
+ *
24
+ * 🔴 **THE REASON IT GOT THAT FAR: NOTHING IN THIS REPOSITORY MODELLED THE
25
+ * RULE.** The SDK's own key GENERATOR was tested against the charset, but a
26
+ * `crypto.randomUUID()` and the `idem-<base36>` fallback both conform and
27
+ * always did — so the one guard that existed covered the half that cannot fail,
28
+ * while a CALLER-SUPPLIED key went through unexamined at every hook, and the dev
29
+ * mock host had no concept of `idempotencyKey` at all.
30
+ *
31
+ * ## PROVENANCE — measured, not assumed
32
+ *
33
+ * Read from the `civitai/civitai` working tree on **2026-10-02**, file
34
+ * `src/server/utils/block-gen-idempotency.ts` line 77:
35
+ *
36
+ * ```ts
37
+ * export const BLOCK_IDEMPOTENCY_KEY_REGEX = /^[A-Za-z0-9_-]{1,64}$/;
38
+ * ```
39
+ *
40
+ * Enforced at **four** host entry points, which is why no single endpoint fix
41
+ * would have closed it:
42
+ *
43
+ * | entry point | file:line | optional? |
44
+ * |---|---|---|
45
+ * | tRPC `blocks.submitWorkflow` (the one that 400'd) | `src/server/routers/blocks.router.ts:6227` | `.optional()` |
46
+ * | REST `POST /api/v1/blocks/workflows/submit` | `src/pages/api/v1/blocks/workflows/submit.ts:135` | **REQUIRED** |
47
+ * | REST `POST /api/v1/blocks/goods/purchase` | `src/pages/api/v1/blocks/goods/purchase.ts:79` | `.optional()` |
48
+ * | REST `POST /api/v1/blocks/tip` | `src/pages/api/v1/blocks/tip.ts:88` | `.optional()` |
49
+ *
50
+ * 🔴 **THE TWO SURFACES ANSWER DIFFERENT ERROR ENVELOPES — do not quote one for
51
+ * the other.** The tRPC/bridge path (the one the block→host `SUBMIT_WORKFLOW`
52
+ * message takes, and the one that produced the report above) answers the
53
+ * `invalid_format` object shown earlier, which reaches a block as a host
54
+ * `errorSnapshot`. The three REST routes answer, verbatim and all three
55
+ * identically (verified 2026-10-02 at `submit.ts:160`, `tip.ts:127`,
56
+ * `goods/purchase.ts:117`):
57
+ *
58
+ * ```json
59
+ * { "error": "Invalid request body", "details": <zod flatten()> }
60
+ * ```
61
+ *
62
+ * also with HTTP 400. So there is no single "the 400 payload" for this field —
63
+ * which path you are on decides it.
64
+ *
65
+ * Re-derive rather than trusting this comment:
66
+ *
67
+ * ```sh
68
+ * gh api repos/civitai/civitai/contents/src/server/utils/block-gen-idempotency.ts \
69
+ * --jq '.content' | base64 -d | grep -n 'BLOCK_IDEMPOTENCY_KEY_REGEX'
70
+ * ```
71
+ *
72
+ * ## 🔴 WHY THE COLON BAN IS A CORRECTNESS INVARIANT, NOT COSMETIC
73
+ *
74
+ * The host's own comment calls the charset "colon-free", and two of its redis
75
+ * key builders depend on that being true:
76
+ *
77
+ * - `block-gen-idempotency.ts` composes `<prefix>:<userId>:<appBlockId>:<key>`
78
+ * and documents it as **INJECTIVE** *because* "`idempotencyKey` is
79
+ * charset-restricted … (colon-free). So no two distinct (user, app, key)
80
+ * triples can ever collide on the delimiter."
81
+ * - `block-tip-rate-limit.ts:215` composes the tip idempotency key the same
82
+ * way, with the same stated justification.
83
+ *
84
+ * A colon-bearing key therefore does not merely fail a cosmetic check — it would
85
+ * make those keys **non-injective**, letting one app's slot alias another's. The
86
+ * tip module spells out the harm in that event: app B "would then REPLAY app A's
87
+ * cached response body verbatim, LEARNING A's tip recipient and amount, while
88
+ * B's own tip silently never happens." The 400 is what prevents it.
89
+ *
90
+ * Separately, the key is substringed into the orchestrator `externalId`
91
+ * (`blk<NN><appBlockId><key>`), whose own contract is `^[A-Za-z0-9_-]+$` with a
92
+ * **128**-char ceiling — a colon is outside that charset too. The host's source
93
+ * records that an earlier revision assumed a colon-delimited id and that it
94
+ * "would have 400'd every keyed block generation submit."
95
+ *
96
+ * ## 🔴 WHERE THE 64 COMES FROM
97
+ *
98
+ * It is DERIVED, not chosen. The host composes the orchestrator `externalId` as
99
+ * `blk<NN><appBlockId><key>`; `ORCHESTRATOR_EXTERNAL_ID_MAX` is 128. 64 keeps the
100
+ * worst-case composition inside 128. Do not "relax" it here — the SDK is not the
101
+ * authority, and a key this module accepts but the host rejects is the exact
102
+ * defect above, reintroduced.
103
+ *
104
+ * ## 🔴 REFUSE, NEVER REWRITE
105
+ *
106
+ * Nothing in this module sanitises, truncates or normalises a key, and no
107
+ * consumer may. An idempotency key is an **identity**: silently rewriting a
108
+ * caller's key breaks the property the key exists to provide — two distinct
109
+ * logical submits could collapse onto one slot (one charge for two intended
110
+ * operations), or a retry could be rewritten differently from the first attempt
111
+ * and mint a SECOND reservation for one logical operation. Both are money bugs,
112
+ * and both are quieter than a rejection. A malformed key is a defect in the
113
+ * calling block's code; the only safe response is to refuse it loudly, before
114
+ * anything is sent.
115
+ */
116
+ /**
117
+ * The host's charset + length rule for a money-POST idempotency key, vendored
118
+ * verbatim from `block-gen-idempotency.ts:77`.
119
+ *
120
+ * 🔴 NO `g` FLAG, DELIBERATELY. A `g`-flagged regex carries `lastIndex` across
121
+ * calls, so repeated `.test()` on the SAME instance alternates true/false for an
122
+ * identical input — which on this surface would mean a key accepted on one
123
+ * submit and refused on its retry.
124
+ */
125
+ export declare const BLOCK_IDEMPOTENCY_KEY_REGEX: RegExp;
126
+ /**
127
+ * The host's length ceiling, named so a caller composing a key can budget
128
+ * against it instead of rediscovering 64 from a rejection.
129
+ *
130
+ * Kept as its own constant rather than parsed back out of the regex: this is the
131
+ * number a caller does arithmetic with, and the two are pinned to each other by
132
+ * `blockIdempotencyKeyRejection`'s own tests.
133
+ */
134
+ export declare const BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH = 64;
135
+ /**
136
+ * Does `value` clear the host's rule?
137
+ *
138
+ * Returns `false` for a non-string (including `null`/`undefined`) rather than
139
+ * throwing: callers reach this with untrusted input from a block's own code, and
140
+ * a type-level `string` is not a runtime guarantee at a package boundary.
141
+ *
142
+ * 🔴 This is the predicate EVERY consumer must call. Re-spelling the regex at a
143
+ * call site is what produced the defect in this module's header.
144
+ *
145
+ * 🔴 RETURNS `boolean`, NOT A TYPE PREDICATE (`value is string`), DELIBERATELY.
146
+ * A predicate signature narrows the FALSE branch to `Exclude<string, string>` =
147
+ * `never` for an already-`string` argument, which silently makes the rejection
148
+ * path below uncompilable — and in other callers would make a legitimate
149
+ * else-branch look like dead code. The narrowing bought nothing: no caller needs
150
+ * `unknown` → `string` narrowing from this function.
151
+ */
152
+ export declare function isValidBlockIdempotencyKey(value: unknown): boolean;
153
+ /**
154
+ * A developer-facing explanation of WHY `value` was refused, or `null` when it
155
+ * is valid.
156
+ *
157
+ * 🔴 DEVELOPER-FACING, NEVER VIEWER-FACING. A malformed idempotency key is a bug
158
+ * in the block's own code — no retry fixes it and no viewer can act on it, so
159
+ * rendering this string into UI tells the wrong person. It names the offending
160
+ * value because that is the single most useful fact for the person who has to
161
+ * fix it, and because the key is the block's own construction, not viewer data.
162
+ *
163
+ * The returned text is not a contract — branch on
164
+ * {@link isValidBlockIdempotencyKey}, never on this wording.
165
+ *
166
+ * Reasons are reported SPECIFICALLY rather than as one generic sentence: "it has
167
+ * a colon" and "it is 71 characters" lead to different fixes, and the colon case
168
+ * is the one that looks most like valid input.
169
+ */
170
+ export declare function blockIdempotencyKeyRejection(value: unknown): string | null;
171
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The IDEMPOTENCY-KEY FORMAT the host enforces on every money POST — **the only
3
+ * site in this repository that spells this rule**.
4
+ *
5
+ * Sibling of `./appStorageLimits.ts`, for the same reason: the rule lives in the
6
+ * host, the SDK cannot change it, and a hand-copied regex at each call site is
7
+ * how the copies come to agree with each other and disagree with the host.
8
+ *
9
+ * ## THE MOTIVATING FAILURE (2026-10-02)
10
+ *
11
+ * A character-sheet block composed its key as `sheetId:panelId:nonce` — a
12
+ * perfectly reasonable-looking composite id. It passed **201 local tests**, the
13
+ * dev harness and review. Every save in production failed:
14
+ *
15
+ * ```json
16
+ * { "code": "invalid_format", "format": "regex",
17
+ * "pattern": "/^[A-Za-z0-9_-]{1,64}$/",
18
+ * "path": ["idempotencyKey"],
19
+ * "message": "Invalid string: must match pattern /^[A-Za-z0-9_-]{1,64}$/" }
20
+ * ```
21
+ *
22
+ * `BAD_REQUEST` / httpStatus 400 on path `blocks.submitWorkflow`.
23
+ *
24
+ * 🔴 **THE REASON IT GOT THAT FAR: NOTHING IN THIS REPOSITORY MODELLED THE
25
+ * RULE.** The SDK's own key GENERATOR was tested against the charset, but a
26
+ * `crypto.randomUUID()` and the `idem-<base36>` fallback both conform and
27
+ * always did — so the one guard that existed covered the half that cannot fail,
28
+ * while a CALLER-SUPPLIED key went through unexamined at every hook, and the dev
29
+ * mock host had no concept of `idempotencyKey` at all.
30
+ *
31
+ * ## PROVENANCE — measured, not assumed
32
+ *
33
+ * Read from the `civitai/civitai` working tree on **2026-10-02**, file
34
+ * `src/server/utils/block-gen-idempotency.ts` line 77:
35
+ *
36
+ * ```ts
37
+ * export const BLOCK_IDEMPOTENCY_KEY_REGEX = /^[A-Za-z0-9_-]{1,64}$/;
38
+ * ```
39
+ *
40
+ * Enforced at **four** host entry points, which is why no single endpoint fix
41
+ * would have closed it:
42
+ *
43
+ * | entry point | file:line | optional? |
44
+ * |---|---|---|
45
+ * | tRPC `blocks.submitWorkflow` (the one that 400'd) | `src/server/routers/blocks.router.ts:6227` | `.optional()` |
46
+ * | REST `POST /api/v1/blocks/workflows/submit` | `src/pages/api/v1/blocks/workflows/submit.ts:135` | **REQUIRED** |
47
+ * | REST `POST /api/v1/blocks/goods/purchase` | `src/pages/api/v1/blocks/goods/purchase.ts:79` | `.optional()` |
48
+ * | REST `POST /api/v1/blocks/tip` | `src/pages/api/v1/blocks/tip.ts:88` | `.optional()` |
49
+ *
50
+ * 🔴 **THE TWO SURFACES ANSWER DIFFERENT ERROR ENVELOPES — do not quote one for
51
+ * the other.** The tRPC/bridge path (the one the block→host `SUBMIT_WORKFLOW`
52
+ * message takes, and the one that produced the report above) answers the
53
+ * `invalid_format` object shown earlier, which reaches a block as a host
54
+ * `errorSnapshot`. The three REST routes answer, verbatim and all three
55
+ * identically (verified 2026-10-02 at `submit.ts:160`, `tip.ts:127`,
56
+ * `goods/purchase.ts:117`):
57
+ *
58
+ * ```json
59
+ * { "error": "Invalid request body", "details": <zod flatten()> }
60
+ * ```
61
+ *
62
+ * also with HTTP 400. So there is no single "the 400 payload" for this field —
63
+ * which path you are on decides it.
64
+ *
65
+ * Re-derive rather than trusting this comment:
66
+ *
67
+ * ```sh
68
+ * gh api repos/civitai/civitai/contents/src/server/utils/block-gen-idempotency.ts \
69
+ * --jq '.content' | base64 -d | grep -n 'BLOCK_IDEMPOTENCY_KEY_REGEX'
70
+ * ```
71
+ *
72
+ * ## 🔴 WHY THE COLON BAN IS A CORRECTNESS INVARIANT, NOT COSMETIC
73
+ *
74
+ * The host's own comment calls the charset "colon-free", and two of its redis
75
+ * key builders depend on that being true:
76
+ *
77
+ * - `block-gen-idempotency.ts` composes `<prefix>:<userId>:<appBlockId>:<key>`
78
+ * and documents it as **INJECTIVE** *because* "`idempotencyKey` is
79
+ * charset-restricted … (colon-free). So no two distinct (user, app, key)
80
+ * triples can ever collide on the delimiter."
81
+ * - `block-tip-rate-limit.ts:215` composes the tip idempotency key the same
82
+ * way, with the same stated justification.
83
+ *
84
+ * A colon-bearing key therefore does not merely fail a cosmetic check — it would
85
+ * make those keys **non-injective**, letting one app's slot alias another's. The
86
+ * tip module spells out the harm in that event: app B "would then REPLAY app A's
87
+ * cached response body verbatim, LEARNING A's tip recipient and amount, while
88
+ * B's own tip silently never happens." The 400 is what prevents it.
89
+ *
90
+ * Separately, the key is substringed into the orchestrator `externalId`
91
+ * (`blk<NN><appBlockId><key>`), whose own contract is `^[A-Za-z0-9_-]+$` with a
92
+ * **128**-char ceiling — a colon is outside that charset too. The host's source
93
+ * records that an earlier revision assumed a colon-delimited id and that it
94
+ * "would have 400'd every keyed block generation submit."
95
+ *
96
+ * ## 🔴 WHERE THE 64 COMES FROM
97
+ *
98
+ * It is DERIVED, not chosen. The host composes the orchestrator `externalId` as
99
+ * `blk<NN><appBlockId><key>`; `ORCHESTRATOR_EXTERNAL_ID_MAX` is 128. 64 keeps the
100
+ * worst-case composition inside 128. Do not "relax" it here — the SDK is not the
101
+ * authority, and a key this module accepts but the host rejects is the exact
102
+ * defect above, reintroduced.
103
+ *
104
+ * ## 🔴 REFUSE, NEVER REWRITE
105
+ *
106
+ * Nothing in this module sanitises, truncates or normalises a key, and no
107
+ * consumer may. An idempotency key is an **identity**: silently rewriting a
108
+ * caller's key breaks the property the key exists to provide — two distinct
109
+ * logical submits could collapse onto one slot (one charge for two intended
110
+ * operations), or a retry could be rewritten differently from the first attempt
111
+ * and mint a SECOND reservation for one logical operation. Both are money bugs,
112
+ * and both are quieter than a rejection. A malformed key is a defect in the
113
+ * calling block's code; the only safe response is to refuse it loudly, before
114
+ * anything is sent.
115
+ */
116
+ /**
117
+ * The host's charset + length rule for a money-POST idempotency key, vendored
118
+ * verbatim from `block-gen-idempotency.ts:77`.
119
+ *
120
+ * 🔴 NO `g` FLAG, DELIBERATELY. A `g`-flagged regex carries `lastIndex` across
121
+ * calls, so repeated `.test()` on the SAME instance alternates true/false for an
122
+ * identical input — which on this surface would mean a key accepted on one
123
+ * submit and refused on its retry.
124
+ */
125
+ export const BLOCK_IDEMPOTENCY_KEY_REGEX = /^[A-Za-z0-9_-]{1,64}$/;
126
+ /**
127
+ * The host's length ceiling, named so a caller composing a key can budget
128
+ * against it instead of rediscovering 64 from a rejection.
129
+ *
130
+ * Kept as its own constant rather than parsed back out of the regex: this is the
131
+ * number a caller does arithmetic with, and the two are pinned to each other by
132
+ * `blockIdempotencyKeyRejection`'s own tests.
133
+ */
134
+ export const BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH = 64;
135
+ /**
136
+ * Does `value` clear the host's rule?
137
+ *
138
+ * Returns `false` for a non-string (including `null`/`undefined`) rather than
139
+ * throwing: callers reach this with untrusted input from a block's own code, and
140
+ * a type-level `string` is not a runtime guarantee at a package boundary.
141
+ *
142
+ * 🔴 This is the predicate EVERY consumer must call. Re-spelling the regex at a
143
+ * call site is what produced the defect in this module's header.
144
+ *
145
+ * 🔴 RETURNS `boolean`, NOT A TYPE PREDICATE (`value is string`), DELIBERATELY.
146
+ * A predicate signature narrows the FALSE branch to `Exclude<string, string>` =
147
+ * `never` for an already-`string` argument, which silently makes the rejection
148
+ * path below uncompilable — and in other callers would make a legitimate
149
+ * else-branch look like dead code. The narrowing bought nothing: no caller needs
150
+ * `unknown` → `string` narrowing from this function.
151
+ */
152
+ export function isValidBlockIdempotencyKey(value) {
153
+ return typeof value === 'string' && BLOCK_IDEMPOTENCY_KEY_REGEX.test(value);
154
+ }
155
+ /**
156
+ * A developer-facing explanation of WHY `value` was refused, or `null` when it
157
+ * is valid.
158
+ *
159
+ * 🔴 DEVELOPER-FACING, NEVER VIEWER-FACING. A malformed idempotency key is a bug
160
+ * in the block's own code — no retry fixes it and no viewer can act on it, so
161
+ * rendering this string into UI tells the wrong person. It names the offending
162
+ * value because that is the single most useful fact for the person who has to
163
+ * fix it, and because the key is the block's own construction, not viewer data.
164
+ *
165
+ * The returned text is not a contract — branch on
166
+ * {@link isValidBlockIdempotencyKey}, never on this wording.
167
+ *
168
+ * Reasons are reported SPECIFICALLY rather than as one generic sentence: "it has
169
+ * a colon" and "it is 71 characters" lead to different fixes, and the colon case
170
+ * is the one that looks most like valid input.
171
+ */
172
+ export function blockIdempotencyKeyRejection(value) {
173
+ if (typeof value !== 'string') {
174
+ return `the idempotency key must be a string, got ${value === null ? 'null' : typeof value}`;
175
+ }
176
+ if (isValidBlockIdempotencyKey(value))
177
+ return null;
178
+ const why = [];
179
+ if (value.length === 0) {
180
+ why.push('it is empty');
181
+ }
182
+ else if (value.length > BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH) {
183
+ why.push(`it is ${value.length} characters (the host allows at most ${BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH})`);
184
+ }
185
+ // Report the offending characters, de-duplicated and in first-appearance
186
+ // order, so a key with four colons does not print four identical complaints.
187
+ const offending = [...new Set(value.split('').filter((ch) => !/[A-Za-z0-9_-]/.test(ch)))];
188
+ if (offending.length > 0) {
189
+ const shown = offending.map((ch) => JSON.stringify(ch)).join(', ');
190
+ why.push(`it contains ${offending.length === 1 ? 'the character' : 'the characters'} ${shown}` +
191
+ ` (the host allows letters, digits, underscore and hyphen only` +
192
+ (offending.includes(':')
193
+ ? // Called out by name: a colon is BOTH the most natural delimiter for a
194
+ // composite key and the one character the host's redis-key injectivity
195
+ // argument depends on excluding. See this module's header.
196
+ `; a colon is specifically excluded because the host composes its` +
197
+ ` per-(user, app, key) rate-limit and dedupe keys with ':' as the delimiter`
198
+ : '') +
199
+ `)`);
200
+ }
201
+ // Unreachable while the regex and this function agree — but a regex change
202
+ // that this function does not mirror must not produce an EMPTY reason, which
203
+ // would read as "refused for no stated cause".
204
+ if (why.length === 0) {
205
+ why.push(`it does not match the host's required pattern ${String(BLOCK_IDEMPOTENCY_KEY_REGEX)}`);
206
+ }
207
+ return `${why.join(', and ')}. Offending value: ${JSON.stringify(value)}`;
208
+ }
209
+ //# sourceMappingURL=idempotency.js.map
@@ -28,6 +28,7 @@ export type { BlockScope, BlockScopeKey, BlockCategory } from './scopes.js';
28
28
  export { BrowsingLevel, SFW_LEVELS, NSFW_LEVELS, isSfwCeiling, isLevelAllowed, effectiveBrowsingCeiling, } from './browsingLevel.js';
29
29
  export type { BrowsingLevelKey, BrowsingLevelBit, ColorDomain } from './browsingLevel.js';
30
30
  export { APP_STORAGE_MAX_VALUE_BYTES, APP_STORAGE_MAX_BYTES, APP_STORAGE_MAX_ROWS, } from './appStorageLimits.js';
31
+ export { BLOCK_IDEMPOTENCY_KEY_REGEX, BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH, isValidBlockIdempotencyKey, blockIdempotencyKeyRejection, } from './idempotency.js';
31
32
  /**
32
33
  * {@link classifyAppStorageError} — the matcher a block branches on — plus the
33
34
  * four rejection messages a MOCK HOST has to emit. The wire carries a
@@ -31,6 +31,7 @@ export { BlockManifestError } from './manifestError.js';
31
31
  export { BLOCK_SCOPES, BLOCK_SCOPE_PATTERN, BLOCK_CATEGORIES, BLOCK_TAGLINE_MAX_LENGTH, } from './scopes.js';
32
32
  export { BrowsingLevel, SFW_LEVELS, NSFW_LEVELS, isSfwCeiling, isLevelAllowed, effectiveBrowsingCeiling, } from './browsingLevel.js';
33
33
  export { APP_STORAGE_MAX_VALUE_BYTES, APP_STORAGE_MAX_BYTES, APP_STORAGE_MAX_ROWS, } from './appStorageLimits.js';
34
+ export { BLOCK_IDEMPOTENCY_KEY_REGEX, BLOCK_IDEMPOTENCY_KEY_MAX_LENGTH, isValidBlockIdempotencyKey, blockIdempotencyKeyRejection, } from './idempotency.js';
34
35
  /**
35
36
  * {@link classifyAppStorageError} — the matcher a block branches on — plus the
36
37
  * four rejection messages a MOCK HOST has to emit. The wire carries a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "OAuth + PKCE, encrypted-cookie sessions, scopes, and orchestrator helpers for building third-party Civitai apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",