@civitai/sdk 0.9.0 → 0.10.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 CHANGED
@@ -60,6 +60,14 @@ Both give you:
60
60
  A block's `app` also has `host`, `viewer`, `context`, `settings`, `theme` and
61
61
  `onChange(listener)`. Outside a block they do not exist, and the types say so.
62
62
 
63
+ `context` and `theme` are LIVE, not mount-time snapshots: the host pushes
64
+ `THEME_CHANGE` when the viewer toggles dark mode and `ROUTE_CHANGED` when a page
65
+ block's sub-path moves, and both are folded into the same snapshot — so
66
+ `app.context.subPath` tracks `app.host.navigate(path)` (and the viewer's own back
67
+ button) and `onChange` tells you when. Read them on each render rather than
68
+ copying them once; there is deliberately no `onRouteChange`, because one value
69
+ observed two ways is two values that can disagree.
70
+
63
71
  A block opts in by declaring `auth: "oauth"` in its `block.manifest.json`. The
64
72
  host then hands it a real OAuth access token that `/api/v1`, the orchestrator
65
73
  and the MCP accept, and consent — including `requestGrants` — goes through the
@@ -338,7 +346,7 @@ wider scope — and refuses without one.
338
346
  | `resize(height)` | Resizes the frame, clamped to the manifest |
339
347
  | `autoResize(element?)` | Keeps the frame as tall as the body (or `element`); returns a stop function |
340
348
  | `reportError(message, { fatal })` | `fatal` swaps the block for the host's fallback |
341
- | `navigate(path, { target })` | Deep-links within this app's own sub-paths |
349
+ | `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'` |
342
350
  | `onVisibilityChange(handler)` | Reports the page hiding and returning |
343
351
  | `requestSignIn({ returnUrl })` | Starts sign-in; the block re-initialises signed in |
344
352
  | `download({ url, filename })` | Saves to the viewer's device — a sandboxed frame cannot |
@@ -99,7 +99,17 @@ export interface BlockInitPayload {
99
99
  /** The domain ceiling narrowed to this viewer. Always a subset of the above. */
100
100
  effectiveBrowsingLevel?: number;
101
101
  }
102
- /** Host messages the transport itself acts on. Domains own everything else. */
102
+ /**
103
+ * Host messages the transport itself acts on. Domains own everything else.
104
+ *
105
+ * `ROUTE_CHANGED` is here rather than in `HostPushes` for the same reason
106
+ * `THEME_CHANGE` is: it carries a value the snapshot already holds, so the
107
+ * transport folds it in and `client.context` / `client.onChange` are how a
108
+ * consumer observes it. A `HostPushes` row plus an `onRouteChange` method would
109
+ * publish a SECOND way to read one value, which is how `client.context.subPath`
110
+ * and a callback end up disagreeing. `SUSPEND`/`RESUME` are in `HostPushes`
111
+ * because they carry no state for the snapshot to hold.
112
+ */
103
113
  export type HostMessage = {
104
114
  type: 'BLOCK_INIT';
105
115
  payload: BlockInitPayload;
@@ -119,6 +129,11 @@ export type HostMessage = {
119
129
  payload: {
120
130
  theme: Theme;
121
131
  };
132
+ } | {
133
+ type: 'ROUTE_CHANGED';
134
+ payload: {
135
+ subPath: string;
136
+ };
122
137
  };
123
138
  /** Discriminator only — the payload is still unvalidated wire data. */
124
139
  export declare function isHostMessage<K extends HostMessage['type']>(data: unknown, type: K): data is Extract<HostMessage, {
@@ -287,6 +287,18 @@ export class IframeTransport {
287
287
  this.#applyThemeChange(data.payload.theme);
288
288
  return;
289
289
  }
290
+ // The page's sub-path moved under the app root. Folded into the snapshot so
291
+ // `client.context.subPath` is live and `onChange` reports it — the same
292
+ // treatment as the theme, and the reason there is no `onRouteChange`.
293
+ //
294
+ // Unlike the theme this IS effectively init-gated, because the sub-path
295
+ // lives only in `context` and the pre-init context has no such key. That
296
+ // costs nothing: the host sends this only after init, and `BLOCK_INIT`
297
+ // replaces the snapshot with its own current value.
298
+ if (isHostMessage(data, 'ROUTE_CHANGED')) {
299
+ this.#applyRouteChange(data.payload.subPath);
300
+ return;
301
+ }
290
302
  // Matching the TYPE too, not just the id: `IMAGE_SCAN_RESOLVED` is a push
291
303
  // that deliberately reuses its `OPEN_IMAGE_UPLOAD`'s requestId, so an
292
304
  // id-only match could resolve the upload with the scan verdict's payload.
@@ -337,6 +349,18 @@ export class IframeTransport {
337
349
  this.#snapshot = next;
338
350
  this.#emit();
339
351
  }
352
+ // Only ever UPDATES a context that already carries the key; never INTRODUCES
353
+ // it. A model slot has no route, so synthesising `subPath` would assert a page
354
+ // context the host never sent. Emits only when the value moved.
355
+ #applyRouteChange(subPath) {
356
+ const context = this.#snapshot.context;
357
+ if (!('subPath' in context))
358
+ return;
359
+ if (context.subPath === subPath)
360
+ return;
361
+ this.#snapshot = { ...this.#snapshot, context: { ...context, subPath } };
362
+ this.#emit();
363
+ }
340
364
  #emit() {
341
365
  for (const listener of this.#listeners)
342
366
  listener();
@@ -1,7 +1,7 @@
1
1
  import { type BlockTransport } from '../core/transport.js';
2
2
  import type { Session } from '../session/index.js';
3
- import type { DownloadRequest, ImageScanResult, PickedResource, ResourcePickerType, SourceImage } from './protocol.js';
4
- export type { ConsentRefusal, DownloadRequest, ImageScanResult, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './protocol.js';
3
+ import type { DownloadRequest, ImageScanResult, NavigateScope, PickedResource, ResourcePickerType, SourceImage } from './protocol.js';
4
+ export type { ConsentRefusal, DownloadRequest, ImageScanResult, NavigateScope, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './protocol.js';
5
5
  export interface HostCallOptions {
6
6
  signal?: AbortSignal;
7
7
  }
@@ -33,8 +33,15 @@ export interface Host {
33
33
  reportError(message: string, args?: {
34
34
  fatal?: boolean;
35
35
  }): void;
36
- /** Deep-links within this app's own sub-paths; the host refuses anything else. */
36
+ /**
37
+ * Asks the host to navigate. `scope` picks the space `path` is resolved in and
38
+ * DEFAULTS to `'app'` — this app's own sub-paths, which is all this method
39
+ * could reach before the field existed. Pass `scope: 'site'` to leave the app
40
+ * for a civitai.com page; the host grants that per-surface and refuses it
41
+ * elsewhere. Fire-and-forget either way: a refusal is silent.
42
+ */
37
43
  navigate(path: string, args?: {
44
+ scope?: NavigateScope;
38
45
  target?: 'current' | 'new_tab';
39
46
  }): void;
40
47
  /** Fires `false` when the page hides and `true` when it returns. */
@@ -25,7 +25,12 @@ export function createHost(transport) {
25
25
  return () => observer.disconnect();
26
26
  },
27
27
  reportError: (message, args = {}) => notify('BLOCK_ERROR', { message, fatal: args.fatal ?? false }, { transport }),
28
- navigate: (path, args = {}) => notify('NAVIGATE', { path, target: args.target ?? 'current' }, { transport }),
28
+ // `scope` is OMITTED rather than sent as `undefined` when the caller did not
29
+ // choose one. Absent and `'app'` mean the same thing to the host, so this is
30
+ // not a behaviour difference — it keeps the wire payload byte-identical to
31
+ // what every pre-`scope` build sends, which is the property the back-compat
32
+ // claim rests on and the one a host-side diff can actually check.
33
+ navigate: (path, args = {}) => notify('NAVIGATE', { path, ...(args.scope ? { scope: args.scope } : {}), target: args.target ?? 'current' }, { transport }),
29
34
  onVisibilityChange(handler) {
30
35
  const offSuspend = on(transport, 'SUSPEND', () => handler(false));
31
36
  const offResume = on(transport, 'RESUME', () => handler(true));
@@ -1,5 +1,18 @@
1
1
  import type { WrappedToken } from '../core/handshake.js';
2
2
  export type ResourcePickerType = 'Checkpoint' | 'LORA';
3
+ /**
4
+ * Which SPACE a `navigate()` path is resolved in.
5
+ *
6
+ * - `'app'` — under this app's own route, as a sub-path of it. The DEFAULT.
7
+ * - `'site'` — at the site root, leaving the app. The host grants this
8
+ * per-surface and refuses it where the surface does not hold the capability.
9
+ *
10
+ * Omitting it means `'app'`, which is what keeps this field additive: a block
11
+ * that never sets it behaves as it did before the field existed. A leading slash
12
+ * carries no meaning — the host normalises it away in both scopes, so
13
+ * `'/settings'` and `'settings'` are one request within the scope you chose.
14
+ */
15
+ export type NavigateScope = 'app' | 'site';
3
16
  /** The one resource the viewer picked. Discovery only: nothing here is an entitlement. */
4
17
  export interface PickedResource {
5
18
  versionId: number;
@@ -153,6 +166,7 @@ export type HostNotifications = {
153
166
  };
154
167
  NAVIGATE: {
155
168
  path: string;
169
+ scope?: NavigateScope;
156
170
  target: 'current' | 'new_tab';
157
171
  };
158
172
  REQUEST_SIGN_IN: {
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export type { StorageCallOptions, StorageClient, StorageKeyEntry, StorageListQue
14
14
  export { isTerminal, isTerminalStatus } from './orchestration/index.js';
15
15
  export type { OrchestrationClient, Step, StepTemplate, WaitOptions, Workflow, WorkflowPage, WorkflowQuery, WorkflowStatus, WorkflowTemplate, } from './orchestration/index.js';
16
16
  export { createHost } from './host/index.js';
17
- export type { ConsentRefusal, DownloadRequest, Host, HostCallOptions, ImageScanResult, PendingImage, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './host/index.js';
17
+ export type { ConsentRefusal, DownloadRequest, Host, HostCallOptions, ImageScanResult, NavigateScope, PendingImage, PickedResource, ResourcePickerType, SourceImage, UploadedImage, } from './host/index.js';
18
18
  export { ApiError } from './http/index.js';
19
19
  export type { Query, QueryValue, RequestOptions } from './http/index.js';
20
20
  export { BridgeError, CivitaiError } from './core/errors.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",