@civitai/blocks-react 0.57.4 → 0.58.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
@@ -211,13 +211,25 @@ const bp = useBlockBreakpoint();
211
211
  ### `useBlockToken()`
212
212
 
213
213
  Current block-scoped JWT, auto-refreshing ~2 min before expiry. Returns the token
214
- fields plus a `refresh()` for the 401-retry path.
214
+ fields plus a `refresh()` for the 401-retry path, which **resolves with the new
215
+ token**.
215
216
 
216
217
  ```tsx
217
218
  const { raw, scopes, expiresAt, buzzBudget, refresh } = useBlockToken();
218
- // after a 401: await refresh(); then retry the request once with the new `raw`.
219
+
220
+ let res = await fetch(url, { headers: { Authorization: `Bearer ${raw}` } });
221
+ if (res.status === 401) {
222
+ const fresh = await refresh(); // resolves WITH the new token
223
+ res = await fetch(url, { headers: { Authorization: `Bearer ${fresh.raw}` } });
224
+ }
219
225
  ```
220
226
 
227
+ > **Retry with the resolved token, not the `raw` you destructured.** That `raw` is
228
+ > a `const` from the render closure that ran *before* the refresh — awaiting
229
+ > `refresh()` re-renders the component but cannot reassign the binding your
230
+ > in-flight callback is already holding. A retry that re-reads the outer `raw`
231
+ > re-sends the stale JWT and 401s for exactly the reason the first call did.
232
+
221
233
  ### `useHostOrigin()`
222
234
 
223
235
  The validated host origin to direct-fetch the App Blocks HTTP API against —
@@ -1,10 +1,11 @@
1
1
  import type { BlockToken } from '@civitai/app-sdk/blocks';
2
2
  /**
3
3
  * What {@link useBlockToken} returns: the live block token plus a manual
4
- * `refresh`. See `./returnTypeLedger.js`.
4
+ * `refresh` that resolves WITH the newly-minted token. See
5
+ * `./returnTypeLedger.js`.
5
6
  */
6
7
  export type UseBlockToken = BlockToken & {
7
- refresh: () => Promise<void>;
8
+ refresh: () => Promise<BlockToken>;
8
9
  };
9
10
  /**
10
11
  * Returns the current block-scoped JWT and keeps it fresh.
@@ -14,21 +15,48 @@ export type UseBlockToken = BlockToken & {
14
15
  * snapshot, and the hook re-renders with the new token.
15
16
  *
16
17
  * Consumers also get a `refresh()` callable for the 401-retry path. Call it
17
- * after any API request returns 401 — it forces an immediate token mint and
18
- * resolves once the new token is applied to the snapshot.
18
+ * after any API request returns 401 — it forces an immediate token mint,
19
+ * applies the new token to the snapshot, and RESOLVES WITH that new
20
+ * {@link BlockToken}.
21
+ *
22
+ * 🔴 RETRY WITH THE RESOLVED TOKEN, NOT THE `raw` YOU DESTRUCTURED. The `raw`
23
+ * in scope was captured from the render closure that ran BEFORE the refresh;
24
+ * awaiting `refresh()` re-renders the component but cannot reassign a `const`
25
+ * binding inside an async callback that is already executing. So a retry that
26
+ * re-reads the outer `raw` re-sends the STALE token and 401s for exactly the
27
+ * reason the first call did. This is why `refresh()` resolves with the token:
28
+ * the resolved value is the only in-scope handle on the fresh JWT.
19
29
  *
20
30
  * @returns The {@link BlockToken} fields (`raw`, `scopes`, `expiresAt`,
21
- * `buzzBudget`, …) plus a `refresh()` for the 401-retry path.
31
+ * `buzzBudget`, …) plus a `refresh()` for the 401-retry path, which resolves
32
+ * with the new {@link BlockToken}.
22
33
  *
23
34
  * @example
24
35
  * const { raw, scopes, expiresAt, buzzBudget, refresh } = useBlockToken();
25
- * // after a 401: await refresh(); then retry the request once with the new `raw`.
36
+ * let res = await fetch(url, { headers: { Authorization: `Bearer ${raw}` } });
37
+ * if (res.status === 401) {
38
+ * const fresh = await refresh(); // resolves WITH the new token
39
+ * res = await fetch(url, { headers: { Authorization: `Bearer ${fresh.raw}` } });
40
+ * }
26
41
  */
27
42
  export declare function useBlockToken(): UseBlockToken;
28
43
  /**
29
44
  * Mint/await a token refresh for one block instance, coalescing concurrent calls
30
45
  * for the SAME `blockInstanceId`. Exported for the inline-mode multi-instance
31
46
  * dedup test — hooks call it via `useBlockToken().refresh`.
47
+ *
48
+ * Resolves with the REFRESHED {@link BlockToken} so a 401-retry has an in-scope
49
+ * handle on the new JWT — the caller's destructured `raw` belongs to a closure
50
+ * that ran before the refresh and cannot be reassigned by it.
51
+ *
52
+ * The value is read back off the transport SNAPSHOT rather than re-parsed from
53
+ * the reply payload, and that is the ordering this depends on:
54
+ * `IframeTransport.handleMessage` calls `applyTokenRefresh` BEFORE
55
+ * `pending.resolve`, deliberately (see the comment there), so by the time this
56
+ * `.then` runs the snapshot already holds the new token. Reading the snapshot
57
+ * keeps ONE parse of the wire token — the resolved value is the very object the
58
+ * next render observes, not a second `tokenFromWrapped` of the same bytes that
59
+ * could drift from it.
32
60
  */
33
- export declare function requestRefresh(blockInstanceId: string): Promise<void>;
61
+ export declare function requestRefresh(blockInstanceId: string): Promise<BlockToken>;
34
62
  //# sourceMappingURL=useBlockToken.d.ts.map
@@ -12,15 +12,29 @@ const REFRESH_LEAD_MS = 2 * 60 * 1000;
12
12
  * snapshot, and the hook re-renders with the new token.
13
13
  *
14
14
  * Consumers also get a `refresh()` callable for the 401-retry path. Call it
15
- * after any API request returns 401 — it forces an immediate token mint and
16
- * resolves once the new token is applied to the snapshot.
15
+ * after any API request returns 401 — it forces an immediate token mint,
16
+ * applies the new token to the snapshot, and RESOLVES WITH that new
17
+ * {@link BlockToken}.
18
+ *
19
+ * 🔴 RETRY WITH THE RESOLVED TOKEN, NOT THE `raw` YOU DESTRUCTURED. The `raw`
20
+ * in scope was captured from the render closure that ran BEFORE the refresh;
21
+ * awaiting `refresh()` re-renders the component but cannot reassign a `const`
22
+ * binding inside an async callback that is already executing. So a retry that
23
+ * re-reads the outer `raw` re-sends the STALE token and 401s for exactly the
24
+ * reason the first call did. This is why `refresh()` resolves with the token:
25
+ * the resolved value is the only in-scope handle on the fresh JWT.
17
26
  *
18
27
  * @returns The {@link BlockToken} fields (`raw`, `scopes`, `expiresAt`,
19
- * `buzzBudget`, …) plus a `refresh()` for the 401-retry path.
28
+ * `buzzBudget`, …) plus a `refresh()` for the 401-retry path, which resolves
29
+ * with the new {@link BlockToken}.
20
30
  *
21
31
  * @example
22
32
  * const { raw, scopes, expiresAt, buzzBudget, refresh } = useBlockToken();
23
- * // after a 401: await refresh(); then retry the request once with the new `raw`.
33
+ * let res = await fetch(url, { headers: { Authorization: `Bearer ${raw}` } });
34
+ * if (res.status === 401) {
35
+ * const fresh = await refresh(); // resolves WITH the new token
36
+ * res = await fetch(url, { headers: { Authorization: `Bearer ${fresh.raw}` } });
37
+ * }
24
38
  */
25
39
  export function useBlockToken() {
26
40
  const snap = useTransportSnapshot();
@@ -64,6 +78,19 @@ const inFlightRefreshByInstance = new Map();
64
78
  * Mint/await a token refresh for one block instance, coalescing concurrent calls
65
79
  * for the SAME `blockInstanceId`. Exported for the inline-mode multi-instance
66
80
  * dedup test — hooks call it via `useBlockToken().refresh`.
81
+ *
82
+ * Resolves with the REFRESHED {@link BlockToken} so a 401-retry has an in-scope
83
+ * handle on the new JWT — the caller's destructured `raw` belongs to a closure
84
+ * that ran before the refresh and cannot be reassigned by it.
85
+ *
86
+ * The value is read back off the transport SNAPSHOT rather than re-parsed from
87
+ * the reply payload, and that is the ordering this depends on:
88
+ * `IframeTransport.handleMessage` calls `applyTokenRefresh` BEFORE
89
+ * `pending.resolve`, deliberately (see the comment there), so by the time this
90
+ * `.then` runs the snapshot already holds the new token. Reading the snapshot
91
+ * keeps ONE parse of the wire token — the resolved value is the very object the
92
+ * next render observes, not a second `tokenFromWrapped` of the same bytes that
93
+ * could drift from it.
67
94
  */
68
95
  export async function requestRefresh(blockInstanceId) {
69
96
  const existing = inFlightRefreshByInstance.get(blockInstanceId);
@@ -71,7 +98,7 @@ export async function requestRefresh(blockInstanceId) {
71
98
  return existing;
72
99
  const transport = getTransport();
73
100
  const refresh = sendTypedRequest(transport, { type: 'REQUEST_TOKEN', payload: { blockInstanceId } }, 'TOKEN_REFRESH_RESPONSE')
74
- .then(() => undefined)
101
+ .then(() => transport.getSnapshot().token)
75
102
  .finally(() => {
76
103
  inFlightRefreshByInstance.delete(blockInstanceId);
77
104
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.57.4",
3
+ "version": "0.58.0",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",