@civitai/blocks-react 0.39.0 → 0.41.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.
Files changed (57) hide show
  1. package/README.md +132 -5
  2. package/dist/hooks/useBlockContext.d.ts +6 -0
  3. package/dist/hooks/useBlockContext.d.ts.map +1 -1
  4. package/dist/hooks/useBlockContext.js +6 -0
  5. package/dist/hooks/useBlockContext.js.map +1 -1
  6. package/dist/hooks/useBlockTheme.d.ts +39 -0
  7. package/dist/hooks/useBlockTheme.d.ts.map +1 -0
  8. package/dist/hooks/useBlockTheme.js +41 -0
  9. package/dist/hooks/useBlockTheme.js.map +1 -0
  10. package/dist/hooks/useBuzzWorkflow.d.ts +13 -1
  11. package/dist/hooks/useBuzzWorkflow.d.ts.map +1 -1
  12. package/dist/hooks/useBuzzWorkflow.js +13 -1
  13. package/dist/hooks/useBuzzWorkflow.js.map +1 -1
  14. package/dist/hooks/useConsentUnavailable.d.ts +92 -0
  15. package/dist/hooks/useConsentUnavailable.d.ts.map +1 -0
  16. package/dist/hooks/useConsentUnavailable.js +93 -0
  17. package/dist/hooks/useConsentUnavailable.js.map +1 -0
  18. package/dist/hooks/useRequestConsent.d.ts +21 -3
  19. package/dist/hooks/useRequestConsent.d.ts.map +1 -1
  20. package/dist/hooks/useRequestConsent.js +30 -4
  21. package/dist/hooks/useRequestConsent.js.map +1 -1
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/internal/consent.d.ts +71 -0
  27. package/dist/internal/consent.d.ts.map +1 -0
  28. package/dist/internal/consent.js +88 -0
  29. package/dist/internal/consent.js.map +1 -0
  30. package/dist/internal/consentRefusalLatch.d.ts +19 -0
  31. package/dist/internal/consentRefusalLatch.d.ts.map +1 -0
  32. package/dist/internal/consentRefusalLatch.js +38 -0
  33. package/dist/internal/consentRefusalLatch.js.map +1 -0
  34. package/dist/internal/iframeTransport.d.ts +25 -0
  35. package/dist/internal/iframeTransport.d.ts.map +1 -1
  36. package/dist/internal/iframeTransport.js +47 -0
  37. package/dist/internal/iframeTransport.js.map +1 -1
  38. package/dist/internal/liveHost.d.ts +8 -2
  39. package/dist/internal/liveHost.d.ts.map +1 -1
  40. package/dist/internal/liveHost.js +100 -6
  41. package/dist/internal/liveHost.js.map +1 -1
  42. package/dist/internal/mockHost.d.ts +57 -13
  43. package/dist/internal/mockHost.d.ts.map +1 -1
  44. package/dist/internal/mockHost.js +188 -29
  45. package/dist/internal/mockHost.js.map +1 -1
  46. package/dist/internal/transport.d.ts +58 -1
  47. package/dist/internal/transport.d.ts.map +1 -1
  48. package/dist/internal/transport.js +62 -0
  49. package/dist/internal/transport.js.map +1 -1
  50. package/dist/internal/validate.d.ts +73 -3
  51. package/dist/internal/validate.d.ts.map +1 -1
  52. package/dist/internal/validate.js +135 -3
  53. package/dist/internal/validate.js.map +1 -1
  54. package/dist/testing.d.ts.map +1 -1
  55. package/dist/testing.js +41 -2
  56. package/dist/testing.js.map +1 -1
  57. package/package.json +4 -4
package/README.md CHANGED
@@ -27,7 +27,7 @@ your block app and the SDK share a single React tree.
27
27
  import { useRef } from 'react';
28
28
  import { useBlockContext, useBlockResize, useBuzzWorkflow } from '@civitai/blocks-react';
29
29
  import { Button } from '@civitai/blocks-react/ui';
30
- import type { ModelSlotContext } from '@civitai/app-sdk/blocks';
30
+ import { isModelSlotContext } from '@civitai/app-sdk/blocks';
31
31
 
32
32
  export function App() {
33
33
  const { ready, context, viewer, theme } = useBlockContext();
@@ -36,21 +36,22 @@ export function App() {
36
36
  useBlockResize(rootRef); // host fits the iframe to content
37
37
 
38
38
  if (!ready) return <div ref={rootRef}>Loading…</div>;
39
- const model = context as ModelSlotContext;
39
+ // `context` is a union keyed on slotId — narrow with the guard, not a cast.
40
+ if (!isModelSlotContext(context)) return <div ref={rootRef}>Wrong slot.</div>;
40
41
 
41
42
  return (
42
43
  // GOTCHA #60: set data-theme on YOUR OWN root — the host can't reach into
43
44
  // the iframe to set it. Without this any [data-theme="dark"] CSS is dormant.
44
45
  <div ref={rootRef} data-theme={theme}>
45
- <p>Block for model {model.modelName} ({viewer?.username ?? 'anon'})</p>
46
+ <p>Block for model {context.modelName} ({viewer ? 'signed in' : 'anon'})</p>
46
47
  {/* `/ui` Button — themed by the data-theme above; `loading` disables + shows a spinner */}
47
48
  <Button
48
49
  loading={status === 'submitting' || status === 'polling'}
49
50
  onClick={() =>
50
51
  submit({
51
52
  kind: 'textToImage',
52
- modelId: model.modelId,
53
- modelVersionId: model.modelVersionId,
53
+ modelId: context.modelId,
54
+ modelVersionId: context.modelVersionId,
54
55
  params: { prompt: 'a cat' },
55
56
  })
56
57
  }
@@ -112,8 +113,35 @@ const { ready, context, viewer, theme, settings, blockId, blockInstanceId, appId
112
113
  model-page slots.
113
114
  - `viewer` — `ViewerInfo | null` (`null` = anonymous).
114
115
  - `theme` — `'light' | 'dark'`. **Set `data-theme={theme}` on your root** (gotcha #60).
116
+ LIVE: it starts at the `BLOCK_INIT` value and then tracks the host's
117
+ `THEME_CHANGE` push when the viewer toggles dark mode mid-session — see
118
+ [`useBlockTheme()`](#useblocktheme).
115
119
  - `settings` — `{ publisherSettings, userSettings }`.
116
120
 
121
+ ### `useBlockTheme()`
122
+
123
+ The host's CURRENT site theme, and nothing else. Same value as
124
+ `useBlockContext().theme` — reach for this when theme is all you need.
125
+
126
+ ```tsx
127
+ function ThemedRoot() {
128
+ const theme = useBlockTheme(); // 'light' | 'dark'
129
+ return <div data-theme={theme}>…</div>;
130
+ }
131
+ ```
132
+
133
+ The viewer can toggle light/dark **while your block is mounted**. The host pushes
134
+ a `THEME_CHANGE` message and this hook re-renders. You get that for free as long
135
+ as you *read* the theme on every render — a block that copies it into state once
136
+ at mount, or writes `data-theme` imperatively in a mount-only effect, will stay
137
+ stuck on the old theme.
138
+
139
+ Against a host that predates `THEME_CHANGE` the value simply never moves (the
140
+ old behaviour). Nothing awaits the message, so there is no hang either way.
141
+
142
+ Exercise it locally: `createMockHost(...).setTheme('light')` (and the same on the
143
+ `dev:live` host) pushes the real message.
144
+
117
145
  ### `useBlockResize(ref)`
118
146
 
119
147
  Attach to your root element. Observes its height and posts `RESIZE_IFRAME` so the
@@ -474,10 +502,109 @@ needs `ai:write:budgeted` but the viewer hasn't granted it). Fire-and-forget —
474
502
  on grant the host pushes a new token; observe `useBlockToken().scopes` and retry.
475
503
 
476
504
  ```tsx
505
+ import { useRequestConsent } from '@civitai/blocks-react';
506
+
477
507
  const { requestConsent } = useRequestConsent();
478
508
  requestConsent({ scopes: ['ai:write:budgeted', 'buzz:read:self'] });
479
509
  ```
480
510
 
511
+ 🔴 **Always pass `scopes`, with a real scope name in it — it is optional in the
512
+ signature but a precondition for the refusal path below.** The host grants the
513
+ missing set it computed at mint, so a bare `requestConsent()` still opens the
514
+ consent dialog. But `CONSENT_UNAVAILABLE` is computed *from the hint*: with no
515
+ explicit scope proven un-grantable, the host cannot tell "can never be granted"
516
+ from "the viewer hasn't confirmed yet", so it stays silent rather than guess.
517
+
518
+ The bar is an array holding **at least one non-empty string** — not merely "an
519
+ array is present". `undefined`, a non-array, `[]`, `['']` and `[1, 2]` all
520
+ produce silence, in `pnpm dev` and in production alike, so
521
+ `requestConsent({ scopes: [] })` follows the instruction and still receives
522
+ nothing. To its author that reads as a broken message rather than a thin
523
+ argument.
524
+
525
+ ### `useConsentUnavailable()`
526
+
527
+ Some environments withhold a scope at mint (a dev-tunnel preview token, a surface
528
+ that carries no money scope), so no consent round-trip can ever add it. The host
529
+ then pushes an uncorrelated `CONSENT_UNAVAILABLE` — *not* a reply, because
530
+ `REQUEST_CONSENT` carries no `requestId`. Consume it and stop telling the user to
531
+ retry something that can't succeed:
532
+
533
+ ```tsx
534
+ import { useConsentUnavailable, useRequestConsent } from '@civitai/blocks-react';
535
+
536
+ function ConsentAwareGenerate() {
537
+ const { requestConsent } = useRequestConsent();
538
+ const { refusal, reset } = useConsentUnavailable();
539
+
540
+ // 🔴 Branch on `refusal !== null`, NEVER on `refusal.scopes.length`. The host
541
+ // refuses on its own unfiltered set but names only scopes in the public
542
+ // vocabulary, so `scopes: []` is a legitimate refusal — gating on the length
543
+ // silently drops the very message you subscribed for. Use the names for copy.
544
+ if (refusal) {
545
+ return (
546
+ <div>
547
+ <p>Generating isn't available on this page.</p>
548
+ <button onClick={reset}>Try again</button>
549
+ </div>
550
+ );
551
+ }
552
+ // 🔴 `scopes` is REQUIRED for a refusal to ever arrive — see above.
553
+ return <button onClick={() => requestConsent({ scopes: ['ai:write:budgeted'] })}>Generate</button>;
554
+ }
555
+ ```
556
+
557
+ `refusal` holds the latest `ConsentUnavailablePayload` (`{ reason, scopes }`) or
558
+ `null`; `reset()` clears it, since a refusal is scoped to the scopes that were
559
+ asked for and shouldn't latch for the life of the block. Against a host that
560
+ never sends the message the hook simply stays `null` — nothing awaits it, so
561
+ there is no timeout to hit.
562
+
563
+ 🔴 **The push is UNCORRELATED, and that is the permanent shape of this API.**
564
+ `REQUEST_CONSENT` carries no `requestId`, so a refusal cannot be matched to the
565
+ request that provoked it. Two consequences to design around:
566
+
567
+ - **Every mounted `useConsentUnavailable()` sees every refusal.** There is no
568
+ reliable filter: `scopes` is advisory and may legitimately be `[]`, so it
569
+ cannot serve as a correlation key. If two independent parts of your block
570
+ request different scopes, both will see both refusals. Keep a request and its
571
+ refusal UI in one component, or track the outstanding request yourself.
572
+ - **A refusal is buffered across mounts, so one that arrives while the consumer
573
+ is unmounted is not lost.** The transport hands an unsolicited push only to
574
+ handlers registered at the instant it arrives, so without this a refusal that
575
+ landed before the consumer mounted — the requester and the consumer being
576
+ different components, or the consumer being conditionally rendered — vanished,
577
+ and the block went back to showing "click Generate again" beside the host's
578
+ "unavailable". `requestConsent()` arms the buffer as it sends. It keeps only
579
+ the latest refusal, is dropped when the block token changes (a refusal is a
580
+ claim about *that* token's scopes, and the grant path re-mints), and is cleared
581
+ by `reset()` — so the "Try again" button above genuinely resets, rather than
582
+ having the refusal reappear on the next mount. A `REQUEST_CONSENT` you post
583
+ through the raw transport instead of the hook does not arm it.
584
+
585
+ Without the hook (a non-React consumer, or one wiring the transport directly),
586
+ the same push is available untyped — note the explicit type import, which the
587
+ cast needs and which the hook makes unnecessary:
588
+
589
+ ```tsx
590
+ import { getTransport } from '@civitai/blocks-react';
591
+ import type { ConsentUnavailablePayload } from '@civitai/app-sdk/blocks';
592
+
593
+ const unsubscribe = getTransport().onMessage('CONSENT_UNAVAILABLE', (payload) => {
594
+ // `onMessage` hands you `unknown`; this cast is UNCHECKED, which is why
595
+ // `useConsentUnavailable()` is the preferred path.
596
+ const { reason, scopes } = payload as ConsentUnavailablePayload;
597
+ console.info('permission unavailable', reason, scopes);
598
+ });
599
+ ```
600
+
601
+ To exercise the refusal locally, run the mock host with
602
+ `createMockHost({ consentGrantable: false })`, flip it live with
603
+ `host.setScenario({ consentGrantable: false })`, or append `?consent=ungrantable`
604
+ to the dev harness URL — the `<Harness>` chrome then reads `consent=ungrantable`
605
+ rather than `withheld`. `dev:live` emits it too: live mode can grant nothing, so
606
+ any request for a scope your dev token lacks produces one.
607
+
481
608
  ### `useDomainMaturity()`
482
609
 
483
610
  Read the surrounding color-domain's maturity ceiling (civitai #2670) so a block
@@ -18,6 +18,12 @@ declare function useTransportSnapshot(): BlockSnapshot;
18
18
  * set `data-theme={theme}` on your root, gotcha #60), `blockId`,
19
19
  * `blockInstanceId`, and `appId`.
20
20
  *
21
+ * `theme` is LIVE: it starts at the `BLOCK_INIT` (or URL-fragment) value and
22
+ * then tracks the host's `THEME_CHANGE` push when the viewer toggles light/dark
23
+ * mid-session. Reading it here is enough — {@link useBlockTheme} is the same
24
+ * value, narrower. Against a host that never pushes it, the value simply never
25
+ * moves (today's behaviour).
26
+ *
21
27
  * @example
22
28
  * const { ready, context, viewer, theme, settings } = useBlockContext();
23
29
  * if (!ready) return <div>Loading…</div>;
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockContext.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,iBAAS,oBAAoB,IAAI,aAAa,CAS7C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,IAAI,IAAI,CACrC,aAAa,EACX,OAAO,GACP,YAAY,GACZ,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,SAAS,GACT,iBAAiB,GACjB,OAAO,CACV,CAcA;AAED,6EAA6E;AAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"useBlockContext.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,iBAAS,oBAAoB,IAAI,aAAa,CAS7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,IAAI,IAAI,CACrC,aAAa,EACX,OAAO,GACP,YAAY,GACZ,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,SAAS,GACT,iBAAiB,GACjB,OAAO,CACV,CAcA;AAED,6EAA6E;AAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
@@ -25,6 +25,12 @@ function useTransportSnapshot() {
25
25
  * set `data-theme={theme}` on your root, gotcha #60), `blockId`,
26
26
  * `blockInstanceId`, and `appId`.
27
27
  *
28
+ * `theme` is LIVE: it starts at the `BLOCK_INIT` (or URL-fragment) value and
29
+ * then tracks the host's `THEME_CHANGE` push when the viewer toggles light/dark
30
+ * mid-session. Reading it here is enough — {@link useBlockTheme} is the same
31
+ * value, narrower. Against a host that never pushes it, the value simply never
32
+ * moves (today's behaviour).
33
+ *
28
34
  * @example
29
35
  * const { ready, context, viewer, theme, settings } = useBlockContext();
30
36
  * if (!ready) return <div>Loading…</div>;
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockContext.js","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD;;;GAGG;AACH,SAAS,oBAAoB;IAC3B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,OAAO,oBAAoB,CACzB,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,EAC/B,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE;IAC7B,iEAAiE;IACjE,mEAAmE;IACnE,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,CAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,eAAe;IAa7B,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;IACpC,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"useBlockContext.js","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD;;;GAGG;AACH,SAAS,oBAAoB;IAC3B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,OAAO,oBAAoB,CACzB,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,EAC/B,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE;IAC7B,iEAAiE;IACjE,mEAAmE;IACnE,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,CAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,eAAe;IAa7B,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;IACpC,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { Theme } from '@civitai/app-sdk/blocks';
2
+ /**
3
+ * The host's CURRENT site theme (`'light' | 'dark'`), kept live for the whole
4
+ * life of the block ON THE IFRAME TRANSPORT.
5
+ *
6
+ * Reads the SAME singleton transport snapshot {@link useBlockContext} does, so
7
+ * it re-renders when the value changes. Three things can set it, in increasing
8
+ * order of authority:
9
+ *
10
+ * 1. the iframe URL fragment fast path (`#civitai-block=v1&theme=…`), read
11
+ * synchronously at construction, BEFORE any message — this is why a block
12
+ * can paint its first frame in the right theme;
13
+ * 2. `BLOCK_INIT` (authoritative — replaces the whole snapshot);
14
+ * 3. `THEME_CHANGE`, the host's push when the viewer toggles light/dark WHILE
15
+ * the block is mounted. Without it a mounted block kept its mount-time
16
+ * theme until reloaded: `BLOCK_INIT` is deduped by the transport and the
17
+ * URL fragment is frozen at mount, so neither can carry a later value.
18
+ *
19
+ * BEFORE `BLOCK_INIT` (and with no fragment) this returns the snapshot's
20
+ * `'light'` sentinel, exactly like `useBlockContext().theme`. Gate first paint
21
+ * on `useBlockContext().ready` if that matters to you.
22
+ *
23
+ * 🔴 OLD HOST: a host that never sends `THEME_CHANGE` simply never moves the
24
+ * value — the hook degrades to today's mount-time-constant behaviour. Nothing
25
+ * here awaits a message, so there is no hang and no timeout.
26
+ *
27
+ * 🔴 INLINE TRANSPORT: the value is FROZEN at the bootstrap theme. v1 inline
28
+ * mode receives no host pushes at all (`InlineTransport.onMessage` is a stub and
29
+ * `subscribe` is a no-op, so nothing can emit), exactly the way
30
+ * {@link useBlockResize} is a no-op there. Same degradation as an old host —
31
+ * correct first paint, no live toggle — and it lifts when v2 inline mode lands.
32
+ *
33
+ * @example
34
+ * // The host cannot reach into your iframe's DOM — put the theme on YOUR root.
35
+ * const theme = useBlockTheme();
36
+ * return <div data-theme={theme}>…</div>;
37
+ */
38
+ export declare function useBlockTheme(): Theme;
39
+ //# sourceMappingURL=useBlockTheme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBlockTheme.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,aAAa,IAAI,KAAK,CAErC"}
@@ -0,0 +1,41 @@
1
+ import { useTransportSnapshot } from './useBlockContext.js';
2
+ /**
3
+ * The host's CURRENT site theme (`'light' | 'dark'`), kept live for the whole
4
+ * life of the block ON THE IFRAME TRANSPORT.
5
+ *
6
+ * Reads the SAME singleton transport snapshot {@link useBlockContext} does, so
7
+ * it re-renders when the value changes. Three things can set it, in increasing
8
+ * order of authority:
9
+ *
10
+ * 1. the iframe URL fragment fast path (`#civitai-block=v1&theme=…`), read
11
+ * synchronously at construction, BEFORE any message — this is why a block
12
+ * can paint its first frame in the right theme;
13
+ * 2. `BLOCK_INIT` (authoritative — replaces the whole snapshot);
14
+ * 3. `THEME_CHANGE`, the host's push when the viewer toggles light/dark WHILE
15
+ * the block is mounted. Without it a mounted block kept its mount-time
16
+ * theme until reloaded: `BLOCK_INIT` is deduped by the transport and the
17
+ * URL fragment is frozen at mount, so neither can carry a later value.
18
+ *
19
+ * BEFORE `BLOCK_INIT` (and with no fragment) this returns the snapshot's
20
+ * `'light'` sentinel, exactly like `useBlockContext().theme`. Gate first paint
21
+ * on `useBlockContext().ready` if that matters to you.
22
+ *
23
+ * 🔴 OLD HOST: a host that never sends `THEME_CHANGE` simply never moves the
24
+ * value — the hook degrades to today's mount-time-constant behaviour. Nothing
25
+ * here awaits a message, so there is no hang and no timeout.
26
+ *
27
+ * 🔴 INLINE TRANSPORT: the value is FROZEN at the bootstrap theme. v1 inline
28
+ * mode receives no host pushes at all (`InlineTransport.onMessage` is a stub and
29
+ * `subscribe` is a no-op, so nothing can emit), exactly the way
30
+ * {@link useBlockResize} is a no-op there. Same degradation as an old host —
31
+ * correct first paint, no live toggle — and it lifts when v2 inline mode lands.
32
+ *
33
+ * @example
34
+ * // The host cannot reach into your iframe's DOM — put the theme on YOUR root.
35
+ * const theme = useBlockTheme();
36
+ * return <div data-theme={theme}>…</div>;
37
+ */
38
+ export function useBlockTheme() {
39
+ return useTransportSnapshot().theme;
40
+ }
41
+ //# sourceMappingURL=useBlockTheme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBlockTheme.js","sourceRoot":"","sources":["../../src/hooks/useBlockTheme.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,oBAAoB,EAAE,CAAC,KAAK,CAAC;AACtC,CAAC"}
@@ -150,12 +150,24 @@ interface UseBuzzWorkflowReturn {
150
150
  * `estimate`/`submit` take a full {@link WorkflowBody} — the discriminated
151
151
  * union keyed by `kind`, with THREE members as of `@civitai/app-sdk@0.30.0`:
152
152
  * a `textToImage` body (`{ kind, modelId, modelVersionId, params }`), a
153
- * `customComfy` recipe body (`{ kind, recipe, params }`), or a `step` body
153
+ * `customComfy` body (`kind: 'customComfy'`), or a `step` body
154
154
  * (`{ kind: 'step', step, params }` — a server-registered orchestrator step
155
155
  * such as `'chat-completion'`), never a bare `{ prompt }`. The hook forwards
156
156
  * the body to the host verbatim and never reads variant-specific fields, so
157
157
  * every member flows through unchanged, including any member added later.
158
158
  *
159
+ * 🔴 `customComfy` IS ITSELF A UNION, on `mode` — an app CAN ship its own
160
+ * ComfyUI graph. `WorkflowBodyCustomComfyRecipe` (`mode` omitted or `'recipe'`)
161
+ * names a server-registered recipe; `WorkflowBodyCustomComfyInline`
162
+ * (`mode: 'inline'`) carries the graph itself, plus its declared AIR
163
+ * `resources` and a `maxBuzz` bound. The inline arm is LIVE in production
164
+ * (developer-only) and this comment used to describe `customComfy` as a
165
+ * recipe-only `{ kind, recipe, params }` shape — written when that was true and
166
+ * never revisited once the arm shipped. A developer working against the live
167
+ * feature read the equivalent claim on the type, believed it over their own
168
+ * instinct, and concluded the capability did not exist. `@civitai/app-sdk`
169
+ * 0.30.0 predates the inline arm; the union above is otherwise unchanged.
170
+ *
159
171
  * @returns `{ estimate, submit, poll, watch, cancel, status, result, error }`.
160
172
  *
161
173
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"useBuzzWorkflow.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AA8BnG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAW7C,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACrD;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyBD,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACjE,MAAM,EAAE,CACN,IAAI,EAAE,YAAY,EAClB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC;;;OAGG;IACH,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7D;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,KAAK,EAAE,CACL,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/D,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACrC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,eAAe,IAAI,qBAAqB,CA+MvD"}
1
+ {"version":3,"file":"useBuzzWorkflow.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AA8BnG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAW7C,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACrD;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyBD,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACjE,MAAM,EAAE,CACN,IAAI,EAAE,YAAY,EAClB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC;;;OAGG;IACH,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7D;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,KAAK,EAAE,CACL,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/D,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACrC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,eAAe,IAAI,qBAAqB,CA+MvD"}
@@ -94,12 +94,24 @@ function sleep(ms, signal) {
94
94
  * `estimate`/`submit` take a full {@link WorkflowBody} — the discriminated
95
95
  * union keyed by `kind`, with THREE members as of `@civitai/app-sdk@0.30.0`:
96
96
  * a `textToImage` body (`{ kind, modelId, modelVersionId, params }`), a
97
- * `customComfy` recipe body (`{ kind, recipe, params }`), or a `step` body
97
+ * `customComfy` body (`kind: 'customComfy'`), or a `step` body
98
98
  * (`{ kind: 'step', step, params }` — a server-registered orchestrator step
99
99
  * such as `'chat-completion'`), never a bare `{ prompt }`. The hook forwards
100
100
  * the body to the host verbatim and never reads variant-specific fields, so
101
101
  * every member flows through unchanged, including any member added later.
102
102
  *
103
+ * 🔴 `customComfy` IS ITSELF A UNION, on `mode` — an app CAN ship its own
104
+ * ComfyUI graph. `WorkflowBodyCustomComfyRecipe` (`mode` omitted or `'recipe'`)
105
+ * names a server-registered recipe; `WorkflowBodyCustomComfyInline`
106
+ * (`mode: 'inline'`) carries the graph itself, plus its declared AIR
107
+ * `resources` and a `maxBuzz` bound. The inline arm is LIVE in production
108
+ * (developer-only) and this comment used to describe `customComfy` as a
109
+ * recipe-only `{ kind, recipe, params }` shape — written when that was true and
110
+ * never revisited once the arm shipped. A developer working against the live
111
+ * feature read the equivalent claim on the type, believed it over their own
112
+ * instinct, and concluded the capability did not exist. `@civitai/app-sdk`
113
+ * 0.30.0 predates the inline arm; the union above is otherwise unchanged.
114
+ *
103
115
  * @returns `{ estimate, submit, poll, watch, cancel, status, result, error }`.
104
116
  *
105
117
  * @example
@@ -1 +1 @@
1
- {"version":3,"file":"useBuzzWorkflow.js","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useBuzzWorkflow.js","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import type { ConsentUnavailablePayload } from '@civitai/app-sdk/blocks';
2
+ export type { ConsentUnavailablePayload };
3
+ /** What {@link useConsentUnavailable} returns. */
4
+ export interface UseConsentUnavailable {
5
+ /**
6
+ * The most recent `CONSENT_UNAVAILABLE` push, or `null` if the host has not
7
+ * refused (the normal case — nothing to render differently).
8
+ *
9
+ * 🔴 `refusal.scopes` CAN BE EMPTY and that is still a refusal. The host
10
+ * decides to refuse on its own UNFILTERED un-grantable set, then filters the
11
+ * names it puts on the wire down to the public block-scope vocabulary — so a
12
+ * request naming nothing the platform recognises refuses with `scopes: []`.
13
+ * Branch on `refusal !== null`; use `refusal.scopes` only to word the copy.
14
+ */
15
+ refusal: ConsentUnavailablePayload | null;
16
+ /**
17
+ * Clear the stored refusal — this hook's state AND the buffered copy.
18
+ *
19
+ * A refusal is scoped to the scopes that were asked for, not to the block, so
20
+ * it must not latch forever: a block that is refused `ai:write:budgeted` may
21
+ * still legitimately request something else later, and a long-lived block
22
+ * whose host re-inits (sign-in, a new token) can find itself grantable again.
23
+ * Call this when you move on from the refused action.
24
+ *
25
+ * Clearing the BUFFER too is load-bearing, not tidiness: the refusal is
26
+ * retained across mounts (see the hook docblock), so a `reset()` that cleared
27
+ * only local state would be silently undone the next time this hook mounted —
28
+ * the documented "Try again" button would put the refusal straight back.
29
+ */
30
+ reset: () => void;
31
+ }
32
+ /**
33
+ * Subscribe to the host's `CONSENT_UNAVAILABLE` push — the signal that a
34
+ * `REQUEST_CONSENT` this block sent can **never** be granted in this
35
+ * environment, because the scope was clamped or withheld at mint. Distinct from
36
+ * "the viewer hasn't confirmed the dialog yet", which produces no message at
37
+ * all.
38
+ *
39
+ * This is the whole point of the message: without it a block keeps telling the
40
+ * user *"Confirm in the Civitai dialog. If you dismissed it, click Generate
41
+ * again"* while the host says the permission is unavailable — two contradictory
42
+ * messages on one screen, and the misleading one is the block's.
43
+ *
44
+ * 🔴 **PRECONDITION — pass `scopes` to `requestConsent()`, with a real scope
45
+ * name in it.** The refusal is computed from the request's `scopes` hint, and
46
+ * both dev hosts and the real host (`resolveUngrantableConsentNotice`) return
47
+ * "no notice" unless that hint is an array holding AT LEAST ONE NON-EMPTY
48
+ * STRING. Measured on this package's implementation: `undefined`, a non-array,
49
+ * `[]`, `['']` and `[1, 2]` ALL yield `notify: false`, and the real host applies
50
+ * the same `requested.length === 0` guard. So `requestConsent({ scopes: [] })`
51
+ * follows the letter of "always pass scopes" and still produces silence — no
52
+ * grant, no refusal, no error — and this hook sits at `null` forever. The reason
53
+ * is that without a named scope proven un-grantable there is no way to
54
+ * distinguish "never" from "not confirmed yet", and guessing is what produced
55
+ * the contradictory screen. Call `requestConsent({ scopes: ['ai:write:budgeted'] })`.
56
+ *
57
+ * 🔴 **UNCORRELATED, AND THAT IS THE SHAPE OF THE PUBLIC API — not an oversight
58
+ * that can be fixed later.** `REQUEST_CONSENT` carries no `requestId`, so
59
+ * `CONSENT_UNAVAILABLE` is a fire-and-forget push and not a reply. Two
60
+ * consequences a block author has to design around: EVERY mounted
61
+ * `useConsentUnavailable()` observes EVERY refusal, and there is no reliable way
62
+ * to filter one out — `scopes` cannot serve as a correlation key because it may
63
+ * legitimately be `[]` and is in any case only advisory. If two independent
64
+ * parts of your block request different scopes, both will see both refusals;
65
+ * keep the request and its refusal UI in one component, or track which request
66
+ * is outstanding yourself. Nothing here awaits anything — against a host that
67
+ * never sends the message the hook simply stays `null`, which is today's
68
+ * behaviour.
69
+ *
70
+ * 🔴 **A refusal is BUFFERED, so one that arrives while this hook is unmounted
71
+ * is not lost.** The transport delivers an unsolicited push only to handlers
72
+ * registered at the moment it arrives, so without a buffer a refusal that landed
73
+ * before mount (or between unmount and remount) vanished — and a dropped refusal
74
+ * puts back the two-message screen above. `requestConsent()` arms the buffer as
75
+ * it sends, and a mounting hook seeds from it. The buffer holds at most the
76
+ * LATEST refusal, is discarded when the block token changes (its premise is that
77
+ * token's scopes), and is cleared by `reset()`. See
78
+ * `internal/consentRefusalLatch.ts`.
79
+ *
80
+ * The subscription goes through the transport, so the handler runs only after
81
+ * the message has cleared the origin allowlist AND the payload validator; the
82
+ * hook never adds its own `window` listener behind that boundary.
83
+ *
84
+ * @example
85
+ * const { requestConsent } = useRequestConsent();
86
+ * const { refusal } = useConsentUnavailable();
87
+ *
88
+ * if (refusal) return <p>Generating isn't available on this page.</p>;
89
+ * return <button onClick={() => requestConsent({ scopes: ['ai:write:budgeted'] })}>Generate</button>;
90
+ */
91
+ export declare function useConsentUnavailable(): UseConsentUnavailable;
92
+ //# sourceMappingURL=useConsentUnavailable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useConsentUnavailable.d.ts","sourceRoot":"","sources":["../../src/hooks/useConsentUnavailable.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAUzE,YAAY,EAAE,yBAAyB,EAAE,CAAC;AAE1C,kDAAkD;AAClD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;OASG;IACH,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC1C;;;;;;;;;;;;;OAaG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAgB,qBAAqB,IAAI,qBAAqB,CA+B7D"}
@@ -0,0 +1,93 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { armConsentRefusalLatch, clearConsentRefusalLatch, readConsentRefusalLatch, } from '../internal/consentRefusalLatch.js';
3
+ import { getTransport } from '../internal/singleton.js';
4
+ import { subscribeTyped } from '../internal/transport.js';
5
+ /**
6
+ * Subscribe to the host's `CONSENT_UNAVAILABLE` push — the signal that a
7
+ * `REQUEST_CONSENT` this block sent can **never** be granted in this
8
+ * environment, because the scope was clamped or withheld at mint. Distinct from
9
+ * "the viewer hasn't confirmed the dialog yet", which produces no message at
10
+ * all.
11
+ *
12
+ * This is the whole point of the message: without it a block keeps telling the
13
+ * user *"Confirm in the Civitai dialog. If you dismissed it, click Generate
14
+ * again"* while the host says the permission is unavailable — two contradictory
15
+ * messages on one screen, and the misleading one is the block's.
16
+ *
17
+ * 🔴 **PRECONDITION — pass `scopes` to `requestConsent()`, with a real scope
18
+ * name in it.** The refusal is computed from the request's `scopes` hint, and
19
+ * both dev hosts and the real host (`resolveUngrantableConsentNotice`) return
20
+ * "no notice" unless that hint is an array holding AT LEAST ONE NON-EMPTY
21
+ * STRING. Measured on this package's implementation: `undefined`, a non-array,
22
+ * `[]`, `['']` and `[1, 2]` ALL yield `notify: false`, and the real host applies
23
+ * the same `requested.length === 0` guard. So `requestConsent({ scopes: [] })`
24
+ * follows the letter of "always pass scopes" and still produces silence — no
25
+ * grant, no refusal, no error — and this hook sits at `null` forever. The reason
26
+ * is that without a named scope proven un-grantable there is no way to
27
+ * distinguish "never" from "not confirmed yet", and guessing is what produced
28
+ * the contradictory screen. Call `requestConsent({ scopes: ['ai:write:budgeted'] })`.
29
+ *
30
+ * 🔴 **UNCORRELATED, AND THAT IS THE SHAPE OF THE PUBLIC API — not an oversight
31
+ * that can be fixed later.** `REQUEST_CONSENT` carries no `requestId`, so
32
+ * `CONSENT_UNAVAILABLE` is a fire-and-forget push and not a reply. Two
33
+ * consequences a block author has to design around: EVERY mounted
34
+ * `useConsentUnavailable()` observes EVERY refusal, and there is no reliable way
35
+ * to filter one out — `scopes` cannot serve as a correlation key because it may
36
+ * legitimately be `[]` and is in any case only advisory. If two independent
37
+ * parts of your block request different scopes, both will see both refusals;
38
+ * keep the request and its refusal UI in one component, or track which request
39
+ * is outstanding yourself. Nothing here awaits anything — against a host that
40
+ * never sends the message the hook simply stays `null`, which is today's
41
+ * behaviour.
42
+ *
43
+ * 🔴 **A refusal is BUFFERED, so one that arrives while this hook is unmounted
44
+ * is not lost.** The transport delivers an unsolicited push only to handlers
45
+ * registered at the moment it arrives, so without a buffer a refusal that landed
46
+ * before mount (or between unmount and remount) vanished — and a dropped refusal
47
+ * puts back the two-message screen above. `requestConsent()` arms the buffer as
48
+ * it sends, and a mounting hook seeds from it. The buffer holds at most the
49
+ * LATEST refusal, is discarded when the block token changes (its premise is that
50
+ * token's scopes), and is cleared by `reset()`. See
51
+ * `internal/consentRefusalLatch.ts`.
52
+ *
53
+ * The subscription goes through the transport, so the handler runs only after
54
+ * the message has cleared the origin allowlist AND the payload validator; the
55
+ * hook never adds its own `window` listener behind that boundary.
56
+ *
57
+ * @example
58
+ * const { requestConsent } = useRequestConsent();
59
+ * const { refusal } = useConsentUnavailable();
60
+ *
61
+ * if (refusal) return <p>Generating isn't available on this page.</p>;
62
+ * return <button onClick={() => requestConsent({ scopes: ['ai:write:budgeted'] })}>Generate</button>;
63
+ */
64
+ export function useConsentUnavailable() {
65
+ const [refusal, setRefusal] = useState(null);
66
+ useEffect(() => {
67
+ const transport = getTransport();
68
+ // Keep recording even while nothing is mounted, and seed from anything
69
+ // recorded before this mount. Seeding in the effect (not lazy `useState`)
70
+ // is deliberate: the latch is armed here too, so the read has to happen
71
+ // after the arm, and an effect is the only place both can be ordered.
72
+ armConsentRefusalLatch(transport);
73
+ const buffered = readConsentRefusalLatch(transport);
74
+ if (buffered)
75
+ setRefusal(buffered);
76
+ // `subscribeTyped` (not the raw `onMessage`) is what makes `payload` a
77
+ // `ConsentUnavailablePayload` here instead of `unknown` + a cast at the call
78
+ // site — the cast is unchecked, so a payload shape change would compile
79
+ // straight through it in every consuming block.
80
+ return subscribeTyped(transport, 'CONSENT_UNAVAILABLE', (payload) => {
81
+ // Store the payload UNCONDITIONALLY. No `payload.scopes.length` gate: an
82
+ // empty `scopes` is a legitimate refusal (see `UseConsentUnavailable`),
83
+ // and gating on it would drop the message this hook exists to deliver.
84
+ setRefusal(payload);
85
+ });
86
+ }, []);
87
+ const reset = useCallback(() => {
88
+ clearConsentRefusalLatch();
89
+ setRefusal(null);
90
+ }, []);
91
+ return { refusal, reset };
92
+ }
93
+ //# sourceMappingURL=useConsentUnavailable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useConsentUnavailable.js","sourceRoot":"","sources":["../../src/hooks/useConsentUnavailable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAIzD,OAAO,EACL,sBAAsB,EACtB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAkC1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAmC,IAAI,CAAC,CAAC;IAE/E,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;QACjC,uEAAuE;QACvE,0EAA0E;QAC1E,wEAAwE;QACxE,sEAAsE;QACtE,sBAAsB,CAAC,SAAS,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;QACpD,IAAI,QAAQ;YAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;QAEnC,uEAAuE;QACvE,6EAA6E;QAC7E,wEAAwE;QACxE,gDAAgD;QAChD,OAAO,cAAc,CAAC,SAAS,EAAE,qBAAqB,EAAE,CAAC,OAAO,EAAE,EAAE;YAClE,yEAAyE;YACzE,wEAAwE;YACxE,uEAAuE;YACvE,UAAU,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC,CAAC,CAAC;IACL,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,wBAAwB,EAAE,CAAC;QAC3B,UAAU,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC"}
@@ -7,9 +7,27 @@
7
7
  * the message like every inbound one (origin + `event.source` pinned, only
8
8
  * honored after BLOCK_READY) and opens its consent UI.
9
9
  *
10
- * `scopes` is an optional advisory hint of which scopes the action needs; the
11
- * host independently grants the missing set it computed at mint, so the block
12
- * can omit it.
10
+ * `scopes` is an advisory hint of which scopes the action needs. It is optional
11
+ * in the signature, and for the GRANT path it genuinely is: the host
12
+ * independently grants the missing set it computed at mint, so a bare
13
+ * `requestConsent()` still opens the consent dialog.
14
+ *
15
+ * 🔴 BUT OMITTING IT DISABLES THE REFUSAL PATH ENTIRELY — pass `scopes`, AND
16
+ * PUT A REAL SCOPE NAME IN IT. The host's `CONSENT_UNAVAILABLE` push (see
17
+ * `useConsentUnavailable`) is computed FROM this hint, and the bar is higher
18
+ * than "an array is present": `resolveUngrantableConsentNotice` returns "no
19
+ * notice" unless the hint is an array containing AT LEAST ONE NON-EMPTY STRING.
20
+ * `undefined`, a non-array, `[]`, `['']` and `[1, 2]` all produce silence —
21
+ * measured on this package's implementation, and the real host has the identical
22
+ * `requested.length === 0` guard, so `requestConsent({ scopes: [] })` obeys the
23
+ * instruction above and STILL gets nothing back. That is deliberate — without a
24
+ * named scope proven un-grantable there is no way to distinguish "can NEVER be
25
+ * granted here" from "the viewer hasn't confirmed the dialog yet", and guessing
26
+ * is what produced the contradictory two-message screen this path exists to
27
+ * remove. The consequence for a block author: call `requestConsent()` bare (or
28
+ * with an empty hint) on an un-grantable surface and you get SILENCE — no grant,
29
+ * no refusal, no error — which reads as a broken message rather than a thin
30
+ * argument.
13
31
  *
14
32
  * Fire-and-forget: the host doesn't reply. On grant the host re-mints the block
15
33
  * token and pushes a TOKEN_REFRESH carrying the now-granted scopes — observe