@btst/stack 2.4.0 → 2.5.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 (136) hide show
  1. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +33 -47
  2. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +33 -47
  3. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.cjs +14 -21
  4. package/dist/packages/stack/src/plugins/ai-chat/client/plugin.mjs +15 -22
  5. package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +28 -45
  6. package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +22 -39
  7. package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +23 -27
  8. package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +24 -28
  9. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +14 -17
  10. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +14 -17
  11. package/dist/packages/stack/src/plugins/cms/client/plugin.cjs +11 -15
  12. package/dist/packages/stack/src/plugins/cms/client/plugin.mjs +12 -16
  13. package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +58 -62
  14. package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +58 -62
  15. package/dist/packages/stack/src/plugins/form-builder/client/plugin.cjs +12 -12
  16. package/dist/packages/stack/src/plugins/form-builder/client/plugin.mjs +13 -13
  17. package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +86 -117
  18. package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +83 -114
  19. package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +22 -29
  20. package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +23 -30
  21. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.cjs +8 -8
  22. package/dist/packages/stack/src/plugins/ui-builder/client/plugin.mjs +9 -9
  23. package/dist/packages/stack/src/plugins/utils.cjs +42 -0
  24. package/dist/packages/stack/src/plugins/utils.mjs +41 -1
  25. package/dist/plugins/ai-chat/api/index.d.cts +1 -1
  26. package/dist/plugins/ai-chat/api/index.d.mts +1 -1
  27. package/dist/plugins/ai-chat/api/index.d.ts +1 -1
  28. package/dist/plugins/ai-chat/client/hooks/index.d.cts +1 -1
  29. package/dist/plugins/ai-chat/client/hooks/index.d.mts +1 -1
  30. package/dist/plugins/ai-chat/client/hooks/index.d.ts +1 -1
  31. package/dist/plugins/ai-chat/client/index.d.cts +8 -8
  32. package/dist/plugins/ai-chat/client/index.d.mts +8 -8
  33. package/dist/plugins/ai-chat/client/index.d.ts +8 -8
  34. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  35. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  36. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  37. package/dist/plugins/blog/api/index.d.cts +1 -1
  38. package/dist/plugins/blog/api/index.d.mts +1 -1
  39. package/dist/plugins/blog/api/index.d.ts +1 -1
  40. package/dist/plugins/blog/client/index.d.cts +12 -12
  41. package/dist/plugins/blog/client/index.d.mts +12 -12
  42. package/dist/plugins/blog/client/index.d.ts +12 -12
  43. package/dist/plugins/blog/query-keys.d.cts +1 -1
  44. package/dist/plugins/blog/query-keys.d.mts +1 -1
  45. package/dist/plugins/blog/query-keys.d.ts +1 -1
  46. package/dist/plugins/client/index.cjs +1 -0
  47. package/dist/plugins/client/index.d.cts +8 -1
  48. package/dist/plugins/client/index.d.mts +8 -1
  49. package/dist/plugins/client/index.d.ts +8 -1
  50. package/dist/plugins/client/index.mjs +1 -1
  51. package/dist/plugins/cms/api/index.d.cts +2 -2
  52. package/dist/plugins/cms/api/index.d.mts +2 -2
  53. package/dist/plugins/cms/api/index.d.ts +2 -2
  54. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  55. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  56. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  57. package/dist/plugins/cms/client/index.d.cts +6 -6
  58. package/dist/plugins/cms/client/index.d.mts +6 -6
  59. package/dist/plugins/cms/client/index.d.ts +6 -6
  60. package/dist/plugins/cms/query-keys.d.cts +2 -2
  61. package/dist/plugins/cms/query-keys.d.mts +2 -2
  62. package/dist/plugins/cms/query-keys.d.ts +2 -2
  63. package/dist/plugins/form-builder/api/index.d.cts +2 -2
  64. package/dist/plugins/form-builder/api/index.d.mts +2 -2
  65. package/dist/plugins/form-builder/api/index.d.ts +2 -2
  66. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  67. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  68. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  69. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  70. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  71. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  72. package/dist/plugins/form-builder/client/index.d.cts +6 -6
  73. package/dist/plugins/form-builder/client/index.d.mts +6 -6
  74. package/dist/plugins/form-builder/client/index.d.ts +6 -6
  75. package/dist/plugins/form-builder/query-keys.d.cts +2 -2
  76. package/dist/plugins/form-builder/query-keys.d.mts +2 -2
  77. package/dist/plugins/form-builder/query-keys.d.ts +2 -2
  78. package/dist/plugins/kanban/api/index.d.cts +1 -1
  79. package/dist/plugins/kanban/api/index.d.mts +1 -1
  80. package/dist/plugins/kanban/api/index.d.ts +1 -1
  81. package/dist/plugins/kanban/client/index.d.cts +12 -12
  82. package/dist/plugins/kanban/client/index.d.mts +12 -12
  83. package/dist/plugins/kanban/client/index.d.ts +12 -12
  84. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  85. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  86. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  87. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  88. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  89. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  90. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  91. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  92. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  93. package/dist/plugins/ui-builder/index.d.cts +2 -2
  94. package/dist/plugins/ui-builder/index.d.mts +2 -2
  95. package/dist/plugins/ui-builder/index.d.ts +2 -2
  96. package/dist/shared/{stack.C-WUPMT6.d.cts → stack.B2xZTSiO.d.cts} +4 -4
  97. package/dist/shared/{stack.CczspVn2.d.mts → stack.B58oHdqm.d.mts} +1 -1
  98. package/dist/shared/{stack.CVDTkMoO.d.mts → stack.B8QD11QU.d.cts} +7 -7
  99. package/dist/shared/{stack.CVDTkMoO.d.cts → stack.B8QD11QU.d.mts} +7 -7
  100. package/dist/shared/{stack.CVDTkMoO.d.ts → stack.B8QD11QU.d.ts} +7 -7
  101. package/dist/shared/{stack.Kq2-QzOC.d.ts → stack.BDVEpue1.d.ts} +2 -2
  102. package/dist/shared/{stack.B7ONvlD_.d.mts → stack.BTvbxZvw.d.cts} +2 -2
  103. package/dist/shared/{stack.DdI5W6MB.d.mts → stack.BozPgbrZ.d.cts} +19 -19
  104. package/dist/shared/{stack.DdI5W6MB.d.cts → stack.BozPgbrZ.d.mts} +19 -19
  105. package/dist/shared/{stack.DdI5W6MB.d.ts → stack.BozPgbrZ.d.ts} +19 -19
  106. package/dist/shared/{stack.BUkC2EsZ.d.cts → stack.C9Mg2Q46.d.cts} +1 -1
  107. package/dist/shared/{stack.BEn34wW6.d.ts → stack.CTDVxbrA.d.ts} +12 -12
  108. package/dist/shared/{stack.C-Ptrz8s.d.ts → stack.Cj_zKww4.d.ts} +4 -4
  109. package/dist/shared/{stack.BepFXT3w.d.mts → stack.CxaFNQCV.d.mts} +25 -25
  110. package/dist/shared/{stack.DWoCZff7.d.cts → stack.D-b5zbPm.d.cts} +12 -12
  111. package/dist/shared/{stack.kcdnD4gA.d.cts → stack.DTtmJPQO.d.mts} +2 -2
  112. package/dist/shared/{stack.CL8ts1Mu.d.ts → stack.DXnclTG7.d.ts} +8 -8
  113. package/dist/shared/{stack.heOA9gzA.d.cts → stack.DaZM10cp.d.cts} +8 -8
  114. package/dist/shared/{stack.DTDxgFj8.d.mts → stack.FVWf2JhZ.d.mts} +12 -12
  115. package/dist/shared/{stack.Dk5r4W1F.d.mts → stack.cfCkioTe.d.mts} +8 -8
  116. package/dist/shared/{stack.6fUOjLs9.d.mts → stack.dH7u-TJH.d.mts} +4 -4
  117. package/dist/shared/{stack.CgWzG5jH.d.ts → stack.j75TpKh2.d.ts} +25 -25
  118. package/dist/shared/{stack.D3GB6wKv.d.cts → stack.n1_i1p2B.d.cts} +25 -25
  119. package/dist/shared/{stack.DASmUVjX.d.ts → stack.sO33ZDhK.d.ts} +1 -1
  120. package/package.json +1 -1
  121. package/src/plugins/ai-chat/api/plugin.ts +48 -63
  122. package/src/plugins/ai-chat/client/plugin.tsx +23 -31
  123. package/src/plugins/blog/api/plugin.ts +31 -47
  124. package/src/plugins/blog/client/plugin.tsx +36 -39
  125. package/src/plugins/client/index.ts +5 -1
  126. package/src/plugins/cms/api/plugin.ts +14 -17
  127. package/src/plugins/cms/client/plugin.tsx +18 -21
  128. package/src/plugins/cms/types.ts +7 -7
  129. package/src/plugins/form-builder/api/plugin.ts +64 -64
  130. package/src/plugins/form-builder/client/plugin.tsx +19 -18
  131. package/src/plugins/form-builder/types.ts +19 -24
  132. package/src/plugins/kanban/api/plugin.ts +111 -136
  133. package/src/plugins/kanban/client/plugin.tsx +35 -41
  134. package/src/plugins/ui-builder/client/plugin.tsx +11 -10
  135. package/src/plugins/ui-builder/types.ts +4 -4
  136. package/src/plugins/utils.ts +92 -1
@@ -2,6 +2,7 @@ import {
2
2
  defineClientPlugin,
3
3
  createApiClient,
4
4
  isConnectionError,
5
+ runClientHookWithShim,
5
6
  } from "@btst/stack/plugins/client";
6
7
  import { createRoute } from "@btst/yar";
7
8
  import type { QueryClient } from "@tanstack/react-query";
@@ -86,39 +87,39 @@ export interface KanbanClientConfig {
86
87
  */
87
88
  export interface KanbanClientHooks {
88
89
  /**
89
- * Called before loading boards list. Return false to cancel loading.
90
+ * Called before loading boards list. Throw an error to cancel loading.
90
91
  */
91
- beforeLoadBoards?: (context: LoaderContext) => Promise<boolean> | boolean;
92
+ beforeLoadBoards?: (context: LoaderContext) => Promise<void> | void;
92
93
  /**
93
- * Called after boards are loaded. Return false to cancel further processing.
94
+ * Called after boards are loaded. Throw an error to cancel further processing.
94
95
  */
95
96
  afterLoadBoards?: (
96
97
  boards: SerializedBoardWithColumns[] | null,
97
98
  context: LoaderContext,
98
- ) => Promise<boolean> | boolean;
99
+ ) => Promise<void> | void;
99
100
  /**
100
- * Called before loading a single board. Return false to cancel loading.
101
+ * Called before loading a single board. Throw an error to cancel loading.
101
102
  */
102
103
  beforeLoadBoard?: (
103
104
  boardId: string,
104
105
  context: LoaderContext,
105
- ) => Promise<boolean> | boolean;
106
+ ) => Promise<void> | void;
106
107
  /**
107
- * Called after a board is loaded. Return false to cancel further processing.
108
+ * Called after a board is loaded. Throw an error to cancel further processing.
108
109
  */
109
110
  afterLoadBoard?: (
110
111
  board: SerializedBoardWithColumns | null,
111
112
  boardId: string,
112
113
  context: LoaderContext,
113
- ) => Promise<boolean> | boolean;
114
+ ) => Promise<void> | void;
114
115
  /**
115
- * Called before loading the new board page. Return false to cancel.
116
+ * Called before loading the new board page. Throw an error to cancel.
116
117
  */
117
- beforeLoadNewBoard?: (context: LoaderContext) => Promise<boolean> | boolean;
118
+ beforeLoadNewBoard?: (context: LoaderContext) => Promise<void> | void;
118
119
  /**
119
- * Called after the new board page is loaded. Return false to cancel.
120
+ * Called after the new board page is loaded. Throw an error to cancel.
120
121
  */
121
- afterLoadNewBoard?: (context: LoaderContext) => Promise<boolean> | boolean;
122
+ afterLoadNewBoard?: (context: LoaderContext) => Promise<void> | void;
122
123
  /**
123
124
  * Called when a loading error occurs
124
125
  */
@@ -141,10 +142,10 @@ function createBoardsLoader(config: KanbanClientConfig) {
141
142
 
142
143
  try {
143
144
  if (hooks?.beforeLoadBoards) {
144
- const canLoad = await hooks.beforeLoadBoards(context);
145
- if (!canLoad) {
146
- throw new Error("Load prevented by beforeLoadBoards hook");
147
- }
145
+ await runClientHookWithShim(
146
+ () => hooks.beforeLoadBoards!(context),
147
+ "Load prevented by beforeLoadBoards hook",
148
+ );
148
149
  }
149
150
 
150
151
  const client = createApiClient<KanbanApiRouter>({
@@ -161,13 +162,10 @@ function createBoardsLoader(config: KanbanClientConfig) {
161
162
  const boards = queryClient.getQueryData<SerializedBoardWithColumns[]>(
162
163
  listQuery.queryKey,
163
164
  );
164
- const canContinue = await hooks.afterLoadBoards(
165
- boards || null,
166
- context,
165
+ await runClientHookWithShim(
166
+ () => hooks.afterLoadBoards!(boards || null, context),
167
+ "Load prevented by afterLoadBoards hook",
167
168
  );
168
- if (canContinue === false) {
169
- throw new Error("Load prevented by afterLoadBoards hook");
170
- }
171
169
  }
172
170
 
173
171
  const queryState = queryClient.getQueryState(listQuery.queryKey);
@@ -210,10 +208,10 @@ function createBoardLoader(boardId: string, config: KanbanClientConfig) {
210
208
 
211
209
  try {
212
210
  if (hooks?.beforeLoadBoard) {
213
- const canLoad = await hooks.beforeLoadBoard(boardId, context);
214
- if (!canLoad) {
215
- throw new Error("Load prevented by beforeLoadBoard hook");
216
- }
211
+ await runClientHookWithShim(
212
+ () => hooks.beforeLoadBoard!(boardId, context),
213
+ "Load prevented by beforeLoadBoard hook",
214
+ );
217
215
  }
218
216
 
219
217
  const client = createApiClient<KanbanApiRouter>({
@@ -229,14 +227,10 @@ function createBoardLoader(boardId: string, config: KanbanClientConfig) {
229
227
  const board = queryClient.getQueryData<SerializedBoardWithColumns>(
230
228
  boardQuery.queryKey,
231
229
  );
232
- const canContinue = await hooks.afterLoadBoard(
233
- board || null,
234
- boardId,
235
- context,
230
+ await runClientHookWithShim(
231
+ () => hooks.afterLoadBoard!(board || null, boardId, context),
232
+ "Load prevented by afterLoadBoard hook",
236
233
  );
237
- if (canContinue === false) {
238
- throw new Error("Load prevented by afterLoadBoard hook");
239
- }
240
234
  }
241
235
 
242
236
  const queryState = queryClient.getQueryState(boardQuery.queryKey);
@@ -278,17 +272,17 @@ function createNewBoardLoader(config: KanbanClientConfig) {
278
272
 
279
273
  try {
280
274
  if (hooks?.beforeLoadNewBoard) {
281
- const canLoad = await hooks.beforeLoadNewBoard(context);
282
- if (!canLoad) {
283
- throw new Error("Load prevented by beforeLoadNewBoard hook");
284
- }
275
+ await runClientHookWithShim(
276
+ () => hooks.beforeLoadNewBoard!(context),
277
+ "Load prevented by beforeLoadNewBoard hook",
278
+ );
285
279
  }
286
280
 
287
281
  if (hooks?.afterLoadNewBoard) {
288
- const canContinue = await hooks.afterLoadNewBoard(context);
289
- if (canContinue === false) {
290
- throw new Error("Load prevented by afterLoadNewBoard hook");
291
- }
282
+ await runClientHookWithShim(
283
+ () => hooks.afterLoadNewBoard!(context),
284
+ "Load prevented by afterLoadNewBoard hook",
285
+ );
292
286
  }
293
287
  } catch (error) {
294
288
  if (hooks?.onLoadError) {
@@ -3,6 +3,7 @@ import { lazy } from "react";
3
3
  import {
4
4
  defineClientPlugin,
5
5
  createApiClient,
6
+ runClientHookWithShim,
6
7
  } from "@btst/stack/plugins/client";
7
8
  import { createRoute } from "@btst/yar";
8
9
  import type { QueryClient } from "@tanstack/react-query";
@@ -69,10 +70,10 @@ function createPageListLoader(config: UIBuilderClientConfig) {
69
70
  try {
70
71
  // Before hook - authorization check
71
72
  if (hooks?.beforeLoadPageList) {
72
- const canLoad = await hooks.beforeLoadPageList(context);
73
- if (!canLoad) {
74
- throw new Error("Load prevented by beforeLoadPageList hook");
75
- }
73
+ await runClientHookWithShim(
74
+ () => hooks.beforeLoadPageList!(context),
75
+ "Load prevented by beforeLoadPageList hook",
76
+ );
76
77
  }
77
78
 
78
79
  const client = createApiClient<CMSApiRouter>({
@@ -161,10 +162,10 @@ function createPageBuilderLoader(
161
162
  try {
162
163
  // Before hook - authorization check
163
164
  if (hooks?.beforeLoadPageBuilder) {
164
- const canLoad = await hooks.beforeLoadPageBuilder(id, context);
165
- if (!canLoad) {
166
- throw new Error("Load prevented by beforeLoadPageBuilder hook");
167
- }
165
+ await runClientHookWithShim(
166
+ () => hooks.beforeLoadPageBuilder!(id, context),
167
+ "Load prevented by beforeLoadPageBuilder hook",
168
+ );
168
169
  }
169
170
 
170
171
  const client = createApiClient<CMSApiRouter>({
@@ -273,11 +274,11 @@ function createPageBuilderMeta(
273
274
  * hooks: {
274
275
  * beforeLoadPageList: async (ctx) => {
275
276
  * const session = await getSession(ctx.headers)
276
- * return session?.user?.isAdmin === true
277
+ * if (!session?.user?.isAdmin) throw new Error("Admin access required")
277
278
  * },
278
279
  * beforeLoadPageBuilder: async (pageId, ctx) => {
279
280
  * const session = await getSession(ctx.headers)
280
- * return session?.user?.isAdmin === true
281
+ * if (!session?.user?.isAdmin) throw new Error("Admin access required")
281
282
  * },
282
283
  * onLoadError: () => redirect("/auth/sign-in"),
283
284
  * },
@@ -92,24 +92,24 @@ export interface LoaderContext {
92
92
  */
93
93
  export interface UIBuilderClientHooks {
94
94
  /**
95
- * Called before loading the page list. Return false to cancel loading.
95
+ * Called before loading the page list. Throw an error to cancel loading.
96
96
  * @param context - Loader context with path, params, etc.
97
97
  */
98
- beforeLoadPageList?: (context: LoaderContext) => Promise<boolean> | boolean;
98
+ beforeLoadPageList?: (context: LoaderContext) => Promise<void> | void;
99
99
  /**
100
100
  * Called after the page list is loaded.
101
101
  * @param context - Loader context
102
102
  */
103
103
  afterLoadPageList?: (context: LoaderContext) => Promise<void> | void;
104
104
  /**
105
- * Called before loading the page builder. Return false to cancel loading.
105
+ * Called before loading the page builder. Throw an error to cancel loading.
106
106
  * @param pageId - The page ID (undefined for new pages)
107
107
  * @param context - Loader context
108
108
  */
109
109
  beforeLoadPageBuilder?: (
110
110
  pageId: string | undefined,
111
111
  context: LoaderContext,
112
- ) => Promise<boolean> | boolean;
112
+ ) => Promise<void> | void;
113
113
  /**
114
114
  * Called after the page builder is loaded.
115
115
  * @param pageId - The page ID (undefined for new pages)
@@ -1,5 +1,96 @@
1
1
  import { createClient } from "better-call/client";
2
2
 
3
+ /**
4
+ * Runs a hook with backward-compatible denial handling.
5
+ * Hooks may deny by returning a falsy value (old) or throwing (new).
6
+ * Both are normalized to an HTTP error via `createError` (`ctx.error`).
7
+ * Returns the hook's result so transform hooks can apply mutations.
8
+ *
9
+ * ## Migration note (v2.4 → v2.5)
10
+ *
11
+ * Old-style hooks signalled denial by returning `false` and allowance by returning `true`.
12
+ * Pre-shim call sites used `if (!result)` which treated `undefined` (fall-through) as deny.
13
+ * New-style hooks throw an Error to deny and return void/undefined to allow.
14
+ *
15
+ * The shim detects old-style boolean returns at runtime and emits a deprecation warning so
16
+ * that any hook with a code path returning a boolean is surfaced immediately. Hooks that fall
17
+ * through to `undefined` on **every** code path (no boolean return anywhere) cannot be
18
+ * distinguished from new-style void hooks — those hooks must be audited manually and updated
19
+ * to throw explicitly when they intend to deny access.
20
+ */
21
+ export async function runHookWithShim<T>(
22
+ hookFn: () => Promise<T> | T,
23
+ createError: (
24
+ status: keyof typeof statusCodes | Status,
25
+ body: { message: string },
26
+ ) => any,
27
+ defaultMessage: string,
28
+ errorStatus = 403 as keyof typeof statusCodes | Status,
29
+ ): Promise<Exclude<Awaited<T>, false>> {
30
+ let result: Awaited<T>;
31
+ try {
32
+ result = await hookFn();
33
+ } catch (e) {
34
+ throw createError(errorStatus, {
35
+ message: e instanceof Error ? e.message : defaultMessage,
36
+ });
37
+ }
38
+ // Detect old-style boolean returns (pre-v2.5 pattern).
39
+ // Emitting a warning here is the only reliable way to surface hooks that still rely on
40
+ // boolean returns — including hooks where one branch returns `false` and another falls
41
+ // through to `undefined`, which was previously denied by `if (!result)` at the call site
42
+ // but would now silently allow if the warning is not acted on.
43
+ if (typeof result === "boolean") {
44
+ if (process.env.NODE_ENV !== "production") {
45
+ console.warn(
46
+ `[btst] A lifecycle hook returned a boolean (${result}). ` +
47
+ `Boolean returns are deprecated — throw an Error to deny access instead. ` +
48
+ `IMPORTANT: any code path in this hook that falls through to undefined ` +
49
+ `now ALLOWS access (previously denied). ` +
50
+ `Update the hook to throw new Error("Unauthorized") to deny.`,
51
+ );
52
+ }
53
+ if (!result) {
54
+ throw createError(errorStatus, { message: defaultMessage });
55
+ }
56
+ }
57
+ return result as Exclude<Awaited<T>, false>;
58
+ }
59
+
60
+ /**
61
+ * Client-side equivalent of runHookWithShim — throws a plain Error instead of an HTTP error.
62
+ * Hooks may deny by returning false (old) or throwing (new); both normalize to an Error.
63
+ *
64
+ * See `runHookWithShim` for the full migration note on boolean-vs-void semantics.
65
+ */
66
+ export async function runClientHookWithShim<T>(
67
+ hookFn: () => Promise<T> | T,
68
+ defaultMessage: string,
69
+ ): Promise<Exclude<Awaited<T>, false>> {
70
+ let result: Awaited<T>;
71
+ try {
72
+ result = await hookFn();
73
+ } catch (e) {
74
+ throw e instanceof Error ? e : new Error(defaultMessage);
75
+ }
76
+ // Detect old-style boolean returns and warn; see runHookWithShim for rationale.
77
+ if (typeof result === "boolean") {
78
+ if (process.env.NODE_ENV !== "production") {
79
+ console.warn(
80
+ `[btst] A lifecycle hook returned a boolean (${result}). ` +
81
+ `Boolean returns are deprecated — throw an Error to deny access instead. ` +
82
+ `IMPORTANT: any code path in this hook that falls through to undefined ` +
83
+ `now ALLOWS access (previously denied). ` +
84
+ `Update the hook to throw new Error("Unauthorized") to deny.`,
85
+ );
86
+ }
87
+ if (!result) {
88
+ throw new Error(defaultMessage);
89
+ }
90
+ }
91
+ return result as Exclude<Awaited<T>, false>;
92
+ }
93
+
3
94
  /**
4
95
  * Returns true when a fetch error is a connection-refused / no-server error.
5
96
  * Used in SSR loaders to emit an actionable build-time warning when
@@ -18,7 +109,7 @@ export function isConnectionError(err: unknown): boolean {
18
109
  code === "ERR_CONNECTION_REFUSED"
19
110
  );
20
111
  }
21
- import type { Router, Endpoint } from "better-call";
112
+ import type { Router, Endpoint, Status, statusCodes } from "better-call";
22
113
 
23
114
  interface CreateApiClientOptions {
24
115
  baseURL?: string;