@eminuckan/spine 0.2.2 → 0.3.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 (135) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/CODE_OF_CONDUCT.md +39 -0
  3. package/CONTRIBUTING.md +78 -0
  4. package/MAINTAINERS.md +43 -0
  5. package/README.md +111 -55
  6. package/ROADMAP.md +58 -0
  7. package/SECURITY.md +37 -0
  8. package/dist/access/evaluator.d.ts +3 -0
  9. package/dist/access/evaluator.d.ts.map +1 -0
  10. package/dist/access/index.d.ts +3 -0
  11. package/dist/access/index.d.ts.map +1 -0
  12. package/dist/access/index.js +4 -0
  13. package/dist/access/types.d.ts +38 -0
  14. package/dist/access/types.d.ts.map +1 -0
  15. package/dist/access-CGrsDMwu.js +61 -0
  16. package/dist/access-CGrsDMwu.js.map +1 -0
  17. package/dist/api-client/api-config.server.d.ts +27 -0
  18. package/dist/api-client/api-config.server.d.ts.map +1 -1
  19. package/dist/api-client/error-codes.d.ts +6 -31
  20. package/dist/api-client/error-codes.d.ts.map +1 -1
  21. package/dist/api-client/index.js +3 -3
  22. package/dist/api-client/server.d.ts +1 -1
  23. package/dist/api-client/server.d.ts.map +1 -1
  24. package/dist/api-client/server.js +4 -4
  25. package/dist/{api-client-DsrxK05Z.js → api-client-CgZUMukO.js} +9 -26
  26. package/dist/api-client-CgZUMukO.js.map +1 -0
  27. package/dist/auth/auth.server.d.ts +21 -16
  28. package/dist/auth/auth.server.d.ts.map +1 -1
  29. package/dist/auth/authorization-parameters.d.ts +5 -0
  30. package/dist/auth/authorization-parameters.d.ts.map +1 -0
  31. package/dist/auth/index.d.ts +1 -1
  32. package/dist/auth/index.d.ts.map +1 -1
  33. package/dist/auth/redis-session-storage.server.d.ts +18 -1
  34. package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
  35. package/dist/auth/route-protection.server.d.ts +20 -4
  36. package/dist/auth/route-protection.server.d.ts.map +1 -1
  37. package/dist/auth/server.d.ts +1 -1
  38. package/dist/auth/server.d.ts.map +1 -1
  39. package/dist/auth/server.js +5 -5
  40. package/dist/auth/types.d.ts +88 -21
  41. package/dist/auth/types.d.ts.map +1 -1
  42. package/dist/auth.server-D5oiz9aF.js +1066 -0
  43. package/dist/auth.server-D5oiz9aF.js.map +1 -0
  44. package/dist/auth.server-lC94lmlx.js +5 -0
  45. package/dist/identity/identity-context.server.d.ts +2 -0
  46. package/dist/identity/identity-context.server.d.ts.map +1 -1
  47. package/dist/identity/identity-store.d.ts +19 -2
  48. package/dist/identity/identity-store.d.ts.map +1 -1
  49. package/dist/identity/index.js +2 -2
  50. package/dist/identity/server.js +3 -3
  51. package/dist/identity/types.d.ts +3 -1
  52. package/dist/identity/types.d.ts.map +1 -1
  53. package/dist/{identity-CFa1txoj.js → identity-C5un-xAs.js} +104 -60
  54. package/dist/identity-C5un-xAs.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +12 -10
  58. package/dist/logging/index.js +1 -1
  59. package/dist/{logging-_h2mi3L9.js → logging-DviiAkLx.js} +2 -2
  60. package/dist/{logging-_h2mi3L9.js.map → logging-DviiAkLx.js.map} +1 -1
  61. package/dist/permission-service-CvGpwI0d.js +95 -0
  62. package/dist/permission-service-CvGpwI0d.js.map +1 -0
  63. package/dist/permissions/constants.d.ts +3 -0
  64. package/dist/permissions/constants.d.ts.map +1 -1
  65. package/dist/permissions/index.js +2 -1
  66. package/dist/permissions/permission-store.d.ts +1 -1
  67. package/dist/permissions/types.d.ts +1 -1
  68. package/dist/permissions/types.d.ts.map +1 -1
  69. package/dist/{permissions-D5ScGGnd.js → permissions-_n1hIG7Q.js} +8 -106
  70. package/dist/permissions-_n1hIG7Q.js.map +1 -0
  71. package/dist/query-client/index.js +1 -1
  72. package/dist/query-client/types.d.ts +9 -9
  73. package/dist/query-client/types.d.ts.map +1 -1
  74. package/dist/{query-client-Bpe08pk9.js → query-client-Bh3TyB8W.js} +2 -2
  75. package/dist/query-client-Bh3TyB8W.js.map +1 -0
  76. package/dist/react-router/index.js +12 -10
  77. package/dist/react-router/server.js +19 -17
  78. package/dist/redis-session-storage.server-v2VcpFsQ.js +500 -0
  79. package/dist/redis-session-storage.server-v2VcpFsQ.js.map +1 -0
  80. package/dist/{server-BHfKHKF_.js → server-B6PCxc-T.js} +39 -87
  81. package/dist/server-B6PCxc-T.js.map +1 -0
  82. package/dist/{server-DZmcd2Lr.js → server-C96NMFBy.js} +18 -10
  83. package/dist/server-C96NMFBy.js.map +1 -0
  84. package/dist/{server-BxvRxjCK.js → server-Ca-3DJf2.js} +44 -11
  85. package/dist/server-Ca-3DJf2.js.map +1 -0
  86. package/dist/{server-C3L8J7iE.js → server-Dxsww9v9.js} +6 -5
  87. package/dist/server-Dxsww9v9.js.map +1 -0
  88. package/dist/{server-CouYoZiw.js → server-hGrElJMG.js} +2 -2
  89. package/dist/{server-CouYoZiw.js.map → server-hGrElJMG.js.map} +1 -1
  90. package/dist/server.js +19 -17
  91. package/dist/signalr/index.js +1 -1
  92. package/dist/signalr/types.d.ts +1 -1
  93. package/dist/{signalr-SJ-1sIxw.js → signalr-k-70VHKo.js} +3 -2
  94. package/dist/signalr-k-70VHKo.js.map +1 -0
  95. package/dist/tenant/index.js +2 -2
  96. package/dist/tenant/server.js +4 -4
  97. package/dist/tenant/tenant-context.d.ts +17 -1
  98. package/dist/tenant/tenant-context.d.ts.map +1 -1
  99. package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
  100. package/dist/tenant/tenant-store.d.ts +54 -9
  101. package/dist/tenant/tenant-store.d.ts.map +1 -1
  102. package/dist/tenant/tenant.server.d.ts +20 -1
  103. package/dist/tenant/tenant.server.d.ts.map +1 -1
  104. package/dist/tenant/types.d.ts +29 -9
  105. package/dist/tenant/types.d.ts.map +1 -1
  106. package/dist/tenant-DfLemb6n.js +384 -0
  107. package/dist/tenant-DfLemb6n.js.map +1 -0
  108. package/dist/{types-BnKaWdfe.js → types-BaaE6wjY.js} +1 -1
  109. package/dist/{types-BnKaWdfe.js.map → types-BaaE6wjY.js.map} +1 -1
  110. package/docs/adapters.md +87 -0
  111. package/docs/architecture.md +193 -0
  112. package/docs/backend-adaptation.md +249 -0
  113. package/docs/codex-for-oss-application.md +31 -0
  114. package/docs/codex-maintenance.md +47 -0
  115. package/docs/installation.md +213 -0
  116. package/docs/module-reference.md +207 -0
  117. package/docs/releasing.md +92 -0
  118. package/package.json +27 -4
  119. package/dist/api-client-DsrxK05Z.js.map +0 -1
  120. package/dist/auth.server-BPiCtlJE.js +0 -5
  121. package/dist/auth.server-Cx2h2qu3.js +0 -724
  122. package/dist/auth.server-Cx2h2qu3.js.map +0 -1
  123. package/dist/identity-CFa1txoj.js.map +0 -1
  124. package/dist/permissions-D5ScGGnd.js.map +0 -1
  125. package/dist/query-client-Bpe08pk9.js.map +0 -1
  126. package/dist/redis-session-storage.server-BR_0y-QO.js +0 -250
  127. package/dist/redis-session-storage.server-BR_0y-QO.js.map +0 -1
  128. package/dist/server-BHfKHKF_.js.map +0 -1
  129. package/dist/server-BxvRxjCK.js.map +0 -1
  130. package/dist/server-C3L8J7iE.js.map +0 -1
  131. package/dist/server-DZmcd2Lr.js.map +0 -1
  132. package/dist/signalr-SJ-1sIxw.js.map +0 -1
  133. package/dist/tenant-C6KPaSCX.js +0 -256
  134. package/dist/tenant-C6KPaSCX.js.map +0 -1
  135. /package/dist/{src-CJ2RLi94.js → src-B9rDUgSb.js} +0 -0
@@ -0,0 +1,384 @@
1
+ import { create } from "zustand";
2
+ import { createContext, useContext, useEffect } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ import { devtools } from "zustand/middleware";
5
+
6
+ //#region src/tenant/tenant-store.ts
7
+ /**
8
+ * Tenant client endpoints and browser-visible tenant cookie (configurable).
9
+ */
10
+ let tenantDataEndpoint = "/api/tenant/data";
11
+ let tenantSwitchEndpoint = "/api/tenant/switch";
12
+ let tenantCookieName = "__spine_tenant";
13
+ let customFetchTenantData = null;
14
+ let customSwitchTenant = null;
15
+ let reloadOnSwitch = true;
16
+ /**
17
+ * Configure tenant client behavior for any backend contract.
18
+ */
19
+ function configureTenantClient(config) {
20
+ if (config.tenantDataEndpoint || config.organizationEndpoint) tenantDataEndpoint = config.tenantDataEndpoint ?? config.organizationEndpoint ?? tenantDataEndpoint;
21
+ if (config.tenantSwitchEndpoint) tenantSwitchEndpoint = config.tenantSwitchEndpoint;
22
+ if (config.tenantCookieName) tenantCookieName = config.tenantCookieName;
23
+ if (config.fetchTenantData) customFetchTenantData = config.fetchTenantData;
24
+ if (config.switchTenant) customSwitchTenant = config.switchTenant;
25
+ if (config.reloadOnSwitch !== void 0) reloadOnSwitch = config.reloadOnSwitch;
26
+ }
27
+ /**
28
+ * Configure tenant store endpoints.
29
+ *
30
+ * @deprecated Use configureTenantClient for endpoint and contract customization.
31
+ */
32
+ function configureTenantEndpoints(config) {
33
+ configureTenantClient(config);
34
+ }
35
+ function getTenantCookieName() {
36
+ return tenantCookieName;
37
+ }
38
+ function resetTenantClientConfig() {
39
+ tenantDataEndpoint = "/api/tenant/data";
40
+ tenantSwitchEndpoint = "/api/tenant/switch";
41
+ tenantCookieName = "__spine_tenant";
42
+ customFetchTenantData = null;
43
+ customSwitchTenant = null;
44
+ reloadOnSwitch = true;
45
+ }
46
+ function asOrganizationMap(tenantDataMap) {
47
+ return tenantDataMap;
48
+ }
49
+ function isRecord(value) {
50
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
51
+ }
52
+ async function readJsonResponse(response) {
53
+ if (response.status === 204) return null;
54
+ const text = await response.text();
55
+ return text.trim().length > 0 ? JSON.parse(text) : null;
56
+ }
57
+ function normalizeTenantDataPayload(payload) {
58
+ if (!isRecord(payload)) return null;
59
+ if (payload.success === false) return null;
60
+ const nestedData = payload.data ?? payload.tenant ?? payload.workspace ?? payload.account;
61
+ if (isRecord(nestedData)) return nestedData;
62
+ return payload;
63
+ }
64
+ function normalizeTenantSwitchPayload(payload) {
65
+ if (payload === null || payload === void 0) return { success: true };
66
+ if (!isRecord(payload)) return {
67
+ success: false,
68
+ error: payload
69
+ };
70
+ const success = payload.success ?? payload.ok ?? payload.switched;
71
+ const nestedData = payload.data ?? payload.tenant ?? payload.workspace ?? payload.account;
72
+ return {
73
+ success: success === void 0 ? true : Boolean(success),
74
+ tenantData: isRecord(nestedData) ? nestedData : null,
75
+ reload: typeof payload.reload === "boolean" ? payload.reload : void 0,
76
+ error: payload.error
77
+ };
78
+ }
79
+ async function defaultFetchTenantData({ tenantId, endpoint, fetch: fetchFn }) {
80
+ const response = await fetchFn(`${endpoint}/${encodeURIComponent(tenantId)}`);
81
+ if (!response.ok) return null;
82
+ return normalizeTenantDataPayload(await readJsonResponse(response));
83
+ }
84
+ async function defaultSwitchTenant({ tenantId, endpoint, fetch: fetchFn }) {
85
+ const response = await fetchFn(endpoint, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify({ tenantId })
89
+ });
90
+ if (!response.ok) return {
91
+ success: false,
92
+ error: response.status
93
+ };
94
+ return normalizeTenantSwitchPayload(await readJsonResponse(response));
95
+ }
96
+ const useTenantStore = create()(devtools((set, get) => ({
97
+ currentTenant: null,
98
+ availableTenants: [],
99
+ memberships: new Map(),
100
+ tenantData: new Map(),
101
+ organizations: new Map(),
102
+ isLoading: false,
103
+ isSwitching: false,
104
+ setCurrentTenant: (tenantId) => set({ currentTenant: tenantId }),
105
+ setAvailableTenants: (tenants) => {
106
+ set({ availableTenants: tenants });
107
+ const { tenantData, fetchTenantData } = get();
108
+ tenants.forEach((tenantId) => {
109
+ if (!tenantData.has(tenantId)) fetchTenantData(tenantId);
110
+ });
111
+ },
112
+ setMemberships: (memberships) => {
113
+ const membershipMap = new Map();
114
+ memberships.forEach((membership) => {
115
+ membershipMap.set(membership.tenantId, membership);
116
+ });
117
+ set({ memberships: membershipMap });
118
+ },
119
+ setTenantData: (tenantId, data) => set((state) => ({
120
+ tenantData: new Map(state.tenantData).set(tenantId, data),
121
+ organizations: asOrganizationMap(new Map(state.tenantData).set(tenantId, data))
122
+ })),
123
+ addTenantData: (tenantId, data) => {
124
+ set((state) => {
125
+ const newTenantData = new Map(state.tenantData);
126
+ newTenantData.set(tenantId, data);
127
+ return {
128
+ tenantData: newTenantData,
129
+ organizations: asOrganizationMap(newTenantData)
130
+ };
131
+ });
132
+ },
133
+ setOrganization: (tenantId, data) => get().setTenantData(tenantId, data),
134
+ addOrganization: (tenantId, data) => get().addTenantData(tenantId, data),
135
+ setLoading: (loading) => set({ isLoading: loading }),
136
+ setSwitching: (switching) => set({ isSwitching: switching }),
137
+ fetchTenantData: async (tenantId) => {
138
+ try {
139
+ const tenantData = await (customFetchTenantData ?? defaultFetchTenantData)({
140
+ tenantId,
141
+ endpoint: tenantDataEndpoint,
142
+ fetch
143
+ });
144
+ if (tenantData) get().setTenantData(tenantId, tenantData);
145
+ } catch (error) {
146
+ console.error("Failed to fetch tenant data:", tenantId, error);
147
+ }
148
+ },
149
+ fetchOrganization: async (tenantId) => get().fetchTenantData(tenantId),
150
+ switchTenant: async (tenantId) => {
151
+ const { availableTenants } = get();
152
+ if (!availableTenants.includes(tenantId)) {
153
+ console.error("Tenant not available:", tenantId);
154
+ return;
155
+ }
156
+ set({ isSwitching: true });
157
+ try {
158
+ const result = await (customSwitchTenant ?? defaultSwitchTenant)({
159
+ tenantId,
160
+ endpoint: tenantSwitchEndpoint,
161
+ fetch,
162
+ currentTenant: get().currentTenant
163
+ });
164
+ if (result.success) {
165
+ set({ currentTenant: tenantId });
166
+ if (result.tenantData) get().setTenantData(tenantId, result.tenantData);
167
+ const { tenantData, fetchTenantData } = get();
168
+ if (!tenantData.has(tenantId)) await fetchTenantData(tenantId);
169
+ set({ isSwitching: false });
170
+ if ((result.reload ?? reloadOnSwitch) && typeof window !== "undefined") window.location.reload();
171
+ } else {
172
+ console.error("Failed to switch tenant:", result.error ?? tenantId);
173
+ set({ isSwitching: false });
174
+ }
175
+ } catch (error) {
176
+ console.error("Failed to switch tenant:", error);
177
+ set({ isSwitching: false });
178
+ }
179
+ },
180
+ getMembership: (tenantId) => {
181
+ const { memberships } = get();
182
+ return memberships.get(tenantId) || null;
183
+ },
184
+ getCurrentMembership: () => {
185
+ const { currentTenant, memberships } = get();
186
+ if (!currentTenant) return null;
187
+ return memberships.get(currentTenant) || null;
188
+ },
189
+ getTenantName: (tenantId) => {
190
+ const { memberships, tenantData } = get();
191
+ const membership = memberships.get(tenantId);
192
+ if (membership?.tenantName) return membership.tenantName;
193
+ if (membership?.organizationName) return membership.organizationName;
194
+ return tenantData.get(tenantId)?.name || tenantId;
195
+ },
196
+ getTenantData: (tenantId) => {
197
+ const { tenantData } = get();
198
+ return tenantData.get(tenantId) || null;
199
+ },
200
+ getCurrentTenantData: () => {
201
+ const { currentTenant, tenantData } = get();
202
+ if (!currentTenant) return null;
203
+ return tenantData.get(currentTenant) || null;
204
+ },
205
+ getOrganization: (tenantId) => {
206
+ return get().getTenantData(tenantId);
207
+ },
208
+ getCurrentOrganization: () => {
209
+ return get().getCurrentTenantData();
210
+ }
211
+ }), { name: "TenantStore" }));
212
+ /**
213
+ * Initialize store with server data
214
+ */
215
+ function initializeTenantStore(currentTenant, availableTenants, memberships) {
216
+ const store = useTenantStore.getState();
217
+ store.setCurrentTenant(currentTenant);
218
+ store.setAvailableTenants(availableTenants);
219
+ if (memberships) store.setMemberships(memberships);
220
+ }
221
+
222
+ //#endregion
223
+ //#region src/tenant/tenant-context.tsx
224
+ const TenantContext = createContext(null);
225
+ function TenantProvider({ children, initialTenant = null, initialTenants = [], initialMemberships = [], onTenantChange }) {
226
+ const currentTenant = useTenantStore((state) => state.currentTenant);
227
+ const availableTenants = useTenantStore((state) => state.availableTenants);
228
+ const memberships = useTenantStore((state) => state.memberships);
229
+ const tenantData = useTenantStore((state) => state.tenantData);
230
+ const organizations = useTenantStore((state) => state.organizations);
231
+ const isLoading = useTenantStore((state) => state.isLoading || state.isSwitching);
232
+ const currentTenantData = useTenantStore((state) => state.getCurrentTenantData());
233
+ const currentMembership = useTenantStore((state) => state.getCurrentMembership());
234
+ const setCurrentTenant = useTenantStore((state) => state.setCurrentTenant);
235
+ const setAvailableTenants = useTenantStore((state) => state.setAvailableTenants);
236
+ const setMemberships = useTenantStore((state) => state.setMemberships);
237
+ const switchTenantAction = useTenantStore((state) => state.switchTenant);
238
+ const fetchTenantData = useTenantStore((state) => state.fetchTenantData);
239
+ const getTenantData = useTenantStore((state) => state.getTenantData);
240
+ const getOrganization = useTenantStore((state) => state.getOrganization);
241
+ const getMembership = useTenantStore((state) => state.getMembership);
242
+ const getTenantName = useTenantStore((state) => state.getTenantName);
243
+ useEffect(() => {
244
+ if (initialTenant) setCurrentTenant(initialTenant);
245
+ if (initialTenants.length > 0) setAvailableTenants(initialTenants);
246
+ if (initialMemberships.length > 0) setMemberships(initialMemberships);
247
+ }, [
248
+ initialTenant,
249
+ initialTenants.join(","),
250
+ initialMemberships.map((membership) => membership.tenantId).join(","),
251
+ setCurrentTenant,
252
+ setAvailableTenants,
253
+ setMemberships
254
+ ]);
255
+ useEffect(() => {
256
+ const getCookieTenant = () => {
257
+ const cookies = document.cookie.split(";");
258
+ const cookieName = getTenantCookieName();
259
+ const tenantCookie = cookies.find((cookie) => cookie.trim().startsWith(`${cookieName}=`));
260
+ if (tenantCookie) {
261
+ const value$1 = tenantCookie.split("=")[1];
262
+ return decodeURIComponent(value$1);
263
+ }
264
+ return null;
265
+ };
266
+ if (!currentTenant) {
267
+ const cookieTenant = getCookieTenant();
268
+ if (cookieTenant && availableTenants.includes(cookieTenant)) setCurrentTenant(cookieTenant);
269
+ }
270
+ }, [
271
+ currentTenant,
272
+ availableTenants,
273
+ setCurrentTenant
274
+ ]);
275
+ const switchTenant = (tenantId) => {
276
+ switchTenantAction(tenantId).then(() => {
277
+ if (onTenantChange) onTenantChange();
278
+ });
279
+ };
280
+ const refreshTenants = () => {
281
+ console.log("Tenant refresh requested - should be handled by identity context");
282
+ };
283
+ const refreshTenantData = () => {
284
+ if (currentTenant) fetchTenantData(currentTenant);
285
+ };
286
+ const refreshOrganization = refreshTenantData;
287
+ useEffect(() => {
288
+ if (currentTenant && !currentTenantData) refreshTenantData();
289
+ }, [currentTenant, currentTenantData]);
290
+ const currentOrganization = currentTenantData;
291
+ const value = {
292
+ currentTenant,
293
+ availableTenants,
294
+ memberships,
295
+ currentTenantData,
296
+ tenantData,
297
+ currentMembership,
298
+ isLoading,
299
+ switchTenant,
300
+ refreshTenants,
301
+ refreshTenantData,
302
+ getTenantData,
303
+ getMembership,
304
+ getTenantName,
305
+ currentOrganization,
306
+ organizations,
307
+ refreshOrganization,
308
+ getOrganization
309
+ };
310
+ return /* @__PURE__ */ jsx(TenantContext.Provider, {
311
+ value,
312
+ children
313
+ });
314
+ }
315
+ /**
316
+ * Hook for using tenant context
317
+ */
318
+ function useTenant() {
319
+ const context = useContext(TenantContext);
320
+ if (!context) throw new Error("useTenant must be used within a TenantProvider");
321
+ return context;
322
+ }
323
+ /**
324
+ * Hook for current tenant only
325
+ */
326
+ function useCurrentTenant() {
327
+ const { currentTenant } = useTenant();
328
+ return currentTenant;
329
+ }
330
+ /**
331
+ * Hook for tenant switching
332
+ */
333
+ function useTenantSwitcher() {
334
+ const { switchTenant, isLoading, availableTenants } = useTenant();
335
+ return {
336
+ switchTenant,
337
+ isLoading,
338
+ availableTenants
339
+ };
340
+ }
341
+ /**
342
+ * Hook for current tenant data
343
+ */
344
+ function useCurrentTenantData() {
345
+ const { currentTenantData } = useTenant();
346
+ return currentTenantData;
347
+ }
348
+ /**
349
+ * Hook for tenant data operations
350
+ */
351
+ function useTenantData() {
352
+ const { currentTenantData, refreshTenantData, isLoading } = useTenant();
353
+ return {
354
+ tenantData: currentTenantData,
355
+ refreshTenantData,
356
+ isLoading
357
+ };
358
+ }
359
+ /**
360
+ * Hook for current organization
361
+ *
362
+ * @deprecated Use useCurrentTenantData instead.
363
+ */
364
+ function useCurrentOrganization() {
365
+ const { currentOrganization } = useTenant();
366
+ return currentOrganization;
367
+ }
368
+ /**
369
+ * Hook for organization operations
370
+ *
371
+ * @deprecated Use useTenantData instead.
372
+ */
373
+ function useOrganization() {
374
+ const { currentOrganization, refreshOrganization, isLoading } = useTenant();
375
+ return {
376
+ organization: currentOrganization,
377
+ refreshOrganization,
378
+ isLoading
379
+ };
380
+ }
381
+
382
+ //#endregion
383
+ export { TenantProvider, configureTenantClient, configureTenantEndpoints, getTenantCookieName, initializeTenantStore, normalizeTenantDataPayload, normalizeTenantSwitchPayload, resetTenantClientConfig, useCurrentOrganization, useCurrentTenant, useCurrentTenantData, useOrganization, useTenant, useTenantData, useTenantStore, useTenantSwitcher };
384
+ //# sourceMappingURL=tenant-DfLemb6n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-DfLemb6n.js","names":["customFetchTenantData: TenantClientConfig['fetchTenantData'] | null","customSwitchTenant: TenantClientConfig['switchTenant'] | null","config: TenantClientConfig","tenantDataMap: Map<string, TenantData>","value: unknown","response: Response","payload: unknown","currentTenant: string | null","availableTenants: string[]","memberships?: TenantMembership[]","value","tenantId: string","value: TenantContextType"],"sources":["../src/tenant/tenant-store.ts","../src/tenant/tenant-context.tsx"],"sourcesContent":["/**\n * Tenant Store (Zustand)\n * \n * Client-side state management for tenant data.\n */\n\nimport { create } from 'zustand';\nimport { devtools } from 'zustand/middleware';\nimport type { OrganizationData, TenantData, TenantMembership } from './types';\n\nexport interface TenantDataFetcherContext {\n tenantId: string;\n endpoint: string;\n fetch: typeof fetch;\n}\n\nexport interface TenantSwitchContext {\n tenantId: string;\n endpoint: string;\n fetch: typeof fetch;\n currentTenant: string | null;\n}\n\nexport interface TenantSwitchResult {\n success: boolean;\n tenantData?: TenantData | null;\n reload?: boolean;\n error?: unknown;\n}\n\nexport interface TenantClientConfig {\n tenantDataEndpoint?: string;\n /** @deprecated Use tenantDataEndpoint instead. */\n organizationEndpoint?: string;\n tenantSwitchEndpoint?: string;\n tenantCookieName?: string;\n fetchTenantData?: (context: TenantDataFetcherContext) => Promise<TenantData | null>;\n switchTenant?: (context: TenantSwitchContext) => Promise<TenantSwitchResult>;\n reloadOnSwitch?: boolean;\n}\n\nexport interface TenantState {\n // State\n currentTenant: string | null;\n availableTenants: string[];\n memberships: Map<string, TenantMembership>;\n tenantData: Map<string, TenantData>;\n /** @deprecated Use tenantData instead. */\n organizations: Map<string, OrganizationData>;\n isLoading: boolean;\n isSwitching: boolean;\n\n // Actions\n setCurrentTenant: (tenantId: string | null) => void;\n setAvailableTenants: (tenants: string[]) => void;\n setMemberships: (memberships: TenantMembership[]) => void;\n setTenantData: (tenantId: string, data: TenantData) => void;\n addTenantData: (tenantId: string, data: TenantData) => void;\n /** @deprecated Use setTenantData instead. */\n setOrganization: (tenantId: string, data: OrganizationData) => void;\n /** @deprecated Use addTenantData instead. */\n addOrganization: (tenantId: string, data: OrganizationData) => void;\n setLoading: (loading: boolean) => void;\n setSwitching: (switching: boolean) => void;\n\n // Async actions\n fetchTenantData: (tenantId: string) => Promise<void>;\n /** @deprecated Use fetchTenantData instead. */\n fetchOrganization: (tenantId: string) => Promise<void>;\n switchTenant: (tenantId: string) => Promise<void>;\n\n // Getters\n getMembership: (tenantId: string) => TenantMembership | null;\n getCurrentMembership: () => TenantMembership | null;\n getTenantName: (tenantId: string) => string;\n getTenantData: (tenantId: string) => TenantData | null;\n getCurrentTenantData: () => TenantData | null;\n /** @deprecated Use getTenantData instead. */\n getOrganization: (tenantId: string) => OrganizationData | null;\n /** @deprecated Use getCurrentTenantData instead. */\n getCurrentOrganization: () => OrganizationData | null;\n}\n\n/**\n * Tenant client endpoints and browser-visible tenant cookie (configurable).\n */\nlet tenantDataEndpoint = '/api/tenant/data';\nlet tenantSwitchEndpoint = '/api/tenant/switch';\nlet tenantCookieName = '__spine_tenant';\nlet customFetchTenantData: TenantClientConfig['fetchTenantData'] | null = null;\nlet customSwitchTenant: TenantClientConfig['switchTenant'] | null = null;\nlet reloadOnSwitch = true;\n\n/**\n * Configure tenant client behavior for any backend contract.\n */\nexport function configureTenantClient(config: TenantClientConfig): void {\n if (config.tenantDataEndpoint || config.organizationEndpoint) {\n tenantDataEndpoint = config.tenantDataEndpoint ?? config.organizationEndpoint ?? tenantDataEndpoint;\n }\n if (config.tenantSwitchEndpoint) {\n tenantSwitchEndpoint = config.tenantSwitchEndpoint;\n }\n if (config.tenantCookieName) {\n tenantCookieName = config.tenantCookieName;\n }\n if (config.fetchTenantData) {\n customFetchTenantData = config.fetchTenantData;\n }\n if (config.switchTenant) {\n customSwitchTenant = config.switchTenant;\n }\n if (config.reloadOnSwitch !== undefined) {\n reloadOnSwitch = config.reloadOnSwitch;\n }\n}\n\n/**\n * Configure tenant store endpoints.\n *\n * @deprecated Use configureTenantClient for endpoint and contract customization.\n */\nexport function configureTenantEndpoints(config: TenantClientConfig): void {\n configureTenantClient(config);\n}\n\nexport function getTenantCookieName(): string {\n return tenantCookieName;\n}\n\nexport function resetTenantClientConfig(): void {\n tenantDataEndpoint = '/api/tenant/data';\n tenantSwitchEndpoint = '/api/tenant/switch';\n tenantCookieName = '__spine_tenant';\n customFetchTenantData = null;\n customSwitchTenant = null;\n reloadOnSwitch = true;\n}\n\nfunction asOrganizationMap(tenantDataMap: Map<string, TenantData>): Map<string, OrganizationData> {\n return tenantDataMap as unknown as Map<string, OrganizationData>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === 'object' && !Array.isArray(value);\n}\n\nasync function readJsonResponse(response: Response): Promise<unknown> {\n if (response.status === 204) {\n return null;\n }\n\n const text = await response.text();\n return text.trim().length > 0 ? JSON.parse(text) : null;\n}\n\nexport function normalizeTenantDataPayload(payload: unknown): TenantData | null {\n if (!isRecord(payload)) {\n return null;\n }\n\n if (payload.success === false) {\n return null;\n }\n\n const nestedData = payload.data ?? payload.tenant ?? payload.workspace ?? payload.account;\n if (isRecord(nestedData)) {\n return nestedData as TenantData;\n }\n\n return payload as TenantData;\n}\n\nexport function normalizeTenantSwitchPayload(payload: unknown): TenantSwitchResult {\n if (payload === null || payload === undefined) {\n return { success: true };\n }\n\n if (!isRecord(payload)) {\n return { success: false, error: payload };\n }\n\n const success = payload.success ?? payload.ok ?? payload.switched;\n const nestedData = payload.data ?? payload.tenant ?? payload.workspace ?? payload.account;\n\n return {\n success: success === undefined ? true : Boolean(success),\n tenantData: isRecord(nestedData) ? nestedData as TenantData : null,\n reload: typeof payload.reload === 'boolean' ? payload.reload : undefined,\n error: payload.error,\n };\n}\n\nasync function defaultFetchTenantData({ tenantId, endpoint, fetch: fetchFn }: TenantDataFetcherContext): Promise<TenantData | null> {\n const response = await fetchFn(`${endpoint}/${encodeURIComponent(tenantId)}`);\n if (!response.ok) {\n return null;\n }\n\n return normalizeTenantDataPayload(await readJsonResponse(response));\n}\n\nasync function defaultSwitchTenant({ tenantId, endpoint, fetch: fetchFn }: TenantSwitchContext): Promise<TenantSwitchResult> {\n const response = await fetchFn(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ tenantId }),\n });\n\n if (!response.ok) {\n return { success: false, error: response.status };\n }\n\n return normalizeTenantSwitchPayload(await readJsonResponse(response));\n}\n\nexport const useTenantStore = create<TenantState>()(\n devtools(\n (set, get) => ({\n // Initial state\n currentTenant: null,\n availableTenants: [],\n memberships: new Map(),\n tenantData: new Map(),\n organizations: new Map(),\n isLoading: false,\n isSwitching: false,\n\n // Setters\n setCurrentTenant: (tenantId) => set({ currentTenant: tenantId }),\n\n setAvailableTenants: (tenants) => {\n set({ availableTenants: tenants });\n\n // Fetch tenant data for all tenants\n const { tenantData, fetchTenantData } = get();\n tenants.forEach((tenantId) => {\n if (!tenantData.has(tenantId)) {\n fetchTenantData(tenantId);\n }\n });\n },\n\n setMemberships: (memberships) => {\n const membershipMap = new Map<string, TenantMembership>();\n memberships.forEach((membership) => {\n membershipMap.set(membership.tenantId, membership);\n });\n set({ memberships: membershipMap });\n },\n\n setTenantData: (tenantId, data) =>\n set((state) => ({\n tenantData: new Map(state.tenantData).set(tenantId, data),\n organizations: asOrganizationMap(new Map(state.tenantData).set(tenantId, data)),\n })),\n\n addTenantData: (tenantId, data) => {\n set((state) => {\n const newTenantData = new Map(state.tenantData);\n newTenantData.set(tenantId, data);\n return {\n tenantData: newTenantData,\n organizations: asOrganizationMap(newTenantData),\n };\n });\n },\n\n setOrganization: (tenantId, data) => get().setTenantData(tenantId, data),\n addOrganization: (tenantId, data) => get().addTenantData(tenantId, data),\n\n setLoading: (loading) => set({ isLoading: loading }),\n setSwitching: (switching) => set({ isSwitching: switching }),\n\n // Async actions\n fetchTenantData: async (tenantId) => {\n try {\n const tenantData = await (customFetchTenantData ?? defaultFetchTenantData)({\n tenantId,\n endpoint: tenantDataEndpoint,\n fetch,\n });\n\n if (tenantData) {\n get().setTenantData(tenantId, tenantData);\n }\n } catch (error) {\n console.error('Failed to fetch tenant data:', tenantId, error);\n }\n },\n\n fetchOrganization: async (tenantId) => get().fetchTenantData(tenantId),\n\n switchTenant: async (tenantId) => {\n const { availableTenants } = get();\n\n if (!availableTenants.includes(tenantId)) {\n console.error('Tenant not available:', tenantId);\n return;\n }\n\n set({ isSwitching: true });\n\n try {\n const result = await (customSwitchTenant ?? defaultSwitchTenant)({\n tenantId,\n endpoint: tenantSwitchEndpoint,\n fetch,\n currentTenant: get().currentTenant,\n });\n\n if (result.success) {\n set({ currentTenant: tenantId });\n\n if (result.tenantData) {\n get().setTenantData(tenantId, result.tenantData);\n }\n\n // Fetch tenant data if not already loaded\n const { tenantData, fetchTenantData } = get();\n if (!tenantData.has(tenantId)) {\n await fetchTenantData(tenantId);\n }\n\n set({ isSwitching: false });\n\n if ((result.reload ?? reloadOnSwitch) && typeof window !== 'undefined') {\n window.location.reload();\n }\n } else {\n console.error('Failed to switch tenant:', result.error ?? tenantId);\n set({ isSwitching: false });\n }\n } catch (error) {\n console.error('Failed to switch tenant:', error);\n set({ isSwitching: false });\n }\n },\n\n // Getters\n getMembership: (tenantId) => {\n const { memberships } = get();\n return memberships.get(tenantId) || null;\n },\n\n getCurrentMembership: () => {\n const { currentTenant, memberships } = get();\n if (!currentTenant) return null;\n return memberships.get(currentTenant) || null;\n },\n\n getTenantName: (tenantId) => {\n const { memberships, tenantData } = get();\n const membership = memberships.get(tenantId);\n if (membership?.tenantName) {\n return membership.tenantName;\n }\n if (membership?.organizationName) {\n return membership.organizationName;\n }\n return tenantData.get(tenantId)?.name || tenantId;\n },\n\n getTenantData: (tenantId) => {\n const { tenantData } = get();\n return tenantData.get(tenantId) || null;\n },\n\n getCurrentTenantData: () => {\n const { currentTenant, tenantData } = get();\n if (!currentTenant) return null;\n return tenantData.get(currentTenant) || null;\n },\n\n getOrganization: (tenantId) => {\n return get().getTenantData(tenantId) as OrganizationData | null;\n },\n\n getCurrentOrganization: () => {\n return get().getCurrentTenantData() as OrganizationData | null;\n },\n }),\n { name: 'TenantStore' }\n )\n);\n\n/**\n * Initialize store with server data\n */\nexport function initializeTenantStore(\n currentTenant: string | null,\n availableTenants: string[],\n memberships?: TenantMembership[]\n) {\n const store = useTenantStore.getState();\n store.setCurrentTenant(currentTenant);\n store.setAvailableTenants(availableTenants);\n if (memberships) {\n store.setMemberships(memberships);\n }\n}\n","/**\n * Tenant Context Provider\n * \n * React context for tenant management.\n */\n\nimport { createContext, useContext, useEffect, type ReactNode } from 'react';\nimport { getTenantCookieName, useTenantStore } from './tenant-store';\nimport type { OrganizationData, TenantContextType, TenantData, TenantMembership } from './types';\n\nconst TenantContext = createContext<TenantContextType | null>(null);\n\ninterface TenantProviderProps {\n children: ReactNode;\n initialTenant?: string | null;\n initialTenants?: string[];\n initialMemberships?: TenantMembership[];\n onTenantChange?: () => void;\n}\n\nexport function TenantProvider({\n children,\n initialTenant = null,\n initialTenants = [],\n initialMemberships = [],\n onTenantChange,\n}: TenantProviderProps) {\n const currentTenant = useTenantStore((state) => state.currentTenant);\n const availableTenants = useTenantStore((state) => state.availableTenants);\n const memberships = useTenantStore((state) => state.memberships);\n const tenantData = useTenantStore((state) => state.tenantData);\n const organizations = useTenantStore((state) => state.organizations);\n const isLoading = useTenantStore((state) => state.isLoading || state.isSwitching);\n const currentTenantData = useTenantStore((state) => state.getCurrentTenantData());\n const currentMembership = useTenantStore((state) => state.getCurrentMembership());\n\n const setCurrentTenant = useTenantStore((state) => state.setCurrentTenant);\n const setAvailableTenants = useTenantStore((state) => state.setAvailableTenants);\n const setMemberships = useTenantStore((state) => state.setMemberships);\n const switchTenantAction = useTenantStore((state) => state.switchTenant);\n const fetchTenantData = useTenantStore((state) => state.fetchTenantData);\n const getTenantData = useTenantStore((state) => state.getTenantData);\n const getOrganization = useTenantStore((state) => state.getOrganization);\n const getMembership = useTenantStore((state) => state.getMembership);\n const getTenantName = useTenantStore((state) => state.getTenantName);\n\n // Initialize store with initial values\n useEffect(() => {\n if (initialTenant) {\n setCurrentTenant(initialTenant);\n }\n if (initialTenants.length > 0) {\n setAvailableTenants(initialTenants);\n }\n if (initialMemberships.length > 0) {\n setMemberships(initialMemberships);\n }\n }, [\n initialTenant,\n initialTenants.join(','),\n initialMemberships.map((membership) => membership.tenantId).join(','),\n setCurrentTenant,\n setAvailableTenants,\n setMemberships,\n ]);\n\n // Read tenant from cookie (client-side)\n useEffect(() => {\n const getCookieTenant = () => {\n const cookies = document.cookie.split(';');\n const cookieName = getTenantCookieName();\n const tenantCookie = cookies.find((cookie) =>\n cookie.trim().startsWith(`${cookieName}=`)\n );\n\n if (tenantCookie) {\n const value = tenantCookie.split('=')[1];\n return decodeURIComponent(value);\n }\n\n return null;\n };\n\n if (!currentTenant) {\n const cookieTenant = getCookieTenant();\n if (cookieTenant && availableTenants.includes(cookieTenant)) {\n setCurrentTenant(cookieTenant);\n }\n }\n }, [currentTenant, availableTenants, setCurrentTenant]);\n\n // Switch tenant handler\n const switchTenant = (tenantId: string) => {\n switchTenantAction(tenantId).then(() => {\n if (onTenantChange) {\n onTenantChange();\n }\n });\n };\n\n // Refresh tenants (handled by identity context)\n const refreshTenants = () => {\n console.log('Tenant refresh requested - should be handled by identity context');\n };\n\n // Refresh tenant data\n const refreshTenantData = () => {\n if (currentTenant) {\n fetchTenantData(currentTenant);\n }\n };\n\n const refreshOrganization = refreshTenantData;\n\n // Load tenant data when tenant is set\n useEffect(() => {\n if (currentTenant && !currentTenantData) {\n refreshTenantData();\n }\n }, [currentTenant, currentTenantData]);\n\n const currentOrganization = currentTenantData as OrganizationData | null;\n\n const value: TenantContextType = {\n currentTenant,\n availableTenants,\n memberships,\n currentTenantData,\n tenantData,\n currentMembership,\n isLoading,\n switchTenant,\n refreshTenants,\n refreshTenantData,\n getTenantData,\n getMembership,\n getTenantName,\n currentOrganization,\n organizations,\n refreshOrganization,\n getOrganization,\n };\n\n return (\n <TenantContext.Provider value={value}>\n {children}\n </TenantContext.Provider>\n );\n}\n\n/**\n * Hook for using tenant context\n */\nexport function useTenant(): TenantContextType {\n const context = useContext(TenantContext);\n\n if (!context) {\n throw new Error('useTenant must be used within a TenantProvider');\n }\n\n return context;\n}\n\n/**\n * Hook for current tenant only\n */\nexport function useCurrentTenant(): string | null {\n const { currentTenant } = useTenant();\n return currentTenant;\n}\n\n/**\n * Hook for tenant switching\n */\nexport function useTenantSwitcher() {\n const { switchTenant, isLoading, availableTenants } = useTenant();\n return { switchTenant, isLoading, availableTenants };\n}\n\n/**\n * Hook for current tenant data\n */\nexport function useCurrentTenantData(): TenantData | null {\n const { currentTenantData } = useTenant();\n return currentTenantData;\n}\n\n/**\n * Hook for tenant data operations\n */\nexport function useTenantData() {\n const { currentTenantData, refreshTenantData, isLoading } = useTenant();\n\n return {\n tenantData: currentTenantData,\n refreshTenantData,\n isLoading,\n };\n}\n\n/**\n * Hook for current organization\n *\n * @deprecated Use useCurrentTenantData instead.\n */\nexport function useCurrentOrganization(): OrganizationData | null {\n const { currentOrganization } = useTenant();\n return currentOrganization;\n}\n\n/**\n * Hook for organization operations\n *\n * @deprecated Use useTenantData instead.\n */\nexport function useOrganization() {\n const { currentOrganization, refreshOrganization, isLoading } = useTenant();\n\n return {\n organization: currentOrganization,\n refreshOrganization,\n isLoading,\n };\n}\n"],"mappings":";;;;;;;;;AAsFA,IAAI,qBAAqB;AACzB,IAAI,uBAAuB;AAC3B,IAAI,mBAAmB;AACvB,IAAIA,wBAAsE;AAC1E,IAAIC,qBAAgE;AACpE,IAAI,iBAAiB;;;;AAKrB,SAAgB,sBAAsBC,QAAkC;AACtE,KAAI,OAAO,sBAAsB,OAAO,qBACtC,sBAAqB,OAAO,sBAAsB,OAAO,wBAAwB;AAEnF,KAAI,OAAO,qBACT,wBAAuB,OAAO;AAEhC,KAAI,OAAO,iBACT,oBAAmB,OAAO;AAE5B,KAAI,OAAO,gBACT,yBAAwB,OAAO;AAEjC,KAAI,OAAO,aACT,sBAAqB,OAAO;AAE9B,KAAI,OAAO,0BACT,kBAAiB,OAAO;AAE3B;;;;;;AAOD,SAAgB,yBAAyBA,QAAkC;AACzE,uBAAsB,OAAO;AAC9B;AAED,SAAgB,sBAA8B;AAC5C,QAAO;AACR;AAED,SAAgB,0BAAgC;AAC9C,sBAAqB;AACrB,wBAAuB;AACvB,oBAAmB;AACnB,yBAAwB;AACxB,sBAAqB;AACrB,kBAAiB;AAClB;AAED,SAAS,kBAAkBC,eAAuE;AAChG,QAAO;AACR;AAED,SAAS,SAASC,OAAkD;AAClE,QAAO,QAAQ,MAAM,WAAW,UAAU,aAAa,MAAM,QAAQ,MAAM;AAC5E;AAED,eAAe,iBAAiBC,UAAsC;AACpE,KAAI,SAAS,WAAW,IACtB,QAAO;CAGT,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,QAAO,KAAK,MAAM,CAAC,SAAS,IAAI,KAAK,MAAM,KAAK,GAAG;AACpD;AAED,SAAgB,2BAA2BC,SAAqC;AAC9E,MAAK,SAAS,QAAQ,CACpB,QAAO;AAGT,KAAI,QAAQ,YAAY,MACtB,QAAO;CAGT,MAAM,aAAa,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,aAAa,QAAQ;AAClF,KAAI,SAAS,WAAW,CACtB,QAAO;AAGT,QAAO;AACR;AAED,SAAgB,6BAA6BA,SAAsC;AACjF,KAAI,YAAY,QAAQ,mBACtB,QAAO,EAAE,SAAS,KAAM;AAG1B,MAAK,SAAS,QAAQ,CACpB,QAAO;EAAE,SAAS;EAAO,OAAO;CAAS;CAG3C,MAAM,UAAU,QAAQ,WAAW,QAAQ,MAAM,QAAQ;CACzD,MAAM,aAAa,QAAQ,QAAQ,QAAQ,UAAU,QAAQ,aAAa,QAAQ;AAElF,QAAO;EACL,SAAS,qBAAwB,OAAO,QAAQ,QAAQ;EACxD,YAAY,SAAS,WAAW,GAAG,aAA2B;EAC9D,eAAe,QAAQ,WAAW,YAAY,QAAQ;EACtD,OAAO,QAAQ;CAChB;AACF;AAED,eAAe,uBAAuB,EAAE,UAAU,UAAU,OAAO,SAAmC,EAA8B;CAClI,MAAM,WAAW,MAAM,SAAS,EAAE,SAAS,GAAG,mBAAmB,SAAS,CAAC,EAAE;AAC7E,MAAK,SAAS,GACZ,QAAO;AAGT,QAAO,2BAA2B,MAAM,iBAAiB,SAAS,CAAC;AACpE;AAED,eAAe,oBAAoB,EAAE,UAAU,UAAU,OAAO,SAA8B,EAA+B;CAC3H,MAAM,WAAW,MAAM,QAAQ,UAAU;EACvC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAoB;EAC/C,MAAM,KAAK,UAAU,EAAE,SAAU,EAAC;CACnC,EAAC;AAEF,MAAK,SAAS,GACZ,QAAO;EAAE,SAAS;EAAO,OAAO,SAAS;CAAQ;AAGnD,QAAO,6BAA6B,MAAM,iBAAiB,SAAS,CAAC;AACtE;AAED,MAAa,iBAAiB,QAAqB,CACjD,SACE,CAAC,KAAK,SAAS;CAEb,eAAe;CACf,kBAAkB,CAAE;CACpB,aAAa,IAAI;CACjB,YAAY,IAAI;CAChB,eAAe,IAAI;CACnB,WAAW;CACX,aAAa;CAGb,kBAAkB,CAAC,aAAa,IAAI,EAAE,eAAe,SAAU,EAAC;CAEhE,qBAAqB,CAAC,YAAY;AAChC,MAAI,EAAE,kBAAkB,QAAS,EAAC;EAGlC,MAAM,EAAE,YAAY,iBAAiB,GAAG,KAAK;AAC7C,UAAQ,QAAQ,CAAC,aAAa;AAC5B,QAAK,WAAW,IAAI,SAAS,CAC3B,iBAAgB,SAAS;EAE5B,EAAC;CACH;CAED,gBAAgB,CAAC,gBAAgB;EAC/B,MAAM,gBAAgB,IAAI;AAC1B,cAAY,QAAQ,CAAC,eAAe;AAClC,iBAAc,IAAI,WAAW,UAAU,WAAW;EACnD,EAAC;AACF,MAAI,EAAE,aAAa,cAAe,EAAC;CACpC;CAED,eAAe,CAAC,UAAU,SACxB,IAAI,CAAC,WAAW;EACd,YAAY,IAAI,IAAI,MAAM,YAAY,IAAI,UAAU,KAAK;EACzD,eAAe,kBAAkB,IAAI,IAAI,MAAM,YAAY,IAAI,UAAU,KAAK,CAAC;CAChF,GAAE;CAEL,eAAe,CAAC,UAAU,SAAS;AACjC,MAAI,CAAC,UAAU;GACb,MAAM,gBAAgB,IAAI,IAAI,MAAM;AACpC,iBAAc,IAAI,UAAU,KAAK;AACjC,UAAO;IACL,YAAY;IACZ,eAAe,kBAAkB,cAAc;GAChD;EACF,EAAC;CACH;CAED,iBAAiB,CAAC,UAAU,SAAS,KAAK,CAAC,cAAc,UAAU,KAAK;CACxE,iBAAiB,CAAC,UAAU,SAAS,KAAK,CAAC,cAAc,UAAU,KAAK;CAExE,YAAY,CAAC,YAAY,IAAI,EAAE,WAAW,QAAS,EAAC;CACpD,cAAc,CAAC,cAAc,IAAI,EAAE,aAAa,UAAW,EAAC;CAG5D,iBAAiB,OAAO,aAAa;AACnC,MAAI;GACF,MAAM,aAAa,MAAM,CAAC,yBAAyB,wBAAwB;IACzE;IACA,UAAU;IACV;GACD,EAAC;AAEF,OAAI,WACF,MAAK,CAAC,cAAc,UAAU,WAAW;EAE5C,SAAQ,OAAO;AACd,WAAQ,MAAM,gCAAgC,UAAU,MAAM;EAC/D;CACF;CAED,mBAAmB,OAAO,aAAa,KAAK,CAAC,gBAAgB,SAAS;CAEtE,cAAc,OAAO,aAAa;EAChC,MAAM,EAAE,kBAAkB,GAAG,KAAK;AAElC,OAAK,iBAAiB,SAAS,SAAS,EAAE;AACxC,WAAQ,MAAM,yBAAyB,SAAS;AAChD;EACD;AAED,MAAI,EAAE,aAAa,KAAM,EAAC;AAE1B,MAAI;GACF,MAAM,SAAS,MAAM,CAAC,sBAAsB,qBAAqB;IAC/D;IACA,UAAU;IACV;IACA,eAAe,KAAK,CAAC;GACtB,EAAC;AAEF,OAAI,OAAO,SAAS;AAClB,QAAI,EAAE,eAAe,SAAU,EAAC;AAEhC,QAAI,OAAO,WACT,MAAK,CAAC,cAAc,UAAU,OAAO,WAAW;IAIlD,MAAM,EAAE,YAAY,iBAAiB,GAAG,KAAK;AAC7C,SAAK,WAAW,IAAI,SAAS,CAC3B,OAAM,gBAAgB,SAAS;AAGjC,QAAI,EAAE,aAAa,MAAO,EAAC;AAE3B,SAAK,OAAO,UAAU,0BAA0B,WAAW,YACzD,QAAO,SAAS,QAAQ;GAE3B,OAAM;AACL,YAAQ,MAAM,4BAA4B,OAAO,SAAS,SAAS;AACnE,QAAI,EAAE,aAAa,MAAO,EAAC;GAC5B;EACF,SAAQ,OAAO;AACd,WAAQ,MAAM,4BAA4B,MAAM;AAChD,OAAI,EAAE,aAAa,MAAO,EAAC;EAC5B;CACF;CAGD,eAAe,CAAC,aAAa;EAC3B,MAAM,EAAE,aAAa,GAAG,KAAK;AAC7B,SAAO,YAAY,IAAI,SAAS,IAAI;CACrC;CAED,sBAAsB,MAAM;EAC1B,MAAM,EAAE,eAAe,aAAa,GAAG,KAAK;AAC5C,OAAK,cAAe,QAAO;AAC3B,SAAO,YAAY,IAAI,cAAc,IAAI;CAC1C;CAED,eAAe,CAAC,aAAa;EAC3B,MAAM,EAAE,aAAa,YAAY,GAAG,KAAK;EACzC,MAAM,aAAa,YAAY,IAAI,SAAS;AAC5C,MAAI,YAAY,WACd,QAAO,WAAW;AAEpB,MAAI,YAAY,iBACd,QAAO,WAAW;AAEpB,SAAO,WAAW,IAAI,SAAS,EAAE,QAAQ;CAC1C;CAED,eAAe,CAAC,aAAa;EAC3B,MAAM,EAAE,YAAY,GAAG,KAAK;AAC5B,SAAO,WAAW,IAAI,SAAS,IAAI;CACpC;CAED,sBAAsB,MAAM;EAC1B,MAAM,EAAE,eAAe,YAAY,GAAG,KAAK;AAC3C,OAAK,cAAe,QAAO;AAC3B,SAAO,WAAW,IAAI,cAAc,IAAI;CACzC;CAED,iBAAiB,CAAC,aAAa;AAC7B,SAAO,KAAK,CAAC,cAAc,SAAS;CACrC;CAED,wBAAwB,MAAM;AAC5B,SAAO,KAAK,CAAC,sBAAsB;CACpC;AACF,IACD,EAAE,MAAM,cAAe,EACxB,CACF;;;;AAKD,SAAgB,sBACdC,eACAC,kBACAC,aACA;CACA,MAAM,QAAQ,eAAe,UAAU;AACvC,OAAM,iBAAiB,cAAc;AACrC,OAAM,oBAAoB,iBAAiB;AAC3C,KAAI,YACF,OAAM,eAAe,YAAY;AAEpC;;;;ACtYD,MAAM,gBAAgB,cAAwC,KAAK;AAUnE,SAAgB,eAAe,EAC7B,UACA,gBAAgB,MAChB,iBAAiB,CAAE,GACnB,qBAAqB,CAAE,GACvB,gBACoB,EAAE;CACtB,MAAM,gBAAgB,eAAe,CAAC,UAAU,MAAM,cAAc;CACpE,MAAM,mBAAmB,eAAe,CAAC,UAAU,MAAM,iBAAiB;CAC1E,MAAM,cAAc,eAAe,CAAC,UAAU,MAAM,YAAY;CAChE,MAAM,aAAa,eAAe,CAAC,UAAU,MAAM,WAAW;CAC9D,MAAM,gBAAgB,eAAe,CAAC,UAAU,MAAM,cAAc;CACpE,MAAM,YAAY,eAAe,CAAC,UAAU,MAAM,aAAa,MAAM,YAAY;CACjF,MAAM,oBAAoB,eAAe,CAAC,UAAU,MAAM,sBAAsB,CAAC;CACjF,MAAM,oBAAoB,eAAe,CAAC,UAAU,MAAM,sBAAsB,CAAC;CAEjF,MAAM,mBAAmB,eAAe,CAAC,UAAU,MAAM,iBAAiB;CAC1E,MAAM,sBAAsB,eAAe,CAAC,UAAU,MAAM,oBAAoB;CAChF,MAAM,iBAAiB,eAAe,CAAC,UAAU,MAAM,eAAe;CACtE,MAAM,qBAAqB,eAAe,CAAC,UAAU,MAAM,aAAa;CACxE,MAAM,kBAAkB,eAAe,CAAC,UAAU,MAAM,gBAAgB;CACxE,MAAM,gBAAgB,eAAe,CAAC,UAAU,MAAM,cAAc;CACpE,MAAM,kBAAkB,eAAe,CAAC,UAAU,MAAM,gBAAgB;CACxE,MAAM,gBAAgB,eAAe,CAAC,UAAU,MAAM,cAAc;CACpE,MAAM,gBAAgB,eAAe,CAAC,UAAU,MAAM,cAAc;AAGpE,WAAU,MAAM;AACd,MAAI,cACF,kBAAiB,cAAc;AAEjC,MAAI,eAAe,SAAS,EAC1B,qBAAoB,eAAe;AAErC,MAAI,mBAAmB,SAAS,EAC9B,gBAAe,mBAAmB;CAErC,GAAE;EACD;EACA,eAAe,KAAK,IAAI;EACxB,mBAAmB,IAAI,CAAC,eAAe,WAAW,SAAS,CAAC,KAAK,IAAI;EACrE;EACA;EACA;CACD,EAAC;AAGF,WAAU,MAAM;EACd,MAAM,kBAAkB,MAAM;GAC5B,MAAM,UAAU,SAAS,OAAO,MAAM,IAAI;GAC1C,MAAM,aAAa,qBAAqB;GACxC,MAAM,eAAe,QAAQ,KAAK,CAAC,WACjC,OAAO,MAAM,CAAC,YAAY,EAAE,WAAW,GAAG,CAC3C;AAED,OAAI,cAAc;IAChB,MAAMC,UAAQ,aAAa,MAAM,IAAI,CAAC;AACtC,WAAO,mBAAmBA,QAAM;GACjC;AAED,UAAO;EACR;AAED,OAAK,eAAe;GAClB,MAAM,eAAe,iBAAiB;AACtC,OAAI,gBAAgB,iBAAiB,SAAS,aAAa,CACzD,kBAAiB,aAAa;EAEjC;CACF,GAAE;EAAC;EAAe;EAAkB;CAAiB,EAAC;CAGvD,MAAM,eAAe,CAACC,aAAqB;AACzC,qBAAmB,SAAS,CAAC,KAAK,MAAM;AACtC,OAAI,eACF,iBAAgB;EAEnB,EAAC;CACH;CAGD,MAAM,iBAAiB,MAAM;AAC3B,UAAQ,IAAI,mEAAmE;CAChF;CAGD,MAAM,oBAAoB,MAAM;AAC9B,MAAI,cACF,iBAAgB,cAAc;CAEjC;CAED,MAAM,sBAAsB;AAG5B,WAAU,MAAM;AACd,MAAI,kBAAkB,kBACpB,oBAAmB;CAEtB,GAAE,CAAC,eAAe,iBAAkB,EAAC;CAEtC,MAAM,sBAAsB;CAE5B,MAAMC,QAA2B;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD;AAED,wBACE,IAAC,cAAc;EAAgB;EAC5B;GACsB;AAE5B;;;;AAKD,SAAgB,YAA+B;CAC7C,MAAM,UAAU,WAAW,cAAc;AAEzC,MAAK,QACH,OAAM,IAAI,MAAM;AAGlB,QAAO;AACR;;;;AAKD,SAAgB,mBAAkC;CAChD,MAAM,EAAE,eAAe,GAAG,WAAW;AACrC,QAAO;AACR;;;;AAKD,SAAgB,oBAAoB;CAClC,MAAM,EAAE,cAAc,WAAW,kBAAkB,GAAG,WAAW;AACjE,QAAO;EAAE;EAAc;EAAW;CAAkB;AACrD;;;;AAKD,SAAgB,uBAA0C;CACxD,MAAM,EAAE,mBAAmB,GAAG,WAAW;AACzC,QAAO;AACR;;;;AAKD,SAAgB,gBAAgB;CAC9B,MAAM,EAAE,mBAAmB,mBAAmB,WAAW,GAAG,WAAW;AAEvE,QAAO;EACL,YAAY;EACZ;EACA;CACD;AACF;;;;;;AAOD,SAAgB,yBAAkD;CAChE,MAAM,EAAE,qBAAqB,GAAG,WAAW;AAC3C,QAAO;AACR;;;;;;AAOD,SAAgB,kBAAkB;CAChC,MAAM,EAAE,qBAAqB,qBAAqB,WAAW,GAAG,WAAW;AAE3E,QAAO;EACL,cAAc;EACd;EACA;CACD;AACF"}
@@ -5,4 +5,4 @@ function isApiClientError(error) {
5
5
 
6
6
  //#endregion
7
7
  export { isApiClientError };
8
- //# sourceMappingURL=types-BnKaWdfe.js.map
8
+ //# sourceMappingURL=types-BaaE6wjY.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-BnKaWdfe.js","names":["error: unknown"],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (request: Request) => Promise<string | null>;\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiBA,OAAyC;AACxE,eACS,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAElE"}
1
+ {"version":3,"file":"types-BaaE6wjY.js","names":["error: unknown"],"sources":["../src/api-client/types.ts"],"sourcesContent":["/**\n * API Client Types\n */\n\n// Re-export Logger from logging to avoid duplication\nexport type { LoggerConfig as Logger } from '../logging/types';\n\nexport interface APIConfig {\n basePath: string;\n accessToken?: string;\n headers: Record<string, string>;\n tenantId: string;\n credentials?: RequestCredentials;\n baseOptions?: {\n timeout?: number;\n };\n}\n\nexport interface CreateAPIConfigOptions {\n requireTenant?: boolean;\n includeAuth?: boolean;\n tenantId?: string;\n customHeaders?: Record<string, string>;\n}\n\nexport interface ProblemDetails {\n type: string;\n title: string;\n status: number;\n detail: string;\n traceId: string;\n requestId?: string;\n instance?: string;\n meta?: {\n errors?: ValidationError[];\n [key: string]: unknown;\n };\n}\n\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface RetryConfig {\n maxRetries?: number;\n baseDelay?: number;\n maxDelay?: number;\n retryCondition?: (error: unknown) => boolean;\n}\n\nexport interface APIRequestConfig {\n url?: string;\n method?: string;\n headers?: Record<string, string>;\n}\n\nexport interface APIResponse<T = unknown> {\n data: T;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n raw?: Response;\n}\n\nexport interface APIClientError<T = unknown> extends Error {\n isApiClientError: true;\n response?: APIResponse<T>;\n config?: APIRequestConfig;\n problemDetails?: ProblemDetails;\n shouldLogout?: boolean;\n originalError?: unknown;\n cause?: unknown;\n}\n\nexport function isApiClientError(error: unknown): error is APIClientError {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isApiClientError' in error &&\n (error as { isApiClientError?: boolean }).isApiClientError === true\n );\n}\n\nexport type FetchAPI = typeof fetch;\n\nexport interface FetchRequestContext {\n fetch: FetchAPI;\n url: string;\n init: RequestInit;\n}\n\nexport interface FetchResponseContext extends FetchRequestContext {\n response: Response;\n}\n\nexport interface FetchErrorContext extends FetchRequestContext {\n error: unknown;\n response?: Response;\n}\n\nexport interface FetchMiddleware {\n pre?: (context: FetchRequestContext) => Promise<{ url: string; init: RequestInit } | void>;\n post?: (context: FetchResponseContext) => Promise<Response | void>;\n onError?: (context: FetchErrorContext) => Promise<Response | void>;\n}\n\nexport type EnhancedAPIClient<T> = {\n [K in keyof T]: T[K] extends (...args: infer A) => Promise<infer R>\n ? K extends `${string}Raw`\n ? T[K]\n : (...args: A) => Promise<APIResponse<R>>\n : T[K];\n};\n\n/**\n * API Logger interface for dependency injection (simplified)\n */\nexport interface APILogger {\n debug?: (message: string, context?: Record<string, unknown>) => void;\n info?: (message: string, context?: Record<string, unknown>) => void;\n warn?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n error?: (message: string, error?: Error, context?: Record<string, unknown>) => void;\n}\n\n/**\n * Function type for getting access token from request\n */\nexport type GetAccessTokenFn = (request: Request) => Promise<string | null>;\n\n/**\n * Function type for getting current tenant from request\n */\nexport type GetCurrentTenantFn = (request: Request) => Promise<string | null>;\n\n/**\n * Token refresh result\n */\nexport interface TokenRefreshResult {\n success: boolean;\n newAccessToken?: string;\n shouldLogout?: boolean;\n error?: string;\n}\n\n/**\n * Function type for attempting token refresh\n */\nexport type TokenRefreshFn = (request: Request) => Promise<TokenRefreshResult>;\n"],"mappings":";AA4EA,SAAgB,iBAAiBA,OAAyC;AACxE,eACS,UAAU,YACjB,UAAU,QACV,sBAAsB,SACrB,MAAyC,qBAAqB;AAElE"}
@@ -0,0 +1,87 @@
1
+ # Adapters
2
+
3
+ Spine keeps framework-specific behavior behind adapter entry points.
4
+
5
+ ## Why Adapters Exist
6
+
7
+ Core primitives should not know about:
8
+
9
+ - React Router redirects
10
+ - Next.js request helpers
11
+ - Remix data APIs
12
+ - framework-specific response classes
13
+
14
+ Adapters solve that by providing a framework-shaped surface without polluting the core package.
15
+
16
+ ## Current Adapter: React Router
17
+
18
+ Available entry points:
19
+
20
+ - `@eminuckan/spine/react-router`
21
+ - `@eminuckan/spine/react-router/server`
22
+
23
+ Today these are thin aliases over the core exports. That is intentional.
24
+
25
+ Benefits:
26
+
27
+ - apps can import from a framework-named namespace today
28
+ - adapter-specific helpers can be added later without changing import strategy
29
+ - future adapters can follow the same pattern
30
+
31
+ ## Import Strategy
32
+
33
+ ### Use root/core imports when
34
+
35
+ - you are building reusable adapter-agnostic logic
36
+ - you are inside a package that should not know the framework
37
+
38
+ ### Use framework adapter imports when
39
+
40
+ - you are writing app-facing framework integration code
41
+ - you want framework intent to be obvious in the import path
42
+
43
+ Example:
44
+
45
+ ```ts
46
+ import { authRoute, getAccessToken } from '@eminuckan/spine/react-router/server';
47
+ ```
48
+
49
+ ## Writing a New Adapter
50
+
51
+ A future adapter package should:
52
+
53
+ 1. Re-export the relevant core/client or core/server APIs
54
+ 2. Add framework-specific convenience helpers only when they reduce real boilerplate
55
+ 3. Avoid duplicating business logic already present in core
56
+ 4. Keep product-specific decisions out of the adapter
57
+
58
+ ## What Should Not Go Into an Adapter
59
+
60
+ - product-specific setup routes
61
+ - tenant-specific marketing flows
62
+ - billing or entitlement plan assumptions
63
+ - app-specific permission constants
64
+
65
+ Those belong in the consuming application.
66
+
67
+ ## Next.js Direction
68
+
69
+ The likely future shape is:
70
+
71
+ - `@eminuckan/spine/nextjs`
72
+ - `@eminuckan/spine/nextjs/server`
73
+
74
+ The goal would be the same:
75
+
76
+ - map Spine core to Next.js primitives
77
+ - keep reusable infrastructure in core
78
+ - keep app policy local
79
+
80
+ ## Adapter Checklist
81
+
82
+ Before adding framework code to Spine, ask:
83
+
84
+ - Is this actually framework-specific?
85
+ - Does it belong in an adapter instead of core?
86
+ - Can the same problem be solved with configuration first?
87
+ - Is the helper generic across many apps using that framework?