@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
@@ -47,7 +47,7 @@
47
47
  * fetch chokepoints ..... 2
48
48
  * REST endpoints ........ GET /api/v1/blocks/me
49
49
  * tRPC procedures ....... 29 = blocks.* (14) + apps.shared.* (10) + apps.storage.* (5)
50
- * switch case labels .... 48, of which 10 REFUSE (enumerated under SCOPE below)
50
+ * switch case labels .... 49, of which 11 REFUSE (enumerated under SCOPE below)
51
51
  * --- END DERIVED ---
52
52
  *
53
53
  * PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
@@ -70,11 +70,12 @@
70
70
  * and row budgets enforced per (app, user) — with the REAL ceilings, so a
71
71
  * write this host accepts is one production would accept.
72
72
  *
73
- * SET_USER_CHECKPOINT (Phase 4): FORWARDED (faithful) to the block-token
74
- * `blocks.updateUserSettings` mutation — never fabricated. The default page
75
- * setup mints a PAGE token which lacks `ctx.modelId`, so the backend returns the
76
- * honest error "block token lacks modelId context"; that real outcome is
77
- * surfaced to the block. With a model-slot token it persists for real.
73
+ * SET_USER_CHECKPOINT (Phase 4): answered as the production host for the slot
74
+ * would. On a PAGE context (the default; dev-token mints page tokens) it is
75
+ * refused locally with the page host's own reply — `ok: false` and the same
76
+ * error text — and no backend call, because the page host makes none. On a
77
+ * model-slot context it is FORWARDED to the block-token
78
+ * `blocks.updateUserSettings` mutation, never fabricated.
78
79
  *
79
80
  * SHARED STORAGE (#386): the live host SERVES all TEN `SHARED_*` bridges —
80
81
  * `apps.shared.{list,get,getCount,getCounts,append,update,vote,unvote,withdraw,
@@ -113,6 +114,10 @@
113
114
  * • RUN_TRAINING — the same dev-token refusal, plus the host-chrome consent
114
115
  * dialog a run is confirmed in, which this harness does not have. Replies
115
116
  * with a refusal.
117
+ * • RUN_CONFIRMED_STEP — the server refuses a confirmed step from a dev token
118
+ * and the estimate never returns a `stepQuote` for one, and the price is
119
+ * confirmed in host chrome this harness does not have. Replies with a
120
+ * refusal.
116
121
  * • PUBLISH_GENERATION_OUTPUTS — publishing requires the viewer's signed-in
117
122
  * civitai.com session, which the local harness does not have; test
118
123
  * publishing against the mock host — `createMockHost` or `Harness` from
@@ -133,6 +138,7 @@
133
138
  * handled the refusal. Use dev:mock for those paths. See the handlers below.
134
139
  */
135
140
  import { consentUnavailablePayload, resolveUngrantableConsentNotice } from './consent.js';
141
+ import { PAGE_CHECKPOINT_PERSIST_ERROR, PAGE_SLOT_ID, emulatesPageHost } from './hostSurface.js';
136
142
  import { hostContextWithTheme } from '../transport/transport.js';
137
143
  import { isRoutableRequestId } from '../transport/requestId.js';
138
144
  import { parseSharedStorageRateLimit } from '../transport/validate.js';
@@ -140,8 +146,6 @@ import { createTrackEventChecker } from './trackEventCheck.js';
140
146
  import { openPickerOverlay, } from './pickerOverlay.js';
141
147
  /** Default civitai backend the live host forwards to. */
142
148
  const DEFAULT_BACKEND_BASE_URL = 'https://civitai.com';
143
- /** Page slot id — dev-token mints page tokens (mirrors PAGE_SLOT_ID server-side). */
144
- const PAGE_SLOT_ID = 'app.page';
145
149
  /** The budgeted-spend scope the workflow procedures require. */
146
150
  const BUDGETED_SCOPE = 'ai:write:budgeted';
147
151
  /**
@@ -153,6 +157,10 @@ export const TRAINING_UNSUPPORTED_ERROR = 'training is not supported in dev:live
153
157
  const trainingUnsupportedLog = (type) => `${type} is not supported in dev:live (the server refuses every training request from a ` +
154
158
  'dev token, and a run needs the host-chrome consent dialog). Replying with a refusal. ' +
155
159
  'Use dev:mock to exercise the training flow.';
160
+ /**
161
+ * The reply `RUN_CONFIRMED_STEP` gets in dev:live — see its handler.
162
+ */
163
+ export const CONFIRMED_STEP_UNSUPPORTED_ERROR = 'confirmed steps are not supported in dev:live — use dev:mock';
156
164
  /**
157
165
  * Decode the middle (payload) segment of a JWT (base64url) WITHOUT verifying
158
166
  * the signature — client-side we only need the claims to build BLOCK_INIT; the
@@ -348,37 +356,33 @@ export function createLiveHost(options) {
348
356
  /**
349
357
  * The single tRPC transport primitive. Builds the non-batched httpLink +
350
358
  * superjson request for `procedure` and returns the PARSED envelope (or an
351
- * error). `method` selects the tRPC verb:
352
- * - `'POST'` (mutation): body is `{ json: <input> }`.
353
- * - `'GET'` (query): input is querystring-encoded as
354
- * `?input=encodeURIComponent(JSON.stringify({ json: <input> }))`.
359
+ * error). Every call — query or mutation — is a POST to
360
+ * `/api/trpc/<procedure>` with body `{ json: <input> }` and no querystring.
361
+ * For a QUERY that is tRPC's query method override, which civitai.com's
362
+ * tRPC handler accepts (`allowMethodOverride: true`); the procedure is still
363
+ * a query on the server. Never GET: every procedure the live host calls
364
+ * takes the block token (a bearer credential) in its input, and a GET would
365
+ * put that input — token included — in the URL (`?input=…`).
355
366
  * Every call carries `Authorization: Bearer <rawToken>`. A thrown fetch →
356
367
  * `{ error, transient: true }`; a non-2xx → `{ error, transient }` (status-
357
368
  * classified via {@link isTransientHttpStatus}). Both `callTrpcMutation` and
358
369
  * `callTrpcData` build on this so the fetch/parse/error logic lives once.
359
370
  */
360
- async function rawTrpcCall(procedure, input, method) {
371
+ async function rawTrpcCall(procedure, input) {
361
372
  if (!fetchImpl)
362
373
  return { error: 'no fetch available' };
363
374
  try {
364
- let url = `${baseUrl}/api/trpc/${procedure}`;
375
+ const url = `${baseUrl}/api/trpc/${procedure}`;
365
376
  const init = {
366
- method,
377
+ method: 'POST',
367
378
  headers: {
368
379
  'content-type': 'application/json',
369
380
  authorization: `Bearer ${rawToken}`,
370
381
  },
371
- };
372
- if (method === 'GET') {
373
- // tRPC query: superjson input goes in the querystring.
374
- const encoded = encodeURIComponent(JSON.stringify({ json: input }));
375
- url = `${url}?input=${encoded}`;
376
- }
377
- else {
378
382
  // Non-batched httpLink + superjson transformer wraps the input as
379
383
  // `{ json: <input> }` (no `meta` for plain JSON inputs).
380
- init.body = JSON.stringify({ json: input });
381
- }
384
+ body: JSON.stringify({ json: input }),
385
+ };
382
386
  const res = await fetchImpl(url, init);
383
387
  const text = await res.text();
384
388
  let parsed;
@@ -412,7 +416,7 @@ export function createLiveHost(options) {
412
416
  * still {@link extractSnapshot}.
413
417
  */
414
418
  async function callTrpcMutation(procedure, input) {
415
- const raw = await rawTrpcCall(procedure, input, 'POST');
419
+ const raw = await rawTrpcCall(procedure, input);
416
420
  if (raw.error !== undefined)
417
421
  return { error: raw.error, transient: raw.transient };
418
422
  const snapshot = extractSnapshot(raw.parsed);
@@ -423,12 +427,13 @@ export function createLiveHost(options) {
423
427
  }
424
428
  /**
425
429
  * Call a tRPC DATA procedure (storage / settings) and unwrap the plain data
426
- * object it returns (via {@link extractData}). `method` is `'GET'` for queries
427
- * (`apps.storage.{get,list,getQuota}`) and `'POST'` for mutations
428
- * (`apps.storage.{set,delete}`, `blocks.updateUserSettings`).
430
+ * object it returns (via {@link extractData}). Queries
431
+ * (`apps.storage.{get,list,getQuota}`) and mutations
432
+ * (`apps.storage.{set,delete}`, `blocks.updateUserSettings`) alike — the
433
+ * wire form is the same POST (see {@link rawTrpcCall}).
429
434
  */
430
- async function callTrpcData(procedure, input, method) {
431
- const raw = await rawTrpcCall(procedure, input, method);
435
+ async function callTrpcData(procedure, input) {
436
+ const raw = await rawTrpcCall(procedure, input);
432
437
  if (raw.error !== undefined) {
433
438
  return {
434
439
  error: raw.error,
@@ -449,11 +454,19 @@ export function createLiveHost(options) {
449
454
  * with {@link POLL_RETRY_BACKOFF_MS} backoff. The final result is only ever a
450
455
  * `snapshot` OR a still-transient error — the caller turns the latter into a
451
456
  * keep-polling `processing` reply, never a fabricated terminal `failed`.
457
+ *
458
+ * `waitSeconds` is the block's long-poll hint, passed through unchanged;
459
+ * `undefined` sends none, which keeps the request byte-identical to a plain
460
+ * read.
452
461
  */
453
- async function pollWithRetry(workflowId) {
462
+ async function pollWithRetry(workflowId, waitSeconds) {
454
463
  let last = { error: 'poll failed', transient: true };
455
464
  for (let attempt = 0; attempt <= POLL_RETRY_BACKOFF_MS.length; attempt += 1) {
456
- last = await callTrpcMutation('blocks.pollWorkflow', { blockToken: rawToken, workflowId });
465
+ last = await callTrpcMutation('blocks.pollWorkflow', {
466
+ blockToken: rawToken,
467
+ workflowId,
468
+ ...(waitSeconds !== undefined ? { waitSeconds } : {}),
469
+ });
457
470
  // A real snapshot (incl. a genuine terminal `failed`) — done.
458
471
  if (last.snapshot)
459
472
  return last;
@@ -772,7 +785,7 @@ export function createLiveHost(options) {
772
785
  // the server's own not-found error comes back here as `error`.
773
786
  if (!isRoutableRequestId(requestId))
774
787
  return;
775
- void callTrpcData('blocks.estimateWorkflowBatch', { blockToken: rawToken, bodies: typed.payload?.bodies }, 'POST').then((r) => {
788
+ void callTrpcData('blocks.estimateWorkflowBatch', { blockToken: rawToken, bodies: typed.payload?.bodies }).then((r) => {
776
789
  const data = r.data;
777
790
  dispatchToBlock({
778
791
  type: 'ESTIMATE_BATCH_RESULT',
@@ -802,7 +815,23 @@ export function createLiveHost(options) {
802
815
  // transfer, and inventing one is a behaviour change on a path where
803
816
  // `dev:live` spends REAL Buzz. Threading it through is tracked
804
817
  // separately rather than bolted on here.
805
- void callTrpcMutation('blocks.submitWorkflow', { blockToken: rawToken, body }).then((r) => {
818
+ //
819
+ // 🔴 THE IDEMPOTENCY KEY IS FORWARDED, as both production hosts do:
820
+ // only when it is a non-empty string, otherwise the field is left
821
+ // off. It was dropped here, so in `dev:live` — which spends REAL
822
+ // Buzz — a retry carrying the same key was a second submit rather
823
+ // than a deduplicated one, and a key in a format the server refuses
824
+ // was never refused. The server validates the format; its refusal
825
+ // comes back as the failed snapshot below.
826
+ const rawIdempotencyKey = typed.payload?.idempotencyKey;
827
+ const idempotencyKey = typeof rawIdempotencyKey === 'string' && rawIdempotencyKey.length > 0
828
+ ? rawIdempotencyKey
829
+ : undefined;
830
+ void callTrpcMutation('blocks.submitWorkflow', {
831
+ blockToken: rawToken,
832
+ body,
833
+ ...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
834
+ }).then((r) => {
806
835
  dispatchToBlock({
807
836
  type: 'WORKFLOW_SUBMITTED',
808
837
  payload: {
@@ -815,7 +844,16 @@ export function createLiveHost(options) {
815
844
  }
816
845
  case 'POLL_WORKFLOW': {
817
846
  const workflowId = typed.payload?.workflowId ?? '';
818
- void pollWithRetry(workflowId).then((r) => {
847
+ // The long-poll hint is forwarded (it was dropped here, so
848
+ // `watch()` could never hold a poll open in `dev:live`), with the
849
+ // production hosts' rule as of civitai/civitai#5798: a finite
850
+ // number is sent UNCHANGED, anything else sends no field. The
851
+ // server owns every bound (floor, `<= 0` is no hold, the cap).
852
+ const rawWaitSeconds = typed.payload?.waitSeconds;
853
+ const waitSeconds = typeof rawWaitSeconds === 'number' && Number.isFinite(rawWaitSeconds)
854
+ ? rawWaitSeconds
855
+ : undefined;
856
+ void pollWithRetry(workflowId, waitSeconds).then((r) => {
819
857
  // Snapshot present (incl. a genuine terminal `failed` from the
820
858
  // backend) — forward it as-is.
821
859
  if (r.snapshot) {
@@ -899,7 +937,7 @@ export function createLiveHost(options) {
899
937
  // so a reply without one is unroutable.
900
938
  if (!isRoutableRequestId(requestId))
901
939
  return;
902
- void callTrpcData('blocks.getMyBuzzBalance', { blockToken: rawToken }, 'POST').then((r) => {
940
+ void callTrpcData('blocks.getMyBuzzBalance', { blockToken: rawToken }).then((r) => {
903
941
  dispatchToBlock({
904
942
  type: 'BUZZ_BALANCE_RESULT',
905
943
  payload: r.data
@@ -920,7 +958,7 @@ export function createLiveHost(options) {
920
958
  // without one is unroutable.
921
959
  if (!isRoutableRequestId(requestId))
922
960
  return;
923
- void callTrpcData('blocks.getMyViewer', { blockToken: rawToken }, 'POST').then((r) => {
961
+ void callTrpcData('blocks.getMyViewer', { blockToken: rawToken }).then((r) => {
924
962
  dispatchToBlock({
925
963
  type: 'VIEWER_RESULT',
926
964
  payload: r.data
@@ -937,7 +975,7 @@ export function createLiveHost(options) {
937
975
  // real host. FREE-TEXT error on failure. Unroutable without requestId.
938
976
  if (!isRoutableRequestId(requestId))
939
977
  return;
940
- void callTrpcData('blocks.getMyBuzzTransactions', { ...(typed.payload?.params ?? {}), blockToken: rawToken }, 'POST').then((r) => {
978
+ void callTrpcData('blocks.getMyBuzzTransactions', { ...(typed.payload?.params ?? {}), blockToken: rawToken }).then((r) => {
941
979
  dispatchToBlock({
942
980
  type: 'BUZZ_TRANSACTIONS_RESULT',
943
981
  payload: r.data
@@ -950,7 +988,7 @@ export function createLiveHost(options) {
950
988
  case 'GET_BUZZ_ACCOUNTS': {
951
989
  if (!isRoutableRequestId(requestId))
952
990
  return;
953
- void callTrpcData('blocks.getMyBuzzAccounts', { blockToken: rawToken }, 'POST').then((r) => {
991
+ void callTrpcData('blocks.getMyBuzzAccounts', { blockToken: rawToken }).then((r) => {
954
992
  dispatchToBlock({
955
993
  type: 'BUZZ_ACCOUNTS_RESULT',
956
994
  payload: r.data
@@ -963,7 +1001,7 @@ export function createLiveHost(options) {
963
1001
  case 'GET_DAILY_COMPENSATION': {
964
1002
  if (!isRoutableRequestId(requestId))
965
1003
  return;
966
- void callTrpcData('blocks.getMyDailyCompensation', { ...(typed.payload?.params ?? {}), blockToken: rawToken }, 'POST').then((r) => {
1004
+ void callTrpcData('blocks.getMyDailyCompensation', { ...(typed.payload?.params ?? {}), blockToken: rawToken }).then((r) => {
967
1005
  dispatchToBlock({
968
1006
  type: 'DAILY_COMPENSATION_RESULT',
969
1007
  payload: r.data
@@ -1080,6 +1118,30 @@ export function createLiveHost(options) {
1080
1118
  });
1081
1119
  return;
1082
1120
  }
1121
+ case 'RUN_CONFIRMED_STEP': {
1122
+ // UNSUPPORTED in live, and the SERVER decides it, exactly as for the
1123
+ // training bridges above: a confirmed step is refused for a dev token
1124
+ // ("a confirmed step is not available from a development or review
1125
+ // session" — `quotedGrantTokenGate` in civitai/civitai), so a forwarded
1126
+ // ESTIMATE never carries a `stepQuote` here and a block has no real
1127
+ // `quoteId` to send. The price is also confirmed in host chrome this
1128
+ // harness does not have, and the job is CHARGED — faking either half
1129
+ // would let dev prove out a flow production does not have.
1130
+ //
1131
+ // Use dev:mock: an over-budget `generation.costPerGen` makes its
1132
+ // estimate quote one, and `runConfirmedStepError` covers `declined`,
1133
+ // `daily-limit-exceeded` and `submission-unconfirmed`.
1134
+ if (!isRoutableRequestId(requestId))
1135
+ return;
1136
+ logOnce('confirmed-step', 'RUN_CONFIRMED_STEP is not supported in dev:live (the server refuses a confirmed ' +
1137
+ 'step from a dev token, and the price is confirmed in host chrome). Replying ' +
1138
+ 'with a refusal. Use dev:mock to exercise the confirmed-step flow.');
1139
+ dispatchToBlock({
1140
+ type: 'CONFIRMED_STEP_RESULT',
1141
+ payload: { requestId, error: CONFIRMED_STEP_UNSUPPORTED_ERROR },
1142
+ });
1143
+ return;
1144
+ }
1083
1145
  case 'GET_WILDCARD_PACK': {
1084
1146
  // UNSUPPORTED in live v1 (honest-by-design, like OPEN_BUZZ_PURCHASE):
1085
1147
  // the real host resolves the pack via a SESSION-authed proc and does
@@ -1110,7 +1172,7 @@ export function createLiveHost(options) {
1110
1172
  // FREE-TEXT error on failure. Unroutable without requestId.
1111
1173
  if (!isRoutableRequestId(requestId))
1112
1174
  return;
1113
- void callTrpcData('blocks.queryAppWorkflows', { ...(typed.payload?.params ?? {}), blockToken: rawToken }, 'POST').then((r) => {
1175
+ void callTrpcData('blocks.queryAppWorkflows', { ...(typed.payload?.params ?? {}), blockToken: rawToken }).then((r) => {
1114
1176
  dispatchToBlock({
1115
1177
  type: 'APP_WORKFLOWS_RESULT',
1116
1178
  payload: r.data
@@ -1132,7 +1194,7 @@ export function createLiveHost(options) {
1132
1194
  const cancelId = typed.payload?.workflowId;
1133
1195
  if (typeof cancelId !== 'string' || cancelId.length === 0)
1134
1196
  return;
1135
- void callTrpcData('blocks.cancelAppWorkflow', { blockToken: rawToken, workflowId: cancelId }, 'POST').then((r) => {
1197
+ void callTrpcData('blocks.cancelAppWorkflow', { blockToken: rawToken, workflowId: cancelId }).then((r) => {
1136
1198
  dispatchToBlock({
1137
1199
  type: 'CANCEL_APP_WORKFLOW_RESULT',
1138
1200
  payload: r.data
@@ -1185,7 +1247,7 @@ export function createLiveHost(options) {
1185
1247
  // FREE-TEXT error on failure. Unroutable without requestId.
1186
1248
  if (!isRoutableRequestId(requestId))
1187
1249
  return;
1188
- void callTrpcData('blocks.getImagesByIds', { blockToken: rawToken, imageIds: typed.payload?.imageIds ?? [] }, 'POST').then((r) => {
1250
+ void callTrpcData('blocks.getImagesByIds', { blockToken: rawToken, imageIds: typed.payload?.imageIds ?? [] }).then((r) => {
1189
1251
  dispatchToBlock({
1190
1252
  type: 'IMAGES_RESULT',
1191
1253
  payload: r.data
@@ -1282,13 +1344,27 @@ export function createLiveHost(options) {
1282
1344
  return;
1283
1345
  }
1284
1346
  case 'SET_USER_CHECKPOINT': {
1285
- // FORWARD (faithful) to blocks.updateUserSettings — never fabricate.
1286
- // Mirror the prod IframeHost's versionId validation: a number or an
1287
- // explicit null is valid; anything else is rejected WITHOUT a backend
1288
- // call. NOTE: dev-token mints PAGE tokens which lack `ctx.modelId`, so
1289
- // with the default page setup the backend returns the honest error
1290
- // "block token lacks modelId context" — this is FAITHFUL (it surfaces
1291
- // the real outcome) and works for real with a model-slot token.
1347
+ // 🔴 A PAGE REFUSES, ALWAYS, WITHOUT A BACKEND CALL — and the default
1348
+ // context is a page. That is the production page host exactly: the
1349
+ // same reply type, shape and error text, whatever `versionId` was
1350
+ // sent, and nothing dropped except a request with no id to answer.
1351
+ // This used to forward to `blocks.updateUserSettings`, a call the
1352
+ // page host never makes, so the block saw the server's wording where
1353
+ // production sends its own refusal. See ./hostSurface.ts.
1354
+ if (emulatesPageHost(options.context)) {
1355
+ if (!isRoutableRequestId(requestId))
1356
+ return;
1357
+ dispatchToBlock({
1358
+ type: 'USER_CHECKPOINT_SET',
1359
+ payload: { requestId, ok: false, error: PAGE_CHECKPOINT_PERSIST_ERROR },
1360
+ });
1361
+ return;
1362
+ }
1363
+ // MODEL SLOT: FORWARD (faithful) to blocks.updateUserSettings — never
1364
+ // fabricate. Mirror the prod IframeHost's versionId validation: a
1365
+ // number or an explicit null is valid; anything else is rejected
1366
+ // WITHOUT a backend call. A token without `ctx.modelId` gets the
1367
+ // server's honest "block token lacks modelId context".
1292
1368
  const rawVersionId = typed.payload?.versionId;
1293
1369
  const versionId = rawVersionId === null
1294
1370
  ? null
@@ -1306,7 +1382,7 @@ export function createLiveHost(options) {
1306
1382
  });
1307
1383
  return;
1308
1384
  }
1309
- void callTrpcData('blocks.updateUserSettings', { blockToken: rawToken, settings: { checkpoint_version_id: versionId } }, 'POST').then((r) => {
1385
+ void callTrpcData('blocks.updateUserSettings', { blockToken: rawToken, settings: { checkpoint_version_id: versionId } }).then((r) => {
1310
1386
  dispatchToBlock({
1311
1387
  type: 'USER_CHECKPOINT_SET',
1312
1388
  payload: r.error
@@ -1317,11 +1393,11 @@ export function createLiveHost(options) {
1317
1393
  return;
1318
1394
  }
1319
1395
  case 'APP_STORAGE_GET': {
1320
- // Query apps.storage.get {blockToken, key} (GET). The server enforces
1396
+ // Query apps.storage.get {blockToken, key} (sent as POST). The server enforces
1321
1397
  // the apps:storage:read scope; the dev token carries it iff the local
1322
1398
  // manifest declared it.
1323
1399
  const key = typed.payload?.key ?? '';
1324
- void callTrpcData('apps.storage.get', { blockToken: rawToken, key }, 'GET').then((r) => {
1400
+ void callTrpcData('apps.storage.get', { blockToken: rawToken, key }).then((r) => {
1325
1401
  if (r.error) {
1326
1402
  dispatchToBlock({
1327
1403
  type: 'APP_STORAGE_GET_RESULT',
@@ -1344,7 +1420,7 @@ export function createLiveHost(options) {
1344
1420
  // keep in sync.
1345
1421
  const key = typed.payload?.key ?? '';
1346
1422
  const value = typed.payload?.value;
1347
- void callTrpcData('apps.storage.set', { blockToken: rawToken, key, value }, 'POST').then((r) => {
1423
+ void callTrpcData('apps.storage.set', { blockToken: rawToken, key, value }).then((r) => {
1348
1424
  if (r.error) {
1349
1425
  dispatchToBlock({
1350
1426
  type: 'APP_STORAGE_SET_RESULT',
@@ -1367,7 +1443,7 @@ export function createLiveHost(options) {
1367
1443
  case 'APP_STORAGE_DELETE': {
1368
1444
  // Mutation apps.storage.delete {blockToken, key} (POST).
1369
1445
  const key = typed.payload?.key ?? '';
1370
- void callTrpcData('apps.storage.delete', { blockToken: rawToken, key }, 'POST').then((r) => {
1446
+ void callTrpcData('apps.storage.delete', { blockToken: rawToken, key }).then((r) => {
1371
1447
  if (r.error) {
1372
1448
  dispatchToBlock({
1373
1449
  type: 'APP_STORAGE_DELETE_RESULT',
@@ -1392,7 +1468,7 @@ export function createLiveHost(options) {
1392
1468
  return;
1393
1469
  }
1394
1470
  case 'APP_STORAGE_LIST': {
1395
- // Query apps.storage.list {blockToken, prefix?, limit, cursor?} (GET).
1471
+ // Query apps.storage.list {blockToken, prefix?, limit, cursor?} (sent as POST).
1396
1472
  // Clamp the limit to the server's [1,200] bound (default 50); only
1397
1473
  // include prefix/cursor when they're strings so JSON.stringify drops
1398
1474
  // the undefined keys (the server input is `.optional()`).
@@ -1407,7 +1483,7 @@ export function createLiveHost(options) {
1407
1483
  listInput.prefix = prefix;
1408
1484
  if (typeof cursor === 'string')
1409
1485
  listInput.cursor = cursor;
1410
- void callTrpcData('apps.storage.list', listInput, 'GET').then((r) => {
1486
+ void callTrpcData('apps.storage.list', listInput).then((r) => {
1411
1487
  if (r.error) {
1412
1488
  dispatchToBlock({
1413
1489
  type: 'APP_STORAGE_LIST_RESULT',
@@ -1440,8 +1516,8 @@ export function createLiveHost(options) {
1440
1516
  return;
1441
1517
  }
1442
1518
  case 'APP_STORAGE_QUOTA': {
1443
- // Query apps.storage.getQuota {blockToken} (GET).
1444
- void callTrpcData('apps.storage.getQuota', { blockToken: rawToken }, 'GET').then((r) => {
1519
+ // Query apps.storage.getQuota {blockToken} (sent as POST).
1520
+ void callTrpcData('apps.storage.getQuota', { blockToken: rawToken }).then((r) => {
1445
1521
  if (r.error) {
1446
1522
  dispatchToBlock({
1447
1523
  type: 'APP_STORAGE_QUOTA_RESULT',
@@ -1490,7 +1566,7 @@ export function createLiveHost(options) {
1490
1566
  listInput.prefix = typed.payload.prefix;
1491
1567
  if (typeof typed.payload?.cursor === 'string')
1492
1568
  listInput.cursor = typed.payload.cursor;
1493
- void callTrpcData('apps.shared.list', listInput, 'GET').then((r) => {
1569
+ void callTrpcData('apps.shared.list', listInput).then((r) => {
1494
1570
  if (r.error) {
1495
1571
  dispatchToBlock({
1496
1572
  type: 'SHARED_LIST_RESULT',
@@ -1516,7 +1592,7 @@ export function createLiveHost(options) {
1516
1592
  }
1517
1593
  case 'SHARED_GET_COUNT': {
1518
1594
  const key = typed.payload?.key ?? '';
1519
- void callTrpcData('apps.shared.getCount', { blockToken: rawToken, key }, 'GET').then((r) => {
1595
+ void callTrpcData('apps.shared.getCount', { blockToken: rawToken, key }).then((r) => {
1520
1596
  if (r.error) {
1521
1597
  dispatchToBlock({
1522
1598
  type: 'SHARED_GET_COUNT_RESULT',
@@ -1534,7 +1610,7 @@ export function createLiveHost(options) {
1534
1610
  }
1535
1611
  case 'SHARED_GET_COUNTS': {
1536
1612
  const keys = Array.isArray(typed.payload?.keys) ? typed.payload.keys : [];
1537
- void callTrpcData('apps.shared.getCounts', { blockToken: rawToken, keys }, 'GET').then((r) => {
1613
+ void callTrpcData('apps.shared.getCounts', { blockToken: rawToken, keys }).then((r) => {
1538
1614
  if (r.error) {
1539
1615
  dispatchToBlock({
1540
1616
  type: 'SHARED_GET_COUNTS_RESULT',
@@ -1571,7 +1647,7 @@ export function createLiveHost(options) {
1571
1647
  // so a `?g=` deep-link to a moderated row cannot distinguish it
1572
1648
  // from an absent one. Only a HOST-SIDE failure sets `error`.
1573
1649
  const key = typed.payload?.key ?? '';
1574
- void callTrpcData('apps.shared.get', { blockToken: rawToken, key }, 'GET').then((r) => {
1650
+ void callTrpcData('apps.shared.get', { blockToken: rawToken, key }).then((r) => {
1575
1651
  if (r.error) {
1576
1652
  dispatchToBlock({
1577
1653
  type: 'SHARED_GET_RESULT',
@@ -1605,7 +1681,7 @@ export function createLiveHost(options) {
1605
1681
  }
1606
1682
  case 'SHARED_APPEND': {
1607
1683
  const value = typed.payload?.value;
1608
- void callTrpcData('apps.shared.append', { blockToken: rawToken, value }, 'POST').then((r) => {
1684
+ void callTrpcData('apps.shared.append', { blockToken: rawToken, value }).then((r) => {
1609
1685
  if (r.error) {
1610
1686
  dispatchToBlock({
1611
1687
  type: 'SHARED_APPEND_RESULT',
@@ -1624,7 +1700,7 @@ export function createLiveHost(options) {
1624
1700
  case 'SHARED_UPDATE': {
1625
1701
  const key = typed.payload?.key ?? '';
1626
1702
  const value = typed.payload?.value;
1627
- void callTrpcData('apps.shared.update', { blockToken: rawToken, key, value }, 'POST').then((r) => {
1703
+ void callTrpcData('apps.shared.update', { blockToken: rawToken, key, value }).then((r) => {
1628
1704
  dispatchToBlock({
1629
1705
  type: 'SHARED_UPDATE_RESULT',
1630
1706
  payload: r.error
@@ -1636,7 +1712,7 @@ export function createLiveHost(options) {
1636
1712
  }
1637
1713
  case 'SHARED_VOTE': {
1638
1714
  const key = typed.payload?.key ?? '';
1639
- void callTrpcData('apps.shared.vote', { blockToken: rawToken, key }, 'POST').then((r) => {
1715
+ void callTrpcData('apps.shared.vote', { blockToken: rawToken, key }).then((r) => {
1640
1716
  if (r.error) {
1641
1717
  dispatchToBlock({
1642
1718
  type: 'SHARED_VOTE_RESULT',
@@ -1654,7 +1730,7 @@ export function createLiveHost(options) {
1654
1730
  }
1655
1731
  case 'SHARED_UNVOTE': {
1656
1732
  const key = typed.payload?.key ?? '';
1657
- void callTrpcData('apps.shared.unvote', { blockToken: rawToken, key }, 'POST').then((r) => {
1733
+ void callTrpcData('apps.shared.unvote', { blockToken: rawToken, key }).then((r) => {
1658
1734
  if (r.error) {
1659
1735
  dispatchToBlock({
1660
1736
  type: 'SHARED_UNVOTE_RESULT',
@@ -1672,7 +1748,7 @@ export function createLiveHost(options) {
1672
1748
  }
1673
1749
  case 'SHARED_WITHDRAW': {
1674
1750
  const key = typed.payload?.key ?? '';
1675
- void callTrpcData('apps.shared.withdraw', { blockToken: rawToken, key }, 'POST').then((r) => {
1751
+ void callTrpcData('apps.shared.withdraw', { blockToken: rawToken, key }).then((r) => {
1676
1752
  if (r.error) {
1677
1753
  dispatchToBlock({
1678
1754
  type: 'SHARED_WITHDRAW_RESULT',
@@ -1714,7 +1790,7 @@ export function createLiveHost(options) {
1714
1790
  if (typeof typed.payload?.reason === 'string') {
1715
1791
  reportInput.reason = typed.payload.reason;
1716
1792
  }
1717
- void callTrpcData('apps.shared.report', reportInput, 'POST').then((r) => {
1793
+ void callTrpcData('apps.shared.report', reportInput).then((r) => {
1718
1794
  dispatchToBlock({
1719
1795
  type: 'SHARED_REPORT_RESULT',
1720
1796
  payload: r.error
@@ -1968,6 +2044,12 @@ export function createLiveHost(options) {
1968
2044
  // failed viewer fetch already warns in resolveViewer).
1969
2045
  return;
1970
2046
  default:
2047
+ // No reply, as in production. `livehost-message-coverage.test.mjs`
2048
+ // holds every protocol type with a reply channel to a `case` above,
2049
+ // so what reaches here is BLOCK_HELLO, BLOCK_MESSAGE_REJECTED or a
2050
+ // type this protocol does not declare. The real host's `unsupported
2051
+ // on this host` reply is built only for a request type it KNOWS and
2052
+ // has no handler for; a type it does not know gets nothing.
1971
2053
  return;
1972
2054
  }
1973
2055
  },