@civitai/sdk 0.10.1 → 0.11.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/BREAKING.md CHANGED
@@ -25,13 +25,22 @@ messages have no destination yet.
25
25
  | `CREATE_POST_FROM_APP` | — | No v1 route, **and deliberately staying on the bridge** — see below |
26
26
  | `PUBLISH_GENERATION_OUTPUTS` | `app.host.publishGenerationOutputs` | Same reasoning: it raises host UI, so it stays on the bridge — **carried** rather than replaced |
27
27
  | `SET_COLLECTION_FOLLOW` | `POST /api/v1/blocks/collections/{id}/follow` | **Route exists** (scope `collections:write:self`). This row previously said "No v1 route"; that was wrong |
28
- | `GET_DAILY_COMPENSATION`, `GET_WILDCARD_PACK`, `TRACK_EVENT` | — | Not carried |
29
-
30
- ⚠ `TRACK_EVENT` is filed above beside two one-consumer capabilities, which understates it: it is emitted by
31
- `useBlockAnalytics`, which the fleet apps call widely. It is listed as *not carried* because **it has
32
- no host handler today either** — `hostHandlerParity.ts` marks both hosts N/A, *"analytics fire-and-forget; no
33
- host-side sink wired (dropped, never hangs)"*. So those call sites are already no-ops; this is net-new
34
- capability rather than a migration gap.
28
+ | `GET_DAILY_COMPENSATION`, `GET_WILDCARD_PACK` | — | Not carried |
29
+ | `TRACK_EVENT` | `app.host.track(eventName, properties)` | **Carried**, same wire as `useBlockAnalytics().track`. 🔴 Only events and properties DECLARED under the manifest's `analytics.events` are recorded — see below |
30
+
31
+ ⚠ **`TRACK_EVENT` used to be listed here as *not carried*, because no host handled it — every
32
+ `useBlockAnalytics` call was a no-op.** Both hosts now forward it, so the calls an app already makes start
33
+ counting — but only against the declaration. What is recorded:
34
+
35
+ - **Declared events only.** An event name the approved manifest does not declare under `analytics.events` is
36
+ counted as a drop; its name and properties are not stored. An app that never declared its events records
37
+ nothing but that count.
38
+ - **Declared properties of the declared type only.** Types are `enum` (one of the declared strings), `number`
39
+ and `boolean`. There is no free-text string, so an id, a prompt or a username sent as a string is dropped.
40
+ An undeclared property, or a value of the wrong type, is dropped and the rest of the event kept.
41
+ - **Budgets.** Each mount may send 10 events a second and 500 in all, in bodies of at most 32 KiB; an event
42
+ over a limit is dropped in the browser and counted nowhere. Signed-out viewers are counted, never identified.
43
+ - **Fire-and-forget.** No reply, no acknowledgement. Owners see aggregates in the app's analytics panel.
35
44
 
36
45
  ## Scope binding is per-route
37
46
 
@@ -299,7 +308,7 @@ iframe, and nothing about the call site looks different.
299
308
  On `app.host`: `requestSignIn`, `download` (was `SAVE_IMAGE`),
300
309
  `openResourcePicker`, `openBuzzPurchase`, `openImageUpload`,
301
310
  `publishGenerationOutputs`, `resize`, **`autoResize`**, `reportError`, `navigate`
302
- and `onVisibilityChange`. Reach for `autoResize(element)` over bare `resize` —
311
+ and `onVisibilityChange` — plus `track`, which raises no UI (see `TRACK_EVENT` above). Reach for `autoResize(element)` over bare `resize` —
303
312
  it wires the `ResizeObserver` for you and returns a stop function. Consent is `app.requestGrants` (was `requestConsent`).
304
313
 
305
314
  `publishGenerationOutputs` keeps `usePublishGenerationOutputs`'s wire exactly —
package/README.md CHANGED
@@ -71,7 +71,11 @@ observed two ways is two values that can disagree.
71
71
  A block opts in by declaring `auth: "oauth"` in its `block.manifest.json`. The
72
72
  host then hands it a real OAuth access token that `/api/v1`, the orchestrator
73
73
  and the MCP accept, and consent — including `requestGrants` — goes through the
74
- host's consent dialog.
74
+ host's consent dialog. The manifest must also declare `user:read:self`, which
75
+ every OAuth token carries: on civitai.com the host never mints one without it
76
+ and hands back the block token. The dev tunnel mints one regardless, so a
77
+ missing `user:read:self` only shows up after approval. And `auth: "oauth"` cannot be declared alongside any
78
+ `apps:storage:*` scope: such a manifest is refused at submit.
75
79
 
76
80
  ### When the token is the block-scoped one
77
81
 
@@ -290,6 +294,28 @@ The `400` body is `{ error: 'Invalid query', details }` and the SDK reads `error
290
294
  first, so `ApiError.message` says only "Invalid query" — the field-level reason is
291
295
  in `ApiError.body.details`.
292
296
 
297
+ **Writes are rate-limited per (viewer, app)**; the limits are listed once, in
298
+ [the `@civitai/blocks-react` README](https://github.com/civitai/civitai-app-starters/blob/main/packages/civitai-blocks-react/README.md#shared-storage-write-limits). A write refused for a limit is an `ApiError` with
299
+ `status === 429`, a `Retry-After` header the client does not expose, and a body
300
+ of `{ code: 'TOO_MANY_REQUESTS', message, error, rateLimit: { retryAfterSeconds,
301
+ retryAt } }`. This client does not validate `body.rateLimit`, so check it before
302
+ you render it:
303
+
304
+ ```ts
305
+ import { ApiError } from '@civitai/sdk';
306
+
307
+ try {
308
+ await app.sharedStorage.append({ title: 'My deck' });
309
+ } catch (error) {
310
+ const rateLimit =
311
+ error instanceof ApiError && error.status === 429
312
+ ? (error.body as { rateLimit?: { retryAt?: unknown } } | null)?.rateLimit
313
+ : undefined;
314
+ if (typeof rateLimit?.retryAt === 'string') showRetryAt(new Date(rateLimit.retryAt));
315
+ else throw error;
316
+ }
317
+ ```
318
+
293
319
  If you are porting from `@civitai/blocks-react`'s `useSharedStorage`, the bridge
294
320
  *host* normalised all of this for you and nothing normalises it here;
295
321
  [`BREAKING.md`](./BREAKING.md) covers what the port does and does not cost.
@@ -349,6 +375,7 @@ wider scope — and refuses without one.
349
375
  | `navigate(path, { scope, target })` | Deep-links within this app's own sub-paths; `scope: 'site'` asks to leave the app for a civitai.com page instead. `scope` defaults to `'app'` |
350
376
  | `onVisibilityChange(handler)` | Reports the page hiding and returning |
351
377
  | `requestSignIn({ returnUrl })` | Starts sign-in; the block re-initialises signed in |
378
+ | `track(eventName, properties)` | Sends a custom event to your app's analytics, fire-and-forget. Only events and properties declared under `analytics.events` in the manifest are recorded — see *Custom events* below |
352
379
  | `download({ url, filename })` | Saves to the viewer's device — a sandboxed frame cannot |
353
380
  | `openResourcePicker({ resourceType })` | civitai's own picker; resolves the one pick, or `null` |
354
381
  | `openBuzzPurchase({ suggestedAmount })` | The purchase flow; resolves `{ purchased }` |
@@ -452,6 +479,38 @@ than failing the call, so `imageIds` can be **shorter** than what you selected
452
479
  and nothing says which index dropped. Compare lengths; do not pair ids to
453
480
  indexes.
454
481
 
482
+ ### Custom events
483
+
484
+ ```ts
485
+ app.host.track('generate_clicked', { mode: 'txt2img', batch: 4 });
486
+ ```
487
+
488
+ 🔴 **Only what the manifest declares is recorded.** Declare each event, and each
489
+ property it carries, under `analytics.events` in `block.manifest.json`; events
490
+ are accepted against the approved manifest:
491
+
492
+ ```json
493
+ "analytics": {
494
+ "events": {
495
+ "generate_clicked": {
496
+ "properties": {
497
+ "mode": { "type": "enum", "values": ["txt2img", "img2img"] },
498
+ "batch": { "type": "number" }
499
+ }
500
+ }
501
+ }
502
+ }
503
+ ```
504
+
505
+ - An undeclared event is counted as dropped; its name and properties are not stored.
506
+ - A declared event keeps only its declared properties, each only when the value
507
+ has the declared type. Types are `enum`, `number` and `boolean`; there is no
508
+ free-text string, so a string is kept only when it is one of the enum's values.
509
+ - Each mount may send 10 events a second and 500 in all, in bodies of at most
510
+ 32 KiB. An event over a limit is dropped in the browser and counted nowhere.
511
+ - Fire-and-forget: there is no reply. Owners see aggregates in the app's
512
+ analytics panel, never individual events.
513
+
455
514
  ## Parent origins
456
515
 
457
516
  Messages are accepted only from an allowlisted parent origin. The allowlist is
@@ -50,6 +50,20 @@ export interface Host {
50
50
  requestSignIn(args?: {
51
51
  returnUrl?: string;
52
52
  }): void;
53
+ /**
54
+ * Sends one custom event to the app's analytics. Fire-and-forget: no reply
55
+ * and no acknowledgement. The wire is `useBlockAnalytics().track`'s.
56
+ *
57
+ * 🔴 Only what the manifest DECLARES under `analytics.events` is recorded,
58
+ * against the approved manifest. An undeclared event is counted as dropped and
59
+ * its name is not stored. A declared event keeps only its declared
60
+ * properties, each only when the value has the declared type — `enum` (one of
61
+ * the declared strings), `number` or `boolean`; there is no free-text string.
62
+ * Each mount may send 10 events a second and 500 in all; an event over a
63
+ * limit is dropped in the browser and counted nowhere. Owners see aggregates
64
+ * in the app's analytics panel, never individual events.
65
+ */
66
+ track(eventName: string, properties?: Record<string, unknown>): void;
53
67
  /**
54
68
  * Saves to the viewer's device through the host, because a sandboxed frame
55
69
  * cannot. The host fetches only from origins it allowlists.
@@ -40,6 +40,10 @@ export function createHost(transport) {
40
40
  };
41
41
  },
42
42
  requestSignIn: (args = {}) => notify('REQUEST_SIGN_IN', args, { transport }),
43
+ // `properties` is OMITTED when not given, so the payload is byte-identical
44
+ // to what `useBlockAnalytics().track(name)` sends after JSON drops the
45
+ // `undefined`.
46
+ track: (eventName, properties) => notify('TRACK_EVENT', { eventName, ...(properties !== undefined ? { properties } : {}) }, { transport }),
43
47
  async download(request, opts = {}) {
44
48
  await call('SAVE_IMAGE', request, { ...opts, transport });
45
49
  },
@@ -175,6 +175,10 @@ export type HostNotifications = {
175
175
  REQUEST_CONSENT: {
176
176
  scopes?: string[];
177
177
  };
178
+ TRACK_EVENT: {
179
+ eventName: string;
180
+ properties?: Record<string, unknown>;
181
+ };
178
182
  };
179
183
  export type HostPushes = {
180
184
  /** The page is hidden; stop timers and polling until `RESUME`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "The Civitai SDK: an app holds a token and calls the Civitai API and the orchestrator; a block asks its host only to show host UI.",
5
5
  "license": "MIT",
6
6
  "type": "module",