@civitai/blocks-react 0.71.0 → 0.72.2

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.
Files changed (71) hide show
  1. package/README.md +308 -15
  2. package/dist/hooks/useAppStorage.js +2 -0
  3. package/dist/hooks/useAppWorkflows.js +2 -0
  4. package/dist/hooks/useBatchEstimate.d.ts +4 -0
  5. package/dist/hooks/useBatchEstimate.js +6 -0
  6. package/dist/hooks/useBlockAnalytics.js +2 -0
  7. package/dist/hooks/useBlockBreakpoint.js +2 -0
  8. package/dist/hooks/useBlockContext.js +2 -0
  9. package/dist/hooks/useBlockResize.js +2 -0
  10. package/dist/hooks/useBlockSettings.js +2 -0
  11. package/dist/hooks/useBlockTheme.js +2 -0
  12. package/dist/hooks/useBlockToken.js +2 -0
  13. package/dist/hooks/useBuzzAccounts.js +2 -0
  14. package/dist/hooks/useBuzzBalance.js +2 -0
  15. package/dist/hooks/useBuzzPurchase.js +2 -0
  16. package/dist/hooks/useBuzzTransactions.js +2 -0
  17. package/dist/hooks/useBuzzWorkflow.d.ts +35 -3
  18. package/dist/hooks/useBuzzWorkflow.js +32 -3
  19. package/dist/hooks/useCheckpointPicker.d.ts +2 -0
  20. package/dist/hooks/useCheckpointPicker.js +4 -0
  21. package/dist/hooks/useCivitaiNavigate.js +2 -0
  22. package/dist/hooks/useCivitaiRoute.js +2 -0
  23. package/dist/hooks/useCollectionFollow.js +2 -0
  24. package/dist/hooks/useConsentUnavailable.js +2 -0
  25. package/dist/hooks/useCreatePostFromApp.js +2 -0
  26. package/dist/hooks/useDailyCompensation.js +2 -0
  27. package/dist/hooks/useDirectLoad.js +2 -0
  28. package/dist/hooks/useDomainMaturity.js +2 -0
  29. package/dist/hooks/useEntitlements.js +2 -0
  30. package/dist/hooks/useGatedImages.js +2 -0
  31. package/dist/hooks/useGenerationResources.js +2 -0
  32. package/dist/hooks/useGoodPurchase.js +2 -0
  33. package/dist/hooks/useHostOrigin.js +2 -0
  34. package/dist/hooks/useImageUpload.js +2 -0
  35. package/dist/hooks/useNestedDocument.js +2 -0
  36. package/dist/hooks/usePrepareTrainingDataset.js +2 -0
  37. package/dist/hooks/usePublishGenerationOutputs.js +2 -0
  38. package/dist/hooks/useRequestConsent.js +2 -0
  39. package/dist/hooks/useRequestSignIn.js +2 -0
  40. package/dist/hooks/useResourcePicker.js +2 -0
  41. package/dist/hooks/useRunConfirmedStep.d.ts +209 -0
  42. package/dist/hooks/useRunConfirmedStep.js +289 -0
  43. package/dist/hooks/useRunTraining.js +2 -0
  44. package/dist/hooks/useSaveImage.js +2 -0
  45. package/dist/hooks/useSharedStorage.js +2 -0
  46. package/dist/hooks/useTip.js +2 -0
  47. package/dist/hooks/useTipAllowance.js +2 -0
  48. package/dist/hooks/useTransportConnection.d.ts +43 -0
  49. package/dist/hooks/useTransportConnection.js +59 -0
  50. package/dist/hooks/useUploadImageBytes.js +2 -0
  51. package/dist/hooks/useViewer.js +2 -0
  52. package/dist/hooks/useWildcardPack.js +2 -0
  53. package/dist/hooks/useWorkflowBatch.js +2 -0
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +1 -0
  56. package/dist/internal/hostSurface.d.ts +37 -0
  57. package/dist/internal/hostSurface.js +38 -0
  58. package/dist/internal/liveHost.d.ts +15 -6
  59. package/dist/internal/liveHost.js +151 -69
  60. package/dist/internal/mockHost.d.ts +123 -10
  61. package/dist/internal/mockHost.js +377 -14
  62. package/dist/internal/mockWorkflowGraph.d.ts +23 -10
  63. package/dist/internal/mockWorkflowGraph.js +34 -11
  64. package/dist/testing.d.ts +1 -1
  65. package/dist/transport/iframeTransport.js +13 -0
  66. package/dist/transport/requestTimeouts.js +6 -0
  67. package/dist/transport/singleton.d.ts +6 -0
  68. package/dist/transport/singleton.js +20 -0
  69. package/dist/transport/validate.d.ts +13 -0
  70. package/dist/transport/validate.js +76 -0
  71. package/package.json +4 -4
@@ -0,0 +1,209 @@
1
+ import type { BlockRunConfirmedStepHostError, BlockStepQuote, BlockWorkflowSnapshot, WorkflowBodyPassThroughStep } from '@civitai/app-sdk/blocks';
2
+ export type { BlockRunConfirmedStepHostError, BlockStepQuote, WorkflowBodyPassThroughStep };
3
+ /**
4
+ * A pass-through step body ready to RUN as a confirmed job:
5
+ * {@link WorkflowBodyPassThroughStep} with the `quoteId` its estimate returned
6
+ * (`snapshot.stepQuote.quoteId`). Required here so a body without one is a
7
+ * compile error rather than a refusal.
8
+ */
9
+ export type QuotedStepBody = WorkflowBodyPassThroughStep & {
10
+ quoteId: string;
11
+ };
12
+ /**
13
+ * The host's CLOSED refusal codes for `RUN_CONFIRMED_STEP`, as a runtime list.
14
+ * Mirrors civitai/civitai's `RUN_CONFIRMED_STEP_HOST_ERRORS`.
15
+ *
16
+ * 🔴 THE ERROR CHANNEL IS NOT AN ENUM: server messages (an expired or used quote,
17
+ * a body or price that differs from the quoted one) arrive in the same field —
18
+ * see {@link isRunConfirmedStepErrorCode}.
19
+ */
20
+ export declare const RUN_CONFIRMED_STEP_ERROR_CODES: readonly ["review-mode", "block is not ready", "sign in to run", "invalid confirmed step request", "no block token", "declined", "daily-limit-exceeded", "submission-unconfirmed"];
21
+ /**
22
+ * The shape of a confirmed-step quote id: `sq_` + 32 lowercase hex characters.
23
+ *
24
+ * A COPY of `@civitai/app-sdk/blocks`'s `BLOCK_STEP_QUOTE_ID_REGEX`, deliberately
25
+ * not value-imported: a value import would raise this package's
26
+ * `@civitai/app-sdk` peer floor for one regular expression (see
27
+ * `PEER_FLOOR.md`). Pinned to the SDK's by `test/confirmedStepFlow.test.tsx`,
28
+ * which imports both.
29
+ */
30
+ export declare const STEP_QUOTE_ID_PATTERN: RegExp;
31
+ /** `true` when `value` is a string shaped like a confirmed-step quote id. */
32
+ export declare function isStepQuoteId(value: unknown): value is string;
33
+ /**
34
+ * The message of the {@link RunConfirmedStepError} `runConfirmedStep` rejects
35
+ * with when the body carries no `sq_`-shaped `quoteId`.
36
+ *
37
+ * 🔴 REFUSED BEFORE THE SEND. The host refuses the same body with `invalid
38
+ * confirmed step request`, so nothing is lost by refusing here — and one thing is
39
+ * gained: a `tq_` training quote, an `undefined` from an estimate that returned
40
+ * no `stepQuote`, or a stale id from storage is named at the call site instead of
41
+ * surfacing as a host refusal. Nothing was sent, so nothing was charged.
42
+ */
43
+ export declare const CONFIRMED_STEP_QUOTE_ID_REFUSAL = "runConfirmedStep() needs the quoteId its estimate returned (snapshot.stepQuote.quoteId, \"sq_\" + 32 hex characters) \u2014 estimate the body with useBuzzWorkflow().estimate() first. Nothing was sent.";
44
+ /**
45
+ * The `code` a {@link RunConfirmedStepError} can carry: one of the host's closed
46
+ * refusal codes, or `'refused'` — assigned by THIS SDK, never sent by the host,
47
+ * for a submit the server refused at a spend cap or as temporarily unavailable
48
+ * (see {@link RunConfirmedStepError.refused}).
49
+ */
50
+ export type RunConfirmedStepErrorCode = BlockRunConfirmedStepHostError | 'refused';
51
+ /** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
52
+ export declare function isRunConfirmedStepErrorCode(error: string): error is BlockRunConfirmedStepHostError;
53
+ /**
54
+ * A confirmed step that did not come back with a snapshot.
55
+ *
56
+ * 🔴 READ THE FLAGS BEFORE SAYING ANYTHING ABOUT MONEY — they are the only part of
57
+ * this error with a money meaning, and they point in opposite directions:
58
+ *
59
+ * - `.declined` — the viewer dismissed the confirm. NO JOB WAS SUBMITTED,
60
+ * guaranteed by the host's consent latch, and the quote is still runnable
61
+ * until it expires. Render nothing.
62
+ * - `.dailyLimitExceeded` — the job would exceed what remains today of the
63
+ * daily Buzz limit the VIEWER set for this app. NO JOB, nothing charged. The
64
+ * host has already explained it to the viewer and pointed at where the limit
65
+ * is raised; a confirm cannot override it. Do not retry, and do not offer a
66
+ * Buzz purchase — more Buzz does not lift a limit.
67
+ * - `.unconfirmed` — the job MAY BE RUNNING AND CHARGED: the host sent the
68
+ * submit and could not learn its outcome (`submission-unconfirmed`), or no
69
+ * reply arrived at all (`.timedOut` — the viewer may have confirmed a dialog
70
+ * this block had stopped waiting for). 🔴 NEVER RETRY AUTOMATICALLY and do not
71
+ * tell the viewer it failed. Every confirmation is its own job, so a retry —
72
+ * which needs a new estimate and a new confirmation — is a SECOND, separately
73
+ * charged job if the first one did start. Check the viewer's generations
74
+ * first (`useAppWorkflows()` lists this app's runs).
75
+ * - `.refused` (`code: 'refused'`) — the server REFUSED the submit at a spend
76
+ * cap after the viewer confirmed (the viewer's daily or private-run Buzz cap,
77
+ * the app's daily spend or rate limit). Its reservation was refunded: NO JOB,
78
+ * nothing charged. `.message` is the server's reason and `.snapshot` the
79
+ * refusal it arrived in. Retrying needs a new estimate and usually a later
80
+ * time.
81
+ * - `.signInRequired` — no session. Route into `useRequestSignIn()`.
82
+ * - anything else — a refusal before any submit (an expired or used quote, a
83
+ * body that no longer matches its quote, a price that changed, or the
84
+ * client-side {@link CONFIRMED_STEP_QUOTE_ID_REFUSAL}). Estimate again before
85
+ * retrying; `.message` is the host's or server's text.
86
+ */
87
+ export declare class RunConfirmedStepError extends Error {
88
+ /**
89
+ * The closed host refusal code, `'refused'` for a cap refusal, or `undefined`
90
+ * for a server/transport error.
91
+ */
92
+ readonly code?: RunConfirmedStepErrorCode;
93
+ /**
94
+ * The server refused the submit (a spend cap, or a temporary-availability deny)
95
+ * and refunded it — no job exists. The quote is used up: estimate again.
96
+ * See the class docs; `.message` is the server's reason.
97
+ */
98
+ readonly refused: boolean;
99
+ /** The refusal snapshot the host replied with, when {@link refused}. */
100
+ readonly snapshot?: BlockWorkflowSnapshot;
101
+ /** The viewer dismissed the confirm — no job was submitted. */
102
+ readonly declined: boolean;
103
+ /**
104
+ * The job would exceed the viewer's own daily Buzz limit for this app — no job
105
+ * was submitted and nothing was charged. Set for the host code
106
+ * `daily-limit-exceeded` and for a refusal snapshot whose `errorCode` is that
107
+ * value.
108
+ */
109
+ readonly dailyLimitExceeded: boolean;
110
+ /**
111
+ * The job may exist and may have been charged — `submission-unconfirmed`, or a
112
+ * transport timeout. Check the viewer's generations before retrying.
113
+ */
114
+ readonly unconfirmed: boolean;
115
+ /** No reply arrived within the 10-minute consent bound. Implies {@link unconfirmed}. */
116
+ readonly timedOut: boolean;
117
+ /** There is no session (`sign in to run`, or a bare `UNAUTHORIZED`). */
118
+ readonly signInRequired: boolean;
119
+ constructor(error: string, opts?: {
120
+ timedOut?: boolean;
121
+ refusedSnapshot?: BlockWorkflowSnapshot;
122
+ code?: BlockRunConfirmedStepHostError;
123
+ });
124
+ }
125
+ /** What {@link useRunConfirmedStep} returns. */
126
+ export interface UseRunConfirmedStep {
127
+ /**
128
+ * Ask the host to run a quoted pass-through step. Civitai shows the viewer a
129
+ * confirm with the server's price; on their click it submits, and this
130
+ * resolves with the submitted workflow's snapshot — poll it with
131
+ * `useBuzzWorkflow().watch(snapshot.workflowId)`.
132
+ *
133
+ * REJECTS with a {@link RunConfirmedStepError} — including `declined` and
134
+ * `dailyLimitExceeded` (no job), `refused` (a spend-cap refusal; no job) and
135
+ * `unconfirmed` (a job may exist). Read the flags before rendering anything. A
136
+ * resolved snapshot is always a submitted job with a pollable `workflowId`.
137
+ *
138
+ * A body whose `quoteId` is missing or not `sq_`-shaped is refused HERE,
139
+ * before anything is sent — see {@link CONFIRMED_STEP_QUOTE_ID_REFUSAL}.
140
+ */
141
+ runConfirmedStep: (body: QuotedStepBody) => Promise<BlockWorkflowSnapshot>;
142
+ /** `true` while a request is in flight (including the viewer's dialog). */
143
+ pending: boolean;
144
+ /** The last request's failure, or `null`. Cleared at the start of the next call. */
145
+ error: RunConfirmedStepError | null;
146
+ }
147
+ /**
148
+ * Run ONE pass-through `kind: 'step'` job whose price is above what the app may
149
+ * spend without asking — through the host-mediated `RUN_CONFIRMED_STEP` →
150
+ * `CONFIRMED_STEP_RESULT` bridge, after the viewer confirms the exact price in
151
+ * Civitai's own dialog.
152
+ *
153
+ * 🔴 AVAILABILITY: page apps only, behind the host flag
154
+ * `app-blocks-confirmed-step` (ships off), with `ai:write:budgeted` declared and
155
+ * granted by a signed-in viewer, on a host carrying civitai/civitai#5786. Refused
156
+ * from `dev:live` and from review sessions — build the flow against the mock
157
+ * host. Where it is unavailable the estimate simply carries no `stepQuote`.
158
+ *
159
+ * The flow: `useBuzzWorkflow().estimate(body)` (its `stepQuote.quoteId`) →
160
+ * `runConfirmedStep({ ...body, quoteId })` → `useBuzzWorkflow().watch(workflowId)`.
161
+ * Change nothing in the body between the estimate and the run: the server checks
162
+ * it against the quoted one.
163
+ *
164
+ * 🔴 ONLY A FIXED PRICE IS CONFIRMABLE. `stepQuote` is present when the
165
+ * orchestrator quoted a fixed price above the token's per-call budget and at
166
+ * most `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB` (5,000 Buzz). A variable or
167
+ * metered price, or no price at all, gets no quote and cannot be run this way.
168
+ *
169
+ * 🔴 ESTIMATE THE BODY ON ITS OWN. A batch estimate (`useBatchEstimate()`) never
170
+ * carries a `stepQuote`. And where a quote would be granted, the estimate
171
+ * REJECTS instead for a training `$type` (`training` / `imageResourceTraining`,
172
+ * any casing — use `kind: 'training'`) and for an `input` that is not plain JSON
173
+ * (a `Date`, `Map`, `Set`, `BigInt`, `NaN`, an empty slot in a sparse array, …). Both are server messages with no code; see
174
+ * `BlockWorkflowSnapshot.stepQuote` for the exact text.
175
+ *
176
+ * 🔴 THE CONFIRM IS CIVITAI'S, AND IT IS THE CHARGE. The price on it is read back
177
+ * from the server's stored quote — never from your body. A quote is single-use
178
+ * and expires 15 minutes after the estimate; the server prices the job again at
179
+ * submit and refuses a higher price.
180
+ *
181
+ * 🔴 NO AUTOMATIC RETRY OF ANY KIND, on purpose. Every confirmation is its own
182
+ * job — there is no idempotency key, the orchestrator id comes from the confirmed
183
+ * quote — and one outcome (`submission-unconfirmed`) means a job may already be
184
+ * running. So this hook never re-sends, and neither should you: a retry is a
185
+ * NEW, separately charged job.
186
+ *
187
+ * Sent under the 10-minute human-interaction bound: the host replies only when
188
+ * the viewer clicks or dismisses its dialog.
189
+ *
190
+ * @example
191
+ * const { estimate, watch } = useBuzzWorkflow();
192
+ * const { runConfirmedStep } = useRunConfirmedStep();
193
+ * const quote = (await estimate(body)).stepQuote;
194
+ * if (!quote) return submit(body); // within budget — the ordinary path
195
+ * try {
196
+ * const started = await runConfirmedStep({ ...body, quoteId: quote.quoteId });
197
+ * await watch(started.workflowId, { onUpdate: render });
198
+ * } catch (e) {
199
+ * if (!(e instanceof RunConfirmedStepError)) throw e;
200
+ * if (e.declined) return; // no job — say nothing
201
+ * if (e.dailyLimitExceeded) return; // no job — Civitai already told the viewer
202
+ * if (e.signInRequired) return requestSignIn();
203
+ * if (e.unconfirmed) return showCheckYourGenerations(); // may be running — never auto-retry
204
+ * if (e.refused) return showError(e.message); // a cap refusal; no job, nothing charged
205
+ * showError('Could not start this job. Get a new price and try again.');
206
+ * }
207
+ */
208
+ export declare function useRunConfirmedStep(): UseRunConfirmedStep;
209
+ //# sourceMappingURL=useRunConfirmedStep.d.ts.map
@@ -0,0 +1,289 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
3
+ import { getTransport } from '../transport/singleton.js';
4
+ import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
5
+ import { useTransportConnection } from './useTransportConnection.js';
6
+ /**
7
+ * The host's CLOSED refusal codes for `RUN_CONFIRMED_STEP`, as a runtime list.
8
+ * Mirrors civitai/civitai's `RUN_CONFIRMED_STEP_HOST_ERRORS`.
9
+ *
10
+ * 🔴 THE ERROR CHANNEL IS NOT AN ENUM: server messages (an expired or used quote,
11
+ * a body or price that differs from the quoted one) arrive in the same field —
12
+ * see {@link isRunConfirmedStepErrorCode}.
13
+ */
14
+ export const RUN_CONFIRMED_STEP_ERROR_CODES = [
15
+ 'review-mode',
16
+ 'block is not ready',
17
+ 'sign in to run',
18
+ 'invalid confirmed step request',
19
+ 'no block token',
20
+ 'declined',
21
+ 'daily-limit-exceeded',
22
+ 'submission-unconfirmed',
23
+ ];
24
+ const CODE_SET = new Set(RUN_CONFIRMED_STEP_ERROR_CODES);
25
+ /**
26
+ * The shape of a confirmed-step quote id: `sq_` + 32 lowercase hex characters.
27
+ *
28
+ * A COPY of `@civitai/app-sdk/blocks`'s `BLOCK_STEP_QUOTE_ID_REGEX`, deliberately
29
+ * not value-imported: a value import would raise this package's
30
+ * `@civitai/app-sdk` peer floor for one regular expression (see
31
+ * `PEER_FLOOR.md`). Pinned to the SDK's by `test/confirmedStepFlow.test.tsx`,
32
+ * which imports both.
33
+ */
34
+ export const STEP_QUOTE_ID_PATTERN = /^sq_[a-f0-9]{32}$/;
35
+ /** `true` when `value` is a string shaped like a confirmed-step quote id. */
36
+ export function isStepQuoteId(value) {
37
+ return typeof value === 'string' && STEP_QUOTE_ID_PATTERN.test(value);
38
+ }
39
+ /**
40
+ * The message of the {@link RunConfirmedStepError} `runConfirmedStep` rejects
41
+ * with when the body carries no `sq_`-shaped `quoteId`.
42
+ *
43
+ * 🔴 REFUSED BEFORE THE SEND. The host refuses the same body with `invalid
44
+ * confirmed step request`, so nothing is lost by refusing here — and one thing is
45
+ * gained: a `tq_` training quote, an `undefined` from an estimate that returned
46
+ * no `stepQuote`, or a stale id from storage is named at the call site instead of
47
+ * surfacing as a host refusal. Nothing was sent, so nothing was charged.
48
+ */
49
+ export const CONFIRMED_STEP_QUOTE_ID_REFUSAL = 'runConfirmedStep() needs the quoteId its estimate returned (snapshot.stepQuote.quoteId, "sq_" + 32 hex characters) — estimate the body with useBuzzWorkflow().estimate() first. Nothing was sent.';
50
+ /**
51
+ * The `workflowId` the host's confirmed submit stamps on a snapshot that is NOT
52
+ * a run — its cap refusals resolve `{ workflowId: 'failed', status: 'failed',
53
+ * cost, error }`, the same shape the training submit uses. Compared with `===`:
54
+ * a real orchestrator id is a workflow that may exist.
55
+ */
56
+ const HOST_REFUSAL_WORKFLOW_ID = 'failed';
57
+ /** The viewer's own daily-limit refusal, as a host code and as a snapshot `errorCode`. */
58
+ const DAILY_LIMIT_CODE = 'daily-limit-exceeded';
59
+ /** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
60
+ export function isRunConfirmedStepErrorCode(error) {
61
+ return CODE_SET.has(error);
62
+ }
63
+ /**
64
+ * A confirmed step that did not come back with a snapshot.
65
+ *
66
+ * 🔴 READ THE FLAGS BEFORE SAYING ANYTHING ABOUT MONEY — they are the only part of
67
+ * this error with a money meaning, and they point in opposite directions:
68
+ *
69
+ * - `.declined` — the viewer dismissed the confirm. NO JOB WAS SUBMITTED,
70
+ * guaranteed by the host's consent latch, and the quote is still runnable
71
+ * until it expires. Render nothing.
72
+ * - `.dailyLimitExceeded` — the job would exceed what remains today of the
73
+ * daily Buzz limit the VIEWER set for this app. NO JOB, nothing charged. The
74
+ * host has already explained it to the viewer and pointed at where the limit
75
+ * is raised; a confirm cannot override it. Do not retry, and do not offer a
76
+ * Buzz purchase — more Buzz does not lift a limit.
77
+ * - `.unconfirmed` — the job MAY BE RUNNING AND CHARGED: the host sent the
78
+ * submit and could not learn its outcome (`submission-unconfirmed`), or no
79
+ * reply arrived at all (`.timedOut` — the viewer may have confirmed a dialog
80
+ * this block had stopped waiting for). 🔴 NEVER RETRY AUTOMATICALLY and do not
81
+ * tell the viewer it failed. Every confirmation is its own job, so a retry —
82
+ * which needs a new estimate and a new confirmation — is a SECOND, separately
83
+ * charged job if the first one did start. Check the viewer's generations
84
+ * first (`useAppWorkflows()` lists this app's runs).
85
+ * - `.refused` (`code: 'refused'`) — the server REFUSED the submit at a spend
86
+ * cap after the viewer confirmed (the viewer's daily or private-run Buzz cap,
87
+ * the app's daily spend or rate limit). Its reservation was refunded: NO JOB,
88
+ * nothing charged. `.message` is the server's reason and `.snapshot` the
89
+ * refusal it arrived in. Retrying needs a new estimate and usually a later
90
+ * time.
91
+ * - `.signInRequired` — no session. Route into `useRequestSignIn()`.
92
+ * - anything else — a refusal before any submit (an expired or used quote, a
93
+ * body that no longer matches its quote, a price that changed, or the
94
+ * client-side {@link CONFIRMED_STEP_QUOTE_ID_REFUSAL}). Estimate again before
95
+ * retrying; `.message` is the host's or server's text.
96
+ */
97
+ export class RunConfirmedStepError extends Error {
98
+ /**
99
+ * The closed host refusal code, `'refused'` for a cap refusal, or `undefined`
100
+ * for a server/transport error.
101
+ */
102
+ code;
103
+ /**
104
+ * The server refused the submit (a spend cap, or a temporary-availability deny)
105
+ * and refunded it — no job exists. The quote is used up: estimate again.
106
+ * See the class docs; `.message` is the server's reason.
107
+ */
108
+ refused;
109
+ /** The refusal snapshot the host replied with, when {@link refused}. */
110
+ snapshot;
111
+ /** The viewer dismissed the confirm — no job was submitted. */
112
+ declined;
113
+ /**
114
+ * The job would exceed the viewer's own daily Buzz limit for this app — no job
115
+ * was submitted and nothing was charged. Set for the host code
116
+ * `daily-limit-exceeded` and for a refusal snapshot whose `errorCode` is that
117
+ * value.
118
+ */
119
+ dailyLimitExceeded;
120
+ /**
121
+ * The job may exist and may have been charged — `submission-unconfirmed`, or a
122
+ * transport timeout. Check the viewer's generations before retrying.
123
+ */
124
+ unconfirmed;
125
+ /** No reply arrived within the 10-minute consent bound. Implies {@link unconfirmed}. */
126
+ timedOut;
127
+ /** There is no session (`sign in to run`, or a bare `UNAUTHORIZED`). */
128
+ signInRequired;
129
+ constructor(error, opts) {
130
+ super(error);
131
+ this.name = 'RunConfirmedStepError';
132
+ this.timedOut = opts?.timedOut === true;
133
+ this.refused = opts?.refusedSnapshot !== undefined;
134
+ if (opts?.refusedSnapshot !== undefined) {
135
+ this.code = 'refused';
136
+ this.snapshot = opts.refusedSnapshot;
137
+ }
138
+ else if (opts?.code !== undefined) {
139
+ this.code = opts.code;
140
+ }
141
+ else if (isRunConfirmedStepErrorCode(error)) {
142
+ this.code = error;
143
+ }
144
+ this.declined = error === 'declined';
145
+ this.dailyLimitExceeded =
146
+ error === DAILY_LIMIT_CODE || opts?.refusedSnapshot?.errorCode === DAILY_LIMIT_CODE;
147
+ this.unconfirmed = this.timedOut || error === 'submission-unconfirmed';
148
+ this.signInRequired = error === 'sign in to run' || error === 'UNAUTHORIZED';
149
+ }
150
+ }
151
+ /**
152
+ * Run ONE pass-through `kind: 'step'` job whose price is above what the app may
153
+ * spend without asking — through the host-mediated `RUN_CONFIRMED_STEP` →
154
+ * `CONFIRMED_STEP_RESULT` bridge, after the viewer confirms the exact price in
155
+ * Civitai's own dialog.
156
+ *
157
+ * 🔴 AVAILABILITY: page apps only, behind the host flag
158
+ * `app-blocks-confirmed-step` (ships off), with `ai:write:budgeted` declared and
159
+ * granted by a signed-in viewer, on a host carrying civitai/civitai#5786. Refused
160
+ * from `dev:live` and from review sessions — build the flow against the mock
161
+ * host. Where it is unavailable the estimate simply carries no `stepQuote`.
162
+ *
163
+ * The flow: `useBuzzWorkflow().estimate(body)` (its `stepQuote.quoteId`) →
164
+ * `runConfirmedStep({ ...body, quoteId })` → `useBuzzWorkflow().watch(workflowId)`.
165
+ * Change nothing in the body between the estimate and the run: the server checks
166
+ * it against the quoted one.
167
+ *
168
+ * 🔴 ONLY A FIXED PRICE IS CONFIRMABLE. `stepQuote` is present when the
169
+ * orchestrator quoted a fixed price above the token's per-call budget and at
170
+ * most `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB` (5,000 Buzz). A variable or
171
+ * metered price, or no price at all, gets no quote and cannot be run this way.
172
+ *
173
+ * 🔴 ESTIMATE THE BODY ON ITS OWN. A batch estimate (`useBatchEstimate()`) never
174
+ * carries a `stepQuote`. And where a quote would be granted, the estimate
175
+ * REJECTS instead for a training `$type` (`training` / `imageResourceTraining`,
176
+ * any casing — use `kind: 'training'`) and for an `input` that is not plain JSON
177
+ * (a `Date`, `Map`, `Set`, `BigInt`, `NaN`, an empty slot in a sparse array, …). Both are server messages with no code; see
178
+ * `BlockWorkflowSnapshot.stepQuote` for the exact text.
179
+ *
180
+ * 🔴 THE CONFIRM IS CIVITAI'S, AND IT IS THE CHARGE. The price on it is read back
181
+ * from the server's stored quote — never from your body. A quote is single-use
182
+ * and expires 15 minutes after the estimate; the server prices the job again at
183
+ * submit and refuses a higher price.
184
+ *
185
+ * 🔴 NO AUTOMATIC RETRY OF ANY KIND, on purpose. Every confirmation is its own
186
+ * job — there is no idempotency key, the orchestrator id comes from the confirmed
187
+ * quote — and one outcome (`submission-unconfirmed`) means a job may already be
188
+ * running. So this hook never re-sends, and neither should you: a retry is a
189
+ * NEW, separately charged job.
190
+ *
191
+ * Sent under the 10-minute human-interaction bound: the host replies only when
192
+ * the viewer clicks or dismisses its dialog.
193
+ *
194
+ * @example
195
+ * const { estimate, watch } = useBuzzWorkflow();
196
+ * const { runConfirmedStep } = useRunConfirmedStep();
197
+ * const quote = (await estimate(body)).stepQuote;
198
+ * if (!quote) return submit(body); // within budget — the ordinary path
199
+ * try {
200
+ * const started = await runConfirmedStep({ ...body, quoteId: quote.quoteId });
201
+ * await watch(started.workflowId, { onUpdate: render });
202
+ * } catch (e) {
203
+ * if (!(e instanceof RunConfirmedStepError)) throw e;
204
+ * if (e.declined) return; // no job — say nothing
205
+ * if (e.dailyLimitExceeded) return; // no job — Civitai already told the viewer
206
+ * if (e.signInRequired) return requestSignIn();
207
+ * if (e.unconfirmed) return showCheckYourGenerations(); // may be running — never auto-retry
208
+ * if (e.refused) return showError(e.message); // a cap refusal; no job, nothing charged
209
+ * showError('Could not start this job. Get a new price and try again.');
210
+ * }
211
+ */
212
+ export function useRunConfirmedStep() {
213
+ useTransportConnection();
214
+ const [pending, setPending] = useState(false);
215
+ const [error, setError] = useState(null);
216
+ // This request can outlive the component by up to ten minutes (the dialog may
217
+ // stay open), so a `pending` toggled on an unmounted control is state nobody
218
+ // can clear.
219
+ const mountedRef = useRef(true);
220
+ useEffect(() => {
221
+ mountedRef.current = true;
222
+ return () => {
223
+ mountedRef.current = false;
224
+ };
225
+ }, []);
226
+ const runConfirmedStep = useCallback(async (body) => {
227
+ // See `CONFIRMED_STEP_QUOTE_ID_REFUSAL`. Before the `pending` flip and the
228
+ // send: nothing about this call may reach the wire. `body` is read through
229
+ // `unknown` because a JavaScript caller, or one that cast its way past the
230
+ // type, is exactly who this is for.
231
+ if (!isStepQuoteId(body?.quoteId)) {
232
+ const refused = new RunConfirmedStepError(CONFIRMED_STEP_QUOTE_ID_REFUSAL, {
233
+ code: 'invalid confirmed step request',
234
+ });
235
+ if (mountedRef.current)
236
+ setError(refused);
237
+ throw refused;
238
+ }
239
+ if (mountedRef.current) {
240
+ setPending(true);
241
+ setError(null);
242
+ }
243
+ try {
244
+ const reply = await sendTypedRequest(getTransport(), { type: 'RUN_CONFIRMED_STEP', payload: { body } }, 'CONFIRMED_STEP_RESULT', { timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS }).catch((err) => {
245
+ if (err instanceof RequestTimeoutError) {
246
+ throw new RunConfirmedStepError(err.message, { timedOut: true });
247
+ }
248
+ throw err instanceof Error ? err : new Error(String(err));
249
+ });
250
+ if (reply.error || !reply.snapshot) {
251
+ // `||`, not `??`: `error: ''` is a shape-valid reply. Its fallback is the
252
+ // CAUTIOUS code — with neither a snapshot nor a reason, nothing says a
253
+ // job was not started, so the block must not be told it was safe to
254
+ // retry.
255
+ throw new RunConfirmedStepError(reply.error || 'submission-unconfirmed');
256
+ }
257
+ // 🔴 A RESOLVED SNAPSHOT IS NOT ALWAYS A RUN. The host's confirmed submit
258
+ // answers its spend-cap refusals (refunded, nothing started) with a
259
+ // resolved `{ workflowId: 'failed', status: 'failed', cost, error }`.
260
+ // Resolving that would hand the caller a workflow id to `watch()` that
261
+ // names nothing, and drop the reason. Exact match on the host's
262
+ // sentinel: a failed snapshot with a REAL id is a workflow that may
263
+ // exist, and keeps resolving so it can be watched.
264
+ if (reply.snapshot.workflowId === HOST_REFUSAL_WORKFLOW_ID &&
265
+ reply.snapshot.status === 'failed') {
266
+ throw new RunConfirmedStepError(reply.snapshot.error || 'confirmed step refused', {
267
+ refusedSnapshot: reply.snapshot,
268
+ });
269
+ }
270
+ return reply.snapshot;
271
+ }
272
+ catch (err) {
273
+ const wrapped = err instanceof RunConfirmedStepError
274
+ ? err
275
+ : new RunConfirmedStepError(err instanceof Error ? err.message : String(err));
276
+ if (mountedRef.current)
277
+ setError(wrapped);
278
+ // Thrown even when unmounted: the caller's `await` must still learn the
279
+ // outcome of a possibly-charged job.
280
+ throw wrapped;
281
+ }
282
+ finally {
283
+ if (mountedRef.current)
284
+ setPending(false);
285
+ }
286
+ }, []);
287
+ return { runConfirmedStep, pending, error };
288
+ }
289
+ //# sourceMappingURL=useRunConfirmedStep.js.map
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
3
3
  import { getTransport } from '../transport/singleton.js';
4
4
  import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
5
+ import { useTransportConnection } from './useTransportConnection.js';
5
6
  /**
6
7
  * The host's CLOSED refusal codes for `RUN_TRAINING`, as a runtime list. Mirrors
7
8
  * civitai/civitai's `RUN_TRAINING_HOST_ERRORS`.
@@ -145,6 +146,7 @@ export class RunTrainingError extends Error {
145
146
  * }
146
147
  */
147
148
  export function useRunTraining() {
149
+ useTransportConnection();
148
150
  const [pending, setPending] = useState(false);
149
151
  const [error, setError] = useState(null);
150
152
  // This request can outlive the component by up to ten minutes (the dialog may
@@ -3,6 +3,7 @@ import { getTransport } from '../transport/singleton.js';
3
3
  import { throwOnFailedReply } from '../internal/replyError.js';
4
4
  import { SAVE_BYTES_MAX_BYTES, SAVE_BYTES_TOO_LARGE_ERROR } from '../internal/saveBytes.js';
5
5
  import { sendTypedRequest } from '../transport/transport.js';
6
+ import { useTransportConnection } from './useTransportConnection.js';
6
7
  /**
7
8
  * Build the wire payload. The ONLY client-side refusal is the cap, so an
8
9
  * over-cap buffer is never copied across `postMessage` just to be refused.
@@ -42,6 +43,7 @@ function toWirePayload(input) {
42
43
  * await saveImage({ bytes: healedPng, filename: 'healed.png' });
43
44
  */
44
45
  export function useSaveImage() {
46
+ useTransportConnection();
45
47
  const saveImage = useCallback(async (input) => {
46
48
  const payload = toWirePayload(input);
47
49
  const reply = await sendTypedRequest(getTransport(), { type: 'SAVE_IMAGE', payload }, 'SAVE_IMAGE_RESULT');
@@ -3,6 +3,7 @@ import { getTransport } from '../transport/singleton.js';
3
3
  import { throwOnFailedReply, throwOnReplyError } from '../internal/replyError.js';
4
4
  import { sendTypedRequest } from '../transport/transport.js';
5
5
  import { parseSharedStorageRateLimit } from '../transport/validate.js';
6
+ import { useTransportConnection } from './useTransportConnection.js';
6
7
  /**
7
8
  * A shared-storage write the server REFUSED because a per-(viewer, app) budget
8
9
  * is empty — thrown by `append`, `update`, `vote`, `unvote`, `withdraw` and
@@ -95,6 +96,7 @@ function rehydrateSharedItem(item) {
95
96
  };
96
97
  }
97
98
  export function useSharedStorage() {
99
+ useTransportConnection();
98
100
  return useMemo(() => {
99
101
  const transport = getTransport();
100
102
  return {
@@ -5,6 +5,7 @@ import { getTransport } from '../transport/singleton.js';
5
5
  import { useHostOrigin } from './useHostOrigin.js';
6
6
  import { useBlockToken } from './useBlockToken.js';
7
7
  import { resolveIdempotencyKey } from '../transport/transport.js';
8
+ import { useTransportConnection } from './useTransportConnection.js';
8
9
  /** The consent-gated scope a tip needs (`social:tip:self`). */
9
10
  const TIP_SCOPES = [BLOCK_SCOPES.SOCIAL_TIP_SELF];
10
11
  /**
@@ -60,6 +61,7 @@ const TIP_REQUEST_TIMEOUT_MS = 30_000;
60
61
  * await tip({ toUserId: 123, amount: 50 }, { idempotencyKey: keyRef.current });
61
62
  */
62
63
  export function useTip() {
64
+ useTransportConnection();
63
65
  const host = useHostOrigin();
64
66
  const { raw } = useBlockToken();
65
67
  const [loading, setLoading] = useState(false);
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { useHostOrigin } from './useHostOrigin.js';
3
3
  import { useBlockToken } from './useBlockToken.js';
4
4
  import { useRequestSequencer } from './useRequestSequencer.js';
5
+ import { useTransportConnection } from './useTransportConnection.js';
5
6
  /**
6
7
  * Backstop timeout for the direct REST allowance GET (see {@link useTip} /
7
8
  * {@link useGenerationResources} for why direct-fetch hooks need their own bound).
@@ -45,6 +46,7 @@ const HOST_ORIGIN_WAIT_MS = 30_000;
45
46
  * // allowance?.remaining — Buzz the viewer may still tip today
46
47
  */
47
48
  export function useTipAllowance() {
49
+ useTransportConnection();
48
50
  const host = useHostOrigin();
49
51
  const { raw } = useBlockToken();
50
52
  const [allowance, setAllowance] = useState(null);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Connects the block to its host at MOUNT. Internal — every public hook calls
3
+ * it, and it is deliberately not exported from the package entry.
4
+ *
5
+ * 🔴 WHY EVERY HOOK, AND WHY AT MOUNT. The host waits 10 s for the block's
6
+ * handshake — `BLOCK_HELLO` on transport construction, `BLOCK_READY` on the
7
+ * first valid `BLOCK_INIT` — and then shows "This app didn't load in time",
8
+ * retries twice and gives up (`bootSkeleton` hides the cover, not the
9
+ * deadline). Both messages are sent by the transport, and the transport is a
10
+ * lazy singleton: nothing goes out until something calls `getTransport()`.
11
+ * Request-only hooks (`useBuzzWorkflow`, `useTip`, …) used to call it only
12
+ * inside their callbacks, so an app whose only SDK use was one of them never
13
+ * answered the host and was torn down before anyone could click. Calling this
14
+ * from every public hook makes "uses any SDK hook" imply "is connected".
15
+ * `test/connectsAtMount.test.tsx` pins it over the whole exported hook set.
16
+ *
17
+ * 🔴 IT IS CALLED EVEN WHERE IT IS REDUNDANT, ON PURPOSE. About half the hooks
18
+ * already reach `getTransport()` at render or in a mount fetch; they call this
19
+ * anyway so the rule is "every public hook", which needs no case analysis. A
20
+ * mount fetch can be conditional (`useWildcardPack` skips it for an invalid id),
21
+ * so "it already fetches on mount" is not a safe reason to leave one out.
22
+ *
23
+ * 🔴 WHY AN EFFECT, NOT RENDER. `useBlockContext` constructs during render, and
24
+ * that is fine for it — but a render-time call throws during SSR (the iframe
25
+ * transport refuses to construct without a `window`), and these hooks have
26
+ * always been safe to render on a server. An effect never runs there, and on
27
+ * the client it runs at mount, long before the host's deadline.
28
+ *
29
+ * 🔴 WHY NOT AT MODULE LOAD. `getTransport(opts)` honours only its FIRST call;
30
+ * dev harnesses and tests inject their allowlist (`installDevTransport()`,
31
+ * `getTransport({ allowedParentOrigins })`) after importing this package and
32
+ * before the first hook runs. Constructing on import would win that race and
33
+ * silently discard their options.
34
+ *
35
+ * 🔴 WHY A FAILURE IS LOGGED, NOT THROWN. Construction throws when no parent
36
+ * allowlist is configured. Before this hook existed, such an app still rendered
37
+ * and only failed on its first bridge call — and it still does: `getTransport()`
38
+ * caches nothing on a throw, so that call throws the same error as before. This
39
+ * hook only ADDS the connection; it must not turn a misconfigured block that
40
+ * renders into one that crashes on mount.
41
+ */
42
+ export declare function useTransportConnection(): void;
43
+ //# sourceMappingURL=useTransportConnection.d.ts.map