@civitai/blocks-react 0.53.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 { isModelSlotContext } from '@civitai/app-sdk/blocks';
30
+ import { isModelSlotContext, isSignedIn } from '@civitai/app-sdk/blocks';
31
31
 
32
32
  export function App() {
33
33
  const { ready, context, viewer, theme } = useBlockContext();
@@ -35,7 +35,9 @@ export function App() {
35
35
  const rootRef = useRef<HTMLDivElement>(null);
36
36
  useBlockResize(rootRef); // host fits the iframe to content
37
37
 
38
- if (!ready) return <div ref={rootRef}>Loading…</div>;
38
+ // No ref on the pre-init skeleton — useBlockResize observes the real root
39
+ // whenever it mounts, including on a later render.
40
+ if (!ready) return <div>Loading…</div>;
39
41
  // `context` is a union keyed on slotId — narrow with the guard, not a cast.
40
42
  if (!isModelSlotContext(context)) return <div ref={rootRef}>Wrong slot.</div>;
41
43
 
@@ -43,7 +45,8 @@ export function App() {
43
45
  // GOTCHA #60: set data-theme on YOUR OWN root — the host can't reach into
44
46
  // the iframe to set it. Without this any [data-theme="dark"] CSS is dormant.
45
47
  <div ref={rootRef} data-theme={theme}>
46
- <p>Block for model {context.modelName} ({viewer ? 'signed in' : 'anon'})</p>
48
+ {/* Sign-in gate: call `isSignedIn`, never an identity read. */}
49
+ <p>Block for model {context.modelName} ({isSignedIn(viewer) ? 'signed in' : 'anon'})</p>
47
50
  {/* `/ui` Button — themed by the data-theme above; `loading` disables + shows a spinner */}
48
51
  <Button
49
52
  loading={status === 'submitting' || status === 'polling'}
@@ -111,7 +114,14 @@ const { ready, context, viewer, theme, settings, blockId, blockInstanceId, appId
111
114
 
112
115
  - `context` — `BlockContext` (`{ slotId, … }`); narrow to `ModelSlotContext` for
113
116
  model-page slots.
114
- - `viewer` — `ViewerInfo | null` (`null` = anonymous).
117
+ - `viewer` — `ViewerInfo | null` (`null` = anonymous). **Gate sign-in with
118
+ `isSignedIn(viewer)`** (from `@civitai/app-sdk/blocks`), never on
119
+ `viewer.id`/`viewer.username` (both `@deprecated`). Don't open-code the gate:
120
+ the SDK owns which spelling is correct — `signedIn` is optional on the wire
121
+ and is the one viewer field the init validator deliberately does not reject
122
+ when malformed, so `isSignedIn` answers from presence instead. Hover it for
123
+ the full reasoning. Need the identity itself? Use
124
+ [`useViewer()`](#useviewer) — scope-gated and audited per call.
115
125
  - `theme` — `'light' | 'dark'`. **Set `data-theme={theme}` on your root** (gotcha #60).
116
126
  LIVE: it starts at the `BLOCK_INIT` value and then tracks the host's
117
127
  `THEME_CHANGE` push when the viewer toggles dark mode mid-session — see
@@ -153,6 +163,12 @@ const rootRef = useRef<HTMLDivElement>(null);
153
163
  useBlockResize(rootRef);
154
164
  ```
155
165
 
166
+ **The element may mount on a later render, and that is the normal case** — a
167
+ block renders a skeleton until `BLOCK_INIT` lands. The hook keys on the observed
168
+ *element*, so you do **not** need to pin the same `ref` to every branch of a
169
+ loading/ready conditional to keep the host resizing. Put it on the root you
170
+ actually want measured, in whichever branch renders it.
171
+
156
172
  > Also set `iframe.minHeight` in your manifest to the block's *real* rendered
157
173
  > height — a too-small minHeight makes the iframe seed short and grow-jump on
158
174
  > `BLOCK_READY` (CLS). Measure it in the dev harness (gotcha #53).
@@ -676,18 +692,72 @@ async function onCancel(id: string) {
676
692
 
677
693
  ### `useAppStorage()`
678
694
 
679
- Per-(block instance, viewer) KV datastore, host-mediated. 64 KB per value,
680
- 50 MB + ~1M rows per app.
695
+ KV datastore, host-mediated. Keys are **namespaced** per (block instance,
696
+ viewer); the byte and row **budgets** are enforced per (**app**, viewer), so
697
+ every instance of one app shares one budget for that viewer.
681
698
 
682
699
  ```tsx
700
+ import {
701
+ APP_STORAGE_MAX_VALUE_BYTES, // largest single value, in wire bytes
702
+ APP_STORAGE_MAX_BYTES, // total stored bytes per (app, viewer)
703
+ APP_STORAGE_MAX_ROWS, // total rows per (app, viewer)
704
+ } from '@civitai/app-sdk/blocks';
705
+
683
706
  const storage = useAppStorage();
684
- await storage.set('key', { any: 'json' }); // throws "PAYLOAD_TOO_LARGE" over a limit
707
+ await storage.set('key', { any: 'json' }); // rejects over ANY of the three ceilings
685
708
  const v = await storage.get<MyShape>('key'); // null if unset / anon
686
709
  await storage.delete('key'); // idempotent
687
710
  const { keys } = await storage.list({ prefix: 'note-' });
688
711
  const quota = await storage.getQuota(); // { usedBytes, rowCount, limitBytes, limitRows }
689
712
  ```
690
713
 
714
+ 🔴 **`getQuota()` is the authority; the constants are a snapshot.** These three
715
+ are the ceilings **as of the version of `@civitai/app-sdk` you installed** —
716
+ compiled-in figures, which is the same frozen-number failure mode this page
717
+ used to demonstrate, just with one copy instead of nine. The host can move a
718
+ ceiling without your lockfile changing. So:
719
+
720
+ - **Render `getQuota()`'s reply**, never a constant, anywhere a viewer sees a
721
+ number or a code path decides whether a write will fit.
722
+ - **Reach for the constants only where no quota reply is available** — a
723
+ build-time sanity check, a test fixture, a rough design-time estimate — and
724
+ treat the answer as "roughly, at install time".
725
+ - **Re-check after any SDK bump**, and expect movement: the per-viewer clamp
726
+ was sized against a measured distribution and the host says to expect a
727
+ re-measure. `appStorageLimits.ts` in `@civitai/app-sdk` carries the
728
+ provenance and a one-liner that re-derives the current values from the host.
729
+
730
+ Never hard-code a figure of your own: the docs here used to quote the app-wide
731
+ umbrella instead of the per-viewer clamp and were **25x** out on bytes and
732
+ **1000x** out on rows.
733
+
734
+ 🔴 **The ROW ceiling is usually the binding one, and a byte-based "x of y used"
735
+ readout will not see it coming.** A block caching one modest record per item a
736
+ viewer touches exhausts `limitRows` while still holding a small fraction of
737
+ `limitBytes`. Show rows too.
738
+
739
+ `createMockHost()` defaults to these same ceilings and enforces the per-value
740
+ cap, the byte budget and — since it was added — the **row** budget on write, so
741
+ a row-limit overrun now fails under `dev:mock` where it previously passed and
742
+ failed only in production. Pass `storage: { quotaBytes, limitRows }` to
743
+ simulate something smaller.
744
+
745
+ ⚠️ The mock is **not** gate-for-gate identical to the host. Three known
746
+ divergences:
747
+
748
+ - the error string a rejection carries
749
+ ([#343](https://github.com/civitai/civitai-app-starters/issues/343));
750
+ - the byte gate refusing a shrinking overwrite that the host admits
751
+ ([#345](https://github.com/civitai/civitai-app-starters/issues/345));
752
+ - 🔴 the byte gate counting **wire** bytes where the host counts **stored**
753
+ bytes — `octet_length(value::jsonb::text)`, larger for every container, up to
754
+ ~1.5x ([#347](https://github.com/civitai/civitai-app-starters/issues/347)).
755
+
756
+ Passing under `dev:mock` is evidence, not proof — and note the third one is
757
+ **permissive**: unlike the other two, it lets a write pass locally that
758
+ production will reject. Size your fixtures against `getQuota()`, not against
759
+ what the mock accepted.
760
+
691
761
  ### `useSharedStorage()`
692
762
 
693
763
  App-scoped, append-only, community-votable SHARED datastore (every viewer sees
@@ -1039,7 +1109,7 @@ import {
1039
1109
  export function App() {
1040
1110
  const { ready, theme } = useBlockContext();
1041
1111
  const rootRef = useRef<HTMLDivElement>(null);
1042
- if (!ready) return <div ref={rootRef}>Loading…</div>;
1112
+ if (!ready) return <div>Loading…</div>;
1043
1113
 
1044
1114
  return (
1045
1115
  // GOTCHA #60 — theme your OWN root; that's what the pack reads.
@@ -14,9 +14,23 @@ export interface AppStorageListResult {
14
14
  export interface AppStorageQuota {
15
15
  usedBytes: number;
16
16
  rowCount: number;
17
- /** Host-enforced ceiling (bytes). Surface in UI so callers don't hard-code 50MB. */
17
+ /**
18
+ * Host-enforced byte ceiling for this (app, viewer). **Render THIS, never a
19
+ * hard-coded figure** — the ceiling moves, and a UI built on a literal goes
20
+ * quietly wrong rather than loudly wrong. Matches
21
+ * `APP_STORAGE_MAX_BYTES` from `@civitai/app-sdk/blocks` against the current
22
+ * host.
23
+ */
18
24
  limitBytes: number;
19
- /** Host-enforced row ceiling (~1M today). */
25
+ /**
26
+ * Host-enforced row ceiling for this (app, viewer). Same rule: render it.
27
+ * Matches `APP_STORAGE_MAX_ROWS`.
28
+ *
29
+ * 🔴 THIS IS USUALLY THE BINDING ONE. Rows are small; a block caching one
30
+ * modest record per item a viewer touches exhausts the row ceiling while
31
+ * still using a small fraction of `limitBytes`, so a byte-only "x of y used"
32
+ * readout will show plenty of headroom right up to the rejection.
33
+ */
20
34
  limitRows: number;
21
35
  }
22
36
  export interface UseAppStorage {
@@ -27,9 +41,11 @@ export interface UseAppStorage {
27
41
  */
28
42
  get<T = unknown>(key: string): Promise<T | null>;
29
43
  /**
30
- * Upsert a value. Resolves on host ack. Rejects with the host's
31
- * `error` string when the value exceeds 64KB, when the per-app 50MB
32
- * quota would be crossed, or when the viewer is anonymous.
44
+ * Upsert a value. Resolves on host ack. Rejects with the host's `error`
45
+ * string when the value exceeds `APP_STORAGE_MAX_VALUE_BYTES`, when the
46
+ * per-(app, viewer) byte or ROW ceiling would be crossed
47
+ * (`APP_STORAGE_MAX_BYTES` / `APP_STORAGE_MAX_ROWS`), or when the viewer is
48
+ * anonymous. All from `@civitai/app-sdk/blocks`.
33
49
  */
34
50
  set<T = unknown>(key: string, value: T): Promise<{
35
51
  ok: true;
@@ -54,8 +70,10 @@ export interface UseAppStorage {
54
70
  cursor?: string;
55
71
  }): Promise<AppStorageListResult>;
56
72
  /**
57
- * Diagnostic: current usage + the v0 ceilings. Build a "X of 50 MB used"
58
- * settings widget against this.
73
+ * Diagnostic: current usage + the host's ceilings. Build an "X of Y used"
74
+ * settings widget against this — taking **both** numbers from the reply, and
75
+ * showing ROWS as well as bytes (see {@link AppStorageQuota.limitRows} for
76
+ * why bytes alone mislead).
59
77
  */
60
78
  getQuota(): Promise<AppStorageQuota>;
61
79
  }
@@ -71,7 +89,20 @@ export interface UseAppStorage {
71
89
  * once the transport singleton is created, so it's safe to put in
72
90
  * dependency arrays of `useEffect` / `useMemo`.
73
91
  *
74
- * 64 KB per value, 50 MB + ~1M rows per app.
92
+ * 🔴 THE NAMESPACE AND THE BUDGET ARE SCOPED DIFFERENTLY. Keys are namespaced
93
+ * per (block instance, viewer) — the tuple above. The BYTE and ROW budgets
94
+ * (`APP_STORAGE_MAX_BYTES` / `APP_STORAGE_MAX_ROWS`, plus
95
+ * `APP_STORAGE_MAX_VALUE_BYTES` per value, all from `@civitai/app-sdk/blocks`)
96
+ * are enforced per (APP, viewer): every instance of the same app draws on ONE
97
+ * budget for that viewer.
98
+ *
99
+ * 🔴 THE CONSTANTS ARE A SNAPSHOT; `getQuota()` IS THE AUTHORITY. They are the
100
+ * ceilings as of the `@civitai/app-sdk` version you installed — a figure
101
+ * compiled into a published package is still a frozen figure, and the host can
102
+ * move a ceiling without your lockfile changing. Render `getQuota()`'s reply
103
+ * anywhere a viewer sees a number or a code path decides whether a write will
104
+ * fit; reach for a constant only where no reply is available (a test fixture, a
105
+ * design-time estimate), and re-check after an SDK bump.
75
106
  *
76
107
  * @example
77
108
  * const storage = useAppStorage();
@@ -1 +1 @@
1
- {"version":3,"file":"useAppStorage.d.ts","sourceRoot":"","sources":["../../src/hooks/useAppStorage.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjD;;;;OAIG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnF;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC7D;;;OAGG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAClC;;;OAGG;IACH,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,IAAI,aAAa,CAiF7C"}
1
+ {"version":3,"file":"useAppStorage.d.ts","sourceRoot":"","sources":["../../src/hooks/useAppStorage.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,kBAAkB,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnF;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC7D;;;OAGG;IACH,IAAI,CAAC,IAAI,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,aAAa,IAAI,aAAa,CAiF7C"}
@@ -14,7 +14,20 @@ import { sendTypedRequest } from '../internal/transport.js';
14
14
  * once the transport singleton is created, so it's safe to put in
15
15
  * dependency arrays of `useEffect` / `useMemo`.
16
16
  *
17
- * 64 KB per value, 50 MB + ~1M rows per app.
17
+ * 🔴 THE NAMESPACE AND THE BUDGET ARE SCOPED DIFFERENTLY. Keys are namespaced
18
+ * per (block instance, viewer) — the tuple above. The BYTE and ROW budgets
19
+ * (`APP_STORAGE_MAX_BYTES` / `APP_STORAGE_MAX_ROWS`, plus
20
+ * `APP_STORAGE_MAX_VALUE_BYTES` per value, all from `@civitai/app-sdk/blocks`)
21
+ * are enforced per (APP, viewer): every instance of the same app draws on ONE
22
+ * budget for that viewer.
23
+ *
24
+ * 🔴 THE CONSTANTS ARE A SNAPSHOT; `getQuota()` IS THE AUTHORITY. They are the
25
+ * ceilings as of the `@civitai/app-sdk` version you installed — a figure
26
+ * compiled into a published package is still a frozen figure, and the host can
27
+ * move a ceiling without your lockfile changing. Render `getQuota()`'s reply
28
+ * anywhere a viewer sees a number or a code path decides whether a write will
29
+ * fit; reach for a constant only where no reply is available (a test fixture, a
30
+ * design-time estimate), and re-check after an SDK bump.
18
31
  *
19
32
  * @example
20
33
  * const storage = useAppStorage();
@@ -1 +1 @@
1
- {"version":3,"file":"useAppStorage.js","sourceRoot":"","sources":["../../src/hooks/useAppStorage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useAppStorage.js","sourceRoot":"","sources":["../../src/hooks/useAppStorage.ts"],"names":[],"mappings":"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"}
@@ -25,10 +25,14 @@ declare function useTransportSnapshot(): BlockSnapshot;
25
25
  * moves (today's behaviour).
26
26
  *
27
27
  * @example
28
+ * import { isSignedIn } from '@civitai/app-sdk/blocks';
28
29
  * const { ready, context, viewer, theme, settings } = useBlockContext();
29
30
  * if (!ready) return <div>Loading…</div>;
30
31
  * // Set data-theme on YOUR root — the host can't reach into the iframe (gotcha #60).
31
- * return <div data-theme={theme}>Hi {viewer?.username ?? 'anon'}</div>;
32
+ * // Sign-in gate: call `isSignedIn`. NOT `viewer?.username` — an identity read
33
+ * // standing in for a presence check, on a field that is `@deprecated` and
34
+ * // scheduled for removal. This snippet used to do exactly that.
35
+ * return <div data-theme={theme}>{isSignedIn(viewer) ? 'Hi there' : 'Hi anon'}</div>;
32
36
  */
33
37
  export declare function useBlockContext(): Pick<BlockSnapshot, 'ready' | 'renderMode' | 'context' | 'token' | 'settings' | 'viewer' | 'theme' | 'blockId' | 'blockInstanceId' | 'appId'>;
34
38
  /** Re-exported so other hooks in this package can share the subscription. */
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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"}
@@ -32,10 +32,14 @@ function useTransportSnapshot() {
32
32
  * moves (today's behaviour).
33
33
  *
34
34
  * @example
35
+ * import { isSignedIn } from '@civitai/app-sdk/blocks';
35
36
  * const { ready, context, viewer, theme, settings } = useBlockContext();
36
37
  * if (!ready) return <div>Loading…</div>;
37
38
  * // Set data-theme on YOUR root — the host can't reach into the iframe (gotcha #60).
38
- * return <div data-theme={theme}>Hi {viewer?.username ?? 'anon'}</div>;
39
+ * // Sign-in gate: call `isSignedIn`. NOT `viewer?.username` — an identity read
40
+ * // standing in for a presence check, on a field that is `@deprecated` and
41
+ * // scheduled for removal. This snippet used to do exactly that.
42
+ * return <div data-theme={theme}>{isSignedIn(viewer) ? 'Hi there' : 'Hi anon'}</div>;
39
43
  */
40
44
  export function useBlockContext() {
41
45
  const snap = useTransportSnapshot();
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;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"}
@@ -9,11 +9,37 @@ import { type RefObject } from 'react';
9
9
  * - Inline path: `InlineTransport.sendMessage` is a no-op (the host DOM
10
10
  * reflows naturally), so the observer fires but no message goes out.
11
11
  *
12
+ * 🔴 THE ELEMENT MAY MOUNT ON A LATER RENDER, AND THAT IS THE NORMAL CASE.
13
+ * Every block renders a skeleton until `BLOCK_INIT` lands, so on the first
14
+ * render there is nothing to observe. This hook therefore keys on the OBSERVED
15
+ * ELEMENT, not on the ref wrapper's identity — the same reasoning written down
16
+ * at `useBlockBreakpoint.ts`'s effect, with one mechanical difference that
17
+ * matters:
18
+ *
19
+ * `useBlockBreakpoint` can compare `ref.current` read DURING RENDER, because
20
+ * it re-renders its own caller and so always gets another render in which to
21
+ * notice. This hook re-renders nobody. React attaches a ref during COMMIT,
22
+ * i.e. AFTER the render that mounts it, so a `[ref.current]` dependency read
23
+ * during render is a render behind and — with no further render coming —
24
+ * never catches up. Measured: with `[ref.current]` as the dependency, a
25
+ * component that mounts its root on the second render still observes
26
+ * nothing.
27
+ *
28
+ * So the effect runs on every render (no dependency array) and does its own
29
+ * identity check against the element it is already observing. The check is a
30
+ * reference compare; the observer is torn down and rebuilt only when the
31
+ * element actually changes.
32
+ *
33
+ * Because of this, a block does NOT need to pin the same `ref` to every branch
34
+ * of a loading/ready conditional to keep the host resizing. That workaround was
35
+ * load-bearing before this fix and is not any more.
36
+ *
12
37
  * @param ref - Ref to the block's root DOM element to observe.
13
38
  *
14
39
  * @example
15
40
  * const rootRef = useRef<HTMLDivElement>(null);
16
41
  * useBlockResize(rootRef); // host fits the iframe to content
42
+ * if (!ready) return <div>Loading…</div>; // no ref needed on this branch
17
43
  * return <div ref={rootRef}>…</div>;
18
44
  */
19
45
  export declare function useBlockResize(ref: RefObject<HTMLElement | null>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockResize.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIlD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAiBvE"}
1
+ {"version":3,"file":"useBlockResize.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAI1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAyCvE"}
@@ -1,4 +1,4 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { getTransport } from '../internal/singleton.js';
3
3
  /**
4
4
  * Observes the referenced element's height and asks the host to resize on
@@ -10,20 +10,67 @@ import { getTransport } from '../internal/singleton.js';
10
10
  * - Inline path: `InlineTransport.sendMessage` is a no-op (the host DOM
11
11
  * reflows naturally), so the observer fires but no message goes out.
12
12
  *
13
+ * 🔴 THE ELEMENT MAY MOUNT ON A LATER RENDER, AND THAT IS THE NORMAL CASE.
14
+ * Every block renders a skeleton until `BLOCK_INIT` lands, so on the first
15
+ * render there is nothing to observe. This hook therefore keys on the OBSERVED
16
+ * ELEMENT, not on the ref wrapper's identity — the same reasoning written down
17
+ * at `useBlockBreakpoint.ts`'s effect, with one mechanical difference that
18
+ * matters:
19
+ *
20
+ * `useBlockBreakpoint` can compare `ref.current` read DURING RENDER, because
21
+ * it re-renders its own caller and so always gets another render in which to
22
+ * notice. This hook re-renders nobody. React attaches a ref during COMMIT,
23
+ * i.e. AFTER the render that mounts it, so a `[ref.current]` dependency read
24
+ * during render is a render behind and — with no further render coming —
25
+ * never catches up. Measured: with `[ref.current]` as the dependency, a
26
+ * component that mounts its root on the second render still observes
27
+ * nothing.
28
+ *
29
+ * So the effect runs on every render (no dependency array) and does its own
30
+ * identity check against the element it is already observing. The check is a
31
+ * reference compare; the observer is torn down and rebuilt only when the
32
+ * element actually changes.
33
+ *
34
+ * Because of this, a block does NOT need to pin the same `ref` to every branch
35
+ * of a loading/ready conditional to keep the host resizing. That workaround was
36
+ * load-bearing before this fix and is not any more.
37
+ *
13
38
  * @param ref - Ref to the block's root DOM element to observe.
14
39
  *
15
40
  * @example
16
41
  * const rootRef = useRef<HTMLDivElement>(null);
17
42
  * useBlockResize(rootRef); // host fits the iframe to content
43
+ * if (!ready) return <div>Loading…</div>; // no ref needed on this branch
18
44
  * return <div ref={rootRef}>…</div>;
19
45
  */
20
46
  export function useBlockResize(ref) {
47
+ /** The element the live observer is watching. `null` = watching nothing. */
48
+ const observedRef = useRef(null);
49
+ const observerRef = useRef(null);
50
+ // Unmount-only teardown. Declared FIRST so that on a remount (React
51
+ // StrictMode's deliberate double-invoke) its cleanup — which clears
52
+ // `observedRef` — runs before the observe effect's setup re-runs and finds a
53
+ // clean slate. React runs every cleanup before any setup.
54
+ useEffect(() => {
55
+ return () => {
56
+ observerRef.current?.disconnect();
57
+ observerRef.current = null;
58
+ observedRef.current = null;
59
+ };
60
+ }, []);
61
+ // No dependency array on purpose — see the block comment above. The identity
62
+ // check below, not a dependency list, is what makes this cheap.
21
63
  useEffect(() => {
64
+ if (typeof ResizeObserver === 'undefined')
65
+ return;
22
66
  const el = ref.current;
67
+ if (el === observedRef.current)
68
+ return; // already observing exactly this
69
+ observerRef.current?.disconnect();
70
+ observerRef.current = null;
71
+ observedRef.current = el;
23
72
  if (!el)
24
73
  return;
25
- if (typeof ResizeObserver === 'undefined')
26
- return;
27
74
  const transport = getTransport();
28
75
  let lastHeight = -1;
29
76
  const observer = new ResizeObserver((entries) => {
@@ -34,7 +81,7 @@ export function useBlockResize(ref) {
34
81
  transport.sendMessage({ type: 'RESIZE_IFRAME', payload: { height } });
35
82
  });
36
83
  observer.observe(el);
37
- return () => observer.disconnect();
38
- }, [ref]);
84
+ observerRef.current = observer;
85
+ });
39
86
  }
40
87
  //# sourceMappingURL=useBlockResize.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useBlockResize.js","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAkB,MAAM,OAAO,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,GAAkC;IAC/D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,IAAI,OAAO,cAAc,KAAK,WAAW;YAAE,OAAO;QAElD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;QACjC,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;QACpB,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC;YAC5E,IAAI,MAAM,KAAK,UAAU;gBAAE,OAAO;YAClC,UAAU,GAAG,MAAM,CAAC;YACpB,SAAS,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrB,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AACZ,CAAC"}
1
+ {"version":3,"file":"useBlockResize.js","sourceRoot":"","sources":["../../src/hooks/useBlockResize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAC;AAE1D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,MAAM,UAAU,cAAc,CAAC,GAAkC;IAC/D,4EAA4E;IAC5E,MAAM,WAAW,GAAG,MAAM,CAAqB,IAAI,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAC;IAExD,oEAAoE;IACpE,oEAAoE;IACpE,6EAA6E;IAC7E,0DAA0D;IAC1D,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,GAAG,EAAE;YACV,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;YAClC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,6EAA6E;IAC7E,gEAAgE;IAChE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,cAAc,KAAK,WAAW;YAAE,OAAO;QAElD,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,KAAK,WAAW,CAAC,OAAO;YAAE,OAAO,CAAC,iCAAiC;QAEzE,WAAW,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;QAClC,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,WAAW,CAAC,OAAO,GAAG,EAAE,CAAC;QACzB,IAAI,CAAC,EAAE;YAAE,OAAO;QAEhB,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;QACjC,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;QACpB,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,OAAO,EAAE,EAAE;YAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,MAAM,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC;YAC5E,IAAI,MAAM,KAAK,UAAU;gBAAE,OAAO;YAClC,UAAU,GAAG,MAAM,CAAC;YACpB,SAAS,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrB,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IACjC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -47,7 +47,9 @@
47
47
  * tRPC procedures (publicProcedure + verifyBlockToken, FLAT `{ blockToken, … }`
48
48
  * input). Reads need the `apps:storage:read` scope, writes `apps:storage:write`
49
49
  * — the dev token already carries whatever the local manifest declared, and the
50
- * server enforces. Real per-(block_instance, user) KV, real 64KB/50MB quotas.
50
+ * server enforces. Real KV — keys namespaced per (block_instance, user), byte
51
+ * and row budgets enforced per (app, user) — with the REAL ceilings, so a
52
+ * write this host accepts is one production would accept.
51
53
  *
52
54
  * SET_USER_CHECKPOINT (Phase 4): FORWARDED (faithful) to the block-token
53
55
  * `blocks.updateUserSettings` mutation — never fabricated. The default page
@@ -1 +1 @@
1
- {"version":3,"file":"liveHost.d.ts","sourceRoot":"","sources":["../../src/internal/liveHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAEH,OAAO,EACL,KAAK,YAAY,EAGjB,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAE,QAAQ,EAAyC,MAAM,eAAe,CAAC;AACrF,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAK5B;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,UAAU,CAAC;IACpC;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACvD;AAQD;;;;;GAKG;AACH,UAAU,wBAAwB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,wBAAwB,CAgB/E;AAyHD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,QAAQ,CAu9CjE"}
1
+ {"version":3,"file":"liveHost.d.ts","sourceRoot":"","sources":["../../src/internal/liveHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,EACL,KAAK,YAAY,EAGjB,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,UAAU,EAGhB,MAAM,yBAAyB,CAAC;AAIjC,OAAO,KAAK,EAAE,QAAQ,EAAyC,MAAM,eAAe,CAAC;AACrF,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAK5B;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,UAAU,CAAC;IACpC;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACvD;AAQD;;;;;GAKG;AACH,UAAU,wBAAwB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,wBAAwB,CAgB/E;AAyHD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,QAAQ,CAw9CjE"}
@@ -47,7 +47,9 @@
47
47
  * tRPC procedures (publicProcedure + verifyBlockToken, FLAT `{ blockToken, … }`
48
48
  * input). Reads need the `apps:storage:read` scope, writes `apps:storage:write`
49
49
  * — the dev token already carries whatever the local manifest declared, and the
50
- * server enforces. Real per-(block_instance, user) KV, real 64KB/50MB quotas.
50
+ * server enforces. Real KV — keys namespaced per (block_instance, user), byte
51
+ * and row budgets enforced per (app, user) — with the REAL ceilings, so a
52
+ * write this host accepts is one production would accept.
51
53
  *
52
54
  * SET_USER_CHECKPOINT (Phase 4): FORWARDED (faithful) to the block-token
53
55
  * `blocks.updateUserSettings` mutation — never fabricated. The default page
@@ -396,10 +398,9 @@ export function createLiveHost(options) {
396
398
  return anonFallbackViewer();
397
399
  // `/api/v1/blocks/me` is the AUTHORITATIVE self-read and does carry
398
400
  // `status` — but this builds `BLOCK_INIT.viewer`, which the real host
399
- // projects down to `{ id, username }` today (civitai/civitai `main`'s
400
- // `projectBlockInitViewer`), and to `{ id, username, signedIn }` once
401
- // civitai/civitai#3707 (OPEN, unmerged) lands. Never moderation state,
402
- // in either version (civitai #2521). Forwarding `me.status` here would
401
+ // projects down to exactly `{ id, username, signedIn }` (civitai/civitai
402
+ // `main`'s `withSignedInFlag`). Never moderation state (civitai #2521).
403
+ // Forwarding `me.status` here would
403
404
  // make the live dev host more generous than production. A block that
404
405
  // wants `status` must ask for it via `GET_VIEWER` / `useViewer()`, which
405
406
  // is exactly the scope-gated path this release steers authors to.
@@ -1012,7 +1013,9 @@ export function createLiveHost(options) {
1012
1013
  }
1013
1014
  case 'APP_STORAGE_SET': {
1014
1015
  // Mutation apps.storage.set {blockToken, key, value} (POST). The
1015
- // server enforces apps:storage:write + the 64KB/50MB quotas.
1016
+ // server enforces apps:storage:write + the per-value cap and both
1017
+ // per-(app, viewer) budgets — no simulation here, so nothing to
1018
+ // keep in sync.
1016
1019
  const key = typed.payload?.key ?? '';
1017
1020
  const value = typed.payload?.value;
1018
1021
  void callTrpcData('apps.storage.set', { blockToken: rawToken, key, value }, 'POST').then((r) => {
@@ -1461,23 +1464,21 @@ export function createLiveHost(options) {
1461
1464
  /**
1462
1465
  * A minimal anon-ish viewer used when `/api/v1/blocks/me` can't be reached.
1463
1466
  *
1464
- * Carries EXACTLY `{ id, username, signedIn }` — the key set the BLOCK_INIT
1465
- * contract is moving to. The two halves have different provenance:
1467
+ * Carries EXACTLY `{ id, username, signedIn }` — byte-for-byte the key set the
1468
+ * real host puts on the wire:
1466
1469
  *
1467
- * - NO `status` is production TODAY. civitai/civitai `main`'s
1468
- * `projectBlockInitViewer` builds `{ id, username }`, pinned as exactly
1469
- * `['id', 'username']` by `__tests__/projectBlockInit.test.ts`. The platform
1470
- * withholds the viewer's moderation state from third-party iframes (civitai
1471
- * #2521), so a dev host that sends it invites a block to read a field
1472
- * production never provides.
1473
- * - `signedIn` is NOT production yet. It appears zero times under
1474
- * `src/components/AppBlocks/` on `main`; it arrives with civitai/civitai#3707
1475
- * (OPEN, unmerged), which also moves the host's pinned key set to
1476
- * `['id', 'signedIn', 'username']`. Emitted here so the field is exercisable
1477
- * locally ahead of the host — `viewer !== null` is still the gate to SHIP.
1470
+ * - NO `status`. The platform withholds the viewer's moderation state from
1471
+ * third-party iframes (civitai #2521), so a dev host that sent it would
1472
+ * invite a block to read a field production never provides.
1473
+ * - WITH `signedIn: true`. civitai/civitai `main`'s `withSignedInFlag`
1474
+ * (`src/components/AppBlocks/projectBlockInit.ts`) stamps it on every
1475
+ * present viewer from BOTH host surfaces, and that repo's
1476
+ * `__tests__/projectBlockInit.test.ts` pins the viewer key set as exactly
1477
+ * `['id', 'signedIn', 'username']`.
1478
1478
  *
1479
- * See {@link DEFAULT_VIEWER} in `mockHost` for the same note and for what to
1480
- * unwind if #3707 is abandoned.
1479
+ * 🔴 THE PROPERTY THIS FENCE HOLDS: the dev hosts must not be more generous
1480
+ * than the host they imitate — this default MATCHES the host, it does not run
1481
+ * ahead of it. See `DEFAULT_VIEWER` in `mockHost` for the same note.
1481
1482
  */
1482
1483
  function anonFallbackViewer() {
1483
1484
  return { id: 0, username: 'dev-live', signedIn: true };