@civitai/sdk 0.12.1 → 0.13.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.
@@ -9,7 +9,10 @@ export interface BlockSnapshot {
9
9
  context: BlockContext;
10
10
  token: BlockToken;
11
11
  settings: BlockSettings;
12
- /** `null` for anonymous viewers. For identity, read `/me` from the API rather than this. */
12
+ /**
13
+ * `null` for anonymous viewers. For identity, read `app.site.get('blocks/me')`
14
+ * rather than this — not `site.get('me')`, which does not accept a block token.
15
+ */
13
16
  viewer: ViewerInfo | null;
14
17
  theme: Theme;
15
18
  blockInstanceId: string;
@@ -0,0 +1,18 @@
1
+ export interface HostReply {
2
+ /** The message type the host answers with. */
3
+ readonly reply: string;
4
+ /** `legacy`: fields straight in the payload, failure as a string. */
5
+ readonly framing: 'envelope' | 'legacy';
6
+ /** The keys a success carries besides `requestId`; `null` where the host's literal could not be read. */
7
+ readonly keys: readonly string[] | null;
8
+ }
9
+ /** The civitai commit this ledger was read from. A request the host added after it is refused too. */
10
+ export declare const HOST_SNAPSHOT = "464b2023e8";
11
+ /**
12
+ * Every request the host answered at {@link HOST_SNAPSHOT}. A type absent here is refused.
13
+ * Reply names and framing are fixed at publish time: if the host renames a reply or moves a
14
+ * type to envelope framing, builds on this version stop receiving it until they upgrade.
15
+ */
16
+ export declare const HOST_REPLIES: Readonly<Record<string, HostReply>>;
17
+ /** The host's fire-and-forget messages: it handles them and never replies. */
18
+ export declare const HOST_NOTIFICATIONS: ReadonlySet<string>;
@@ -0,0 +1,66 @@
1
+ // Generated by scripts/gen-host-replies.mjs from snapshots/host-messages.json
2
+ // (civitai/civitai 464b2023e8, 2026-10-14). Do not edit.
3
+ /** The civitai commit this ledger was read from. A request the host added after it is refused too. */
4
+ export const HOST_SNAPSHOT = "464b2023e8";
5
+ /**
6
+ * Every request the host answered at {@link HOST_SNAPSHOT}. A type absent here is refused.
7
+ * Reply names and framing are fixed at publish time: if the host renames a reply or moves a
8
+ * type to envelope framing, builds on this version stop receiving it until they upgrade.
9
+ */
10
+ export const HOST_REPLIES = {
11
+ "APP_STORAGE_DELETE": { "reply": "APP_STORAGE_DELETE_RESULT", "framing": "legacy", "keys": ["deleted", "ok"] },
12
+ "APP_STORAGE_GET": { "reply": "APP_STORAGE_GET_RESULT", "framing": "legacy", "keys": ["value"] },
13
+ "APP_STORAGE_LIST": { "reply": "APP_STORAGE_LIST_RESULT", "framing": "legacy", "keys": ["keys", "nextCursor"] },
14
+ "APP_STORAGE_QUOTA": { "reply": "APP_STORAGE_QUOTA_RESULT", "framing": "legacy", "keys": ["limitBytes", "limitRows", "rowCount", "usedBytes"] },
15
+ "APP_STORAGE_SET": { "reply": "APP_STORAGE_SET_RESULT", "framing": "legacy", "keys": ["ok", "sizeBytes"] },
16
+ "CANCEL_APP_WORKFLOW": { "reply": "CANCEL_APP_WORKFLOW_RESULT", "framing": "legacy", "keys": ["result"] },
17
+ "CANCEL_WORKFLOW": { "reply": "WORKFLOW_CANCELED", "framing": "legacy", "keys": ["snapshot"] },
18
+ "CREATE_POST_FROM_APP": { "reply": "CREATE_POST_RESULT", "framing": "legacy", "keys": ["result"] },
19
+ "ESTIMATE_WORKFLOW": { "reply": "ESTIMATE_RESULT", "framing": "legacy", "keys": ["snapshot"] },
20
+ "ESTIMATE_WORKFLOW_BATCH": { "reply": "ESTIMATE_BATCH_RESULT", "framing": "legacy", "keys": ["aggregate", "snapshots"] },
21
+ "GET_BUZZ_ACCOUNTS": { "reply": "BUZZ_ACCOUNTS_RESULT", "framing": "legacy", "keys": ["result"] },
22
+ "GET_BUZZ_BALANCE": { "reply": "BUZZ_BALANCE_RESULT", "framing": "legacy", "keys": ["balance"] },
23
+ "GET_BUZZ_TRANSACTIONS": { "reply": "BUZZ_TRANSACTIONS_RESULT", "framing": "legacy", "keys": ["result"] },
24
+ "GET_DAILY_COMPENSATION": { "reply": "DAILY_COMPENSATION_RESULT", "framing": "legacy", "keys": ["result"] },
25
+ "GET_IMAGES_BY_IDS": { "reply": "IMAGES_RESULT", "framing": "legacy", "keys": ["result"] },
26
+ "GET_VIEWER": { "reply": "VIEWER_RESULT", "framing": "legacy", "keys": ["viewer"] },
27
+ "GET_WILDCARD_PACK": { "reply": "WILDCARD_PACK_RESULT", "framing": "legacy", "keys": ["pack"] },
28
+ "OPEN_BUZZ_PURCHASE": { "reply": "BUZZ_PURCHASE_RESULT", "framing": "legacy", "keys": ["purchased"] },
29
+ "OPEN_CHECKPOINT_PICKER": { "reply": "CHECKPOINT_PICKER_RESULT", "framing": "legacy", "keys": ["selected"] },
30
+ "OPEN_IMAGE_PICKER": { "reply": "IMAGE_PICKER_RESULT", "framing": "legacy", "keys": ["selected", "selectedImages"] },
31
+ "OPEN_IMAGE_UPLOAD": { "reply": "IMAGE_UPLOAD_RESULT", "framing": "legacy", "keys": ["selected"] },
32
+ "OPEN_RESOURCE_PICKER": { "reply": "RESOURCE_PICKER_RESULT", "framing": "legacy", "keys": ["selected", "selectedResources"] },
33
+ "POLL_WORKFLOW": { "reply": "WORKFLOW_STATUS", "framing": "legacy", "keys": ["snapshot"] },
34
+ "PREPARE_TRAINING_DATASET": { "reply": "TRAINING_DATASET_RESULT", "framing": "legacy", "keys": ["result"] },
35
+ "PUBLISH_GENERATION_OUTPUTS": { "reply": "PUBLISH_RESULT", "framing": "legacy", "keys": ["result"] },
36
+ "QUERY_APP_WORKFLOWS": { "reply": "APP_WORKFLOWS_RESULT", "framing": "legacy", "keys": ["result"] },
37
+ "REQUEST_TOKEN": { "reply": "TOKEN_REFRESH_RESPONSE", "framing": "legacy", "keys": ["token"] },
38
+ "RUN_CONFIRMED_STEP": { "reply": "CONFIRMED_STEP_RESULT", "framing": "legacy", "keys": ["snapshot"] },
39
+ "RUN_TRAINING": { "reply": "TRAINING_RESULT", "framing": "legacy", "keys": ["snapshot"] },
40
+ "SAVE_IMAGE": { "reply": "SAVE_IMAGE_RESULT", "framing": "legacy", "keys": ["ok"] },
41
+ "SET_COLLECTION_FOLLOW": { "reply": "COLLECTION_FOLLOW_RESULT", "framing": "legacy", "keys": ["result"] },
42
+ "SET_USER_CHECKPOINT": { "reply": "USER_CHECKPOINT_SET", "framing": "legacy", "keys": ["ok"] },
43
+ "SHARED_APPEND": { "reply": "SHARED_APPEND_RESULT", "framing": "legacy", "keys": ["key"] },
44
+ "SHARED_GET": { "reply": "SHARED_GET_RESULT", "framing": "legacy", "keys": ["item"] },
45
+ "SHARED_GET_COUNT": { "reply": "SHARED_GET_COUNT_RESULT", "framing": "legacy", "keys": ["count"] },
46
+ "SHARED_GET_COUNTS": { "reply": "SHARED_GET_COUNTS_RESULT", "framing": "legacy", "keys": ["counts"] },
47
+ "SHARED_LIST": { "reply": "SHARED_LIST_RESULT", "framing": "legacy", "keys": ["items", "nextCursor"] },
48
+ "SHARED_REPORT": { "reply": "SHARED_REPORT_RESULT", "framing": "legacy", "keys": ["ok"] },
49
+ "SHARED_UNVOTE": { "reply": "SHARED_UNVOTE_RESULT", "framing": "legacy", "keys": ["count"] },
50
+ "SHARED_UPDATE": { "reply": "SHARED_UPDATE_RESULT", "framing": "legacy", "keys": ["ok"] },
51
+ "SHARED_VOTE": { "reply": "SHARED_VOTE_RESULT", "framing": "legacy", "keys": ["count"] },
52
+ "SHARED_WITHDRAW": { "reply": "SHARED_WITHDRAW_RESULT", "framing": "legacy", "keys": ["deleted", "ok"] },
53
+ "SUBMIT_WORKFLOW": { "reply": "WORKFLOW_SUBMITTED", "framing": "legacy", "keys": ["snapshot"] },
54
+ };
55
+ /** The host's fire-and-forget messages: it handles them and never replies. */
56
+ export const HOST_NOTIFICATIONS = new Set([
57
+ "BLOCK_ERROR",
58
+ "BLOCK_HELLO",
59
+ "BLOCK_MESSAGE_REJECTED",
60
+ "BLOCK_READY",
61
+ "NAVIGATE",
62
+ "REQUEST_CONSENT",
63
+ "REQUEST_SIGN_IN",
64
+ "RESIZE_IFRAME",
65
+ "TRACK_EVENT",
66
+ ]);
@@ -1,4 +1,10 @@
1
1
  import { type BlockSnapshot, type BlockTransport, type RequestOptions } from '../transport.js';
2
+ import { BridgeError } from '../errors.js';
3
+ import { type HostReply } from './host-replies.generated.js';
4
+ /** Why `request(type)` cannot settle, and what to call instead. */
5
+ export declare function unanswerable(type: string): BridgeError;
6
+ /** Turns a reply into the value it carries, or the failure it reports. */
7
+ export declare function unwrap(type: string, reply: unknown, expected: HostReply): unknown;
2
8
  export interface IframeTransportOptions {
3
9
  /** At least one entry. Other origins — including the local one — drop silently. */
4
10
  allowedParentOrigins: string[];
@@ -2,48 +2,63 @@ import { isHostMessage, parseBlockInitFragment, stripBlockInitFragment, } from '
2
2
  import { EMPTY_SNAPSHOT, snapshotFromInit, tokenFromWrapped, } from '../transport.js';
3
3
  import { BridgeError, classifyHostError } from '../errors.js';
4
4
  import { OriginMatcher } from '../origin-matcher.js';
5
+ import { HOST_NOTIFICATIONS, HOST_REPLIES, HOST_SNAPSHOT } from './host-replies.generated.js';
5
6
  import { VERSION } from '../../version.generated.js';
6
7
  /**
7
8
  * The framing the host had before this package owned the protocol: fields
8
9
  * straight in the payload, and failure as the server's own sentence. Converting
9
10
  * it here is what keeps the rest of this file — and every domain — speaking one
10
- * shape; when the host modernises a message, its domain drops the declaration
11
- * and this stops being reached.
11
+ * shape; when the host modernises a message, the snapshot reports it as
12
+ * `envelope` and this stops being reached for it.
12
13
  */
13
- function fromLegacyReply(reply) {
14
+ function fromLegacyReply(reply, keys) {
14
15
  const { requestId, error, ...fields } = reply;
15
16
  // An empty string is how the host spells "no failure" on these replies.
16
17
  if (typeof error === 'string' && error !== '') {
17
18
  return { requestId, error: { code: classifyHostError(error), message: error } };
18
19
  }
19
20
  // Some of these replies nest their value under `result` and others put its
20
- // fields straight in the payload; the host was never consistent about it.
21
- return { requestId, result: 'result' in fields ? fields.result : fields };
21
+ // fields straight in the payload. The snapshot says which; where the host's
22
+ // reply could not be read (`keys` null), the payload itself decides.
23
+ const nested = keys === null ? 'result' in fields : keys.length === 1 && keys[0] === 'result';
24
+ return { requestId, result: nested ? fields.result : fields };
22
25
  }
23
26
  /**
24
- * The host's pre-protocol messages, each with the reply it answers with —
25
- * `<REQUEST>_RESULT` holds across most of them but not all. It is the migration
26
- * ledger for this protocol: an entry leaves the day the host modernises that
27
- * reply, and when it empties `fromLegacyReply` goes with it.
27
+ * Where a caller should go instead of a bridge message, from BREAKING.md. Only
28
+ * read when the host does not answer the type, so it is what a request the
29
+ * host dropped (or never had) says instead of hanging.
28
30
  */
29
- const LEGACY_REPLIES = {
30
- OPEN_BUZZ_PURCHASE: 'BUZZ_PURCHASE_RESULT',
31
- OPEN_IMAGE_UPLOAD: 'IMAGE_UPLOAD_RESULT',
32
- OPEN_RESOURCE_PICKER: 'RESOURCE_PICKER_RESULT',
33
- PUBLISH_GENERATION_OUTPUTS: 'PUBLISH_RESULT',
34
- REQUEST_TOKEN: 'TOKEN_REFRESH_RESPONSE',
35
- SAVE_IMAGE: 'SAVE_IMAGE_RESULT',
36
- };
37
- function replyTypeFor(type) {
38
- return LEGACY_REPLIES[type] ?? `${type}_RESULT`;
31
+ const REPLACED_BY = [
32
+ [/^GET_VIEWER$/, "app.site.get('blocks/me')"],
33
+ [/^(SUBMIT|ESTIMATE|POLL|CANCEL)_WORKFLOW$|^QUERY_APP_WORKFLOWS$|^CANCEL_APP_WORKFLOW$/, 'app.workflows'],
34
+ [/^GET_IMAGES_BY_IDS$/, 'GET /api/v1/blocks/images?ids=… (app.site)'],
35
+ [/^APP_STORAGE_/, 'POST /api/v1/blocks/app-storage/* (app.site)'],
36
+ [/^SHARED_/, 'app.sharedStorage'],
37
+ [/^GET_BUZZ_BALANCE$/, "app.site.get('blocks/buzz')"],
38
+ [/^SET_COLLECTION_FOLLOW$/, 'POST /api/v1/blocks/collections/{id}/follow (app.site)'],
39
+ [/^TRACK_EVENT$/, 'app.host.track(eventName, properties)'],
40
+ [/^PUBLISH_GENERATION_OUTPUTS$/, 'app.host.publishGenerationOutputs'],
41
+ ];
42
+ /** Why `request(type)` cannot settle, and what to call instead. */
43
+ export function unanswerable(type) {
44
+ const instead = REPLACED_BY.find(([pattern]) => pattern.test(type))?.[1];
45
+ const message = HOST_NOTIFICATIONS.has(type)
46
+ ? `${type} is fire-and-forget: the host never answers it, so request() would wait forever. ` +
47
+ `Send it with notify()${instead ? `, or use ${instead}` : ''}.`
48
+ : `${type} is not a request the host answered at civitai ${HOST_SNAPSHOT}, ` +
49
+ `the host snapshot @civitai/sdk ${VERSION} was built from, so request() would wait forever. ` +
50
+ (instead
51
+ ? `Use ${instead} instead.`
52
+ : 'If the host has added it since, upgrade @civitai/sdk; otherwise see BREAKING.md for where bridge messages moved.');
53
+ return new BridgeError('invalid', type, message);
39
54
  }
40
55
  /** Turns a reply into the value it carries, or the failure it reports. */
41
- function unwrap(type, reply, framing) {
56
+ export function unwrap(type, reply, expected) {
42
57
  if (reply == null || typeof reply !== 'object') {
43
58
  throw new BridgeError('malformed', type, `${type} replied with no payload`);
44
59
  }
45
- const envelope = framing === 'legacy'
46
- ? fromLegacyReply(reply)
60
+ const envelope = expected.framing === 'legacy'
61
+ ? fromLegacyReply(reply, expected.keys)
47
62
  : reply;
48
63
  if (envelope.error !== undefined) {
49
64
  throw new BridgeError(envelope.error.code, type, envelope.error.message || `${type} failed`);
@@ -187,11 +202,14 @@ export class IframeTransport {
187
202
  this.#dispatch(message.type, message.payload);
188
203
  }
189
204
  async request(type, params, opts = {}) {
190
- const framing = type in LEGACY_REPLIES ? 'legacy' : 'envelope';
191
- return unwrap(type, await this.#exchange(type, params, opts), framing);
205
+ // Own keys only: `HOST_REPLIES['toString']` must not read as an answer.
206
+ const expected = Object.hasOwn(HOST_REPLIES, type) ? HOST_REPLIES[type] : undefined;
207
+ // Nothing would ever answer it, so say so now rather than wait forever.
208
+ if (!expected)
209
+ throw unanswerable(type);
210
+ return unwrap(type, await this.#exchange(type, expected.reply, params, opts), expected);
192
211
  }
193
- #exchange(type, params, opts) {
194
- const responseType = replyTypeFor(type);
212
+ #exchange(type, responseType, params, opts) {
195
213
  const { signal } = opts;
196
214
  if (signal?.aborted)
197
215
  return Promise.reject(signal.reason);
@@ -60,10 +60,12 @@ export function createHost(transport) {
60
60
  transport,
61
61
  });
62
62
  // The host spells "no failure" as `error: ''`, which the transport reads
63
- // as a success — so a reply carrying neither the ids nor a message gets
64
- // this far, and returning it would resolve `undefined` from a promise
65
- // that says `number[]`. A caller reading that as "nothing published"
66
- // would be guessing: images may well have been.
63
+ // as a success. A reply with no `result` at all is refused by the
64
+ // transport (the ledger says this reply nests under `result`), but one
65
+ // whose `result` lacks the ids gets this far, and returning it would
66
+ // resolve `undefined` from a promise that says `number[]`. A caller
67
+ // reading that as "nothing published" would be guessing: images may well
68
+ // have been.
67
69
  if (!Array.isArray(imageIds) || !imageIds.every((id) => typeof id === 'number')) {
68
70
  throw new BridgeError('malformed', 'PUBLISH_GENERATION_OUTPUTS', 'PUBLISH_GENERATION_OUTPUTS reply carried no image ids. Whether anything ' +
69
71
  'was published is unknown; read the workflow back rather than assuming.');
@@ -1,5 +1,5 @@
1
1
  /** Every scope an app can hold, as the site names them. */
2
- export declare const SCOPES: readonly ['ai:write:budgeted', 'apps:storage:read', 'apps:storage:shared:read', 'apps:storage:shared:write', 'apps:storage:write', 'buzz:read:self', 'collections:read:private', 'collections:read:self', 'collections:write:self', 'models:read:self', 'posts:write:self', 'social:tip:self', 'user:read:self'];
2
+ export declare const SCOPES: readonly ['ai:write:budgeted', 'apps:storage', 'apps:storage:publish', 'apps:storage:read', 'apps:storage:shared:read', 'apps:storage:shared:write', 'apps:storage:write', 'apps:store:items:write', 'buzz:read:self', 'collections:read:private', 'collections:read:self', 'collections:write:self', 'goods:purchase:self', 'goods:read:self', 'models:read:self', 'posts:write:self', 'social:tip:self', 'user:read:self'];
3
3
  export type Scope = (typeof SCOPES)[number];
4
4
  export interface TokenOptions {
5
5
  /** Skip the held token and get a new one, e.g. after the API refused it. */
@@ -1,14 +1,19 @@
1
1
  /** Every scope an app can hold, as the site names them. */
2
2
  export const SCOPES = [
3
3
  'ai:write:budgeted',
4
+ 'apps:storage',
5
+ 'apps:storage:publish',
4
6
  'apps:storage:read',
5
7
  'apps:storage:shared:read',
6
8
  'apps:storage:shared:write',
7
9
  'apps:storage:write',
10
+ 'apps:store:items:write',
8
11
  'buzz:read:self',
9
12
  'collections:read:private',
10
13
  'collections:read:self',
11
14
  'collections:write:self',
15
+ 'goods:purchase:self',
16
+ 'goods:read:self',
12
17
  'models:read:self',
13
18
  'posts:write:self',
14
19
  'social:tip:self',
@@ -1,2 +1,2 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.12.1";
2
+ export declare const VERSION = "0.13.0";
@@ -1,3 +1,3 @@
1
1
  /* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
2
2
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
3
- export const VERSION = "0.12.1";
3
+ export const VERSION = "0.13.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/sdk",
3
- "version": "0.12.1",
3
+ "version": "0.13.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",
@@ -69,6 +69,8 @@
69
69
  "gen:steps": "node scripts/gen-step-types.mjs",
70
70
  "check:steps": "node scripts/gen-step-types.mjs --check",
71
71
  "gen:workflow-types": "node scripts/gen-workflow-types.mjs",
72
+ "gen:host-replies": "node scripts/gen-host-replies.mjs",
73
+ "check:host-replies": "node scripts/gen-host-replies.mjs --check",
72
74
  "snapshot:host": "node scripts/snapshot-host-messages.mjs",
73
75
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json && tsc -p tsconfig.examples.json",
74
76
  "test": "vitest run",