@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.
- package/CHANGELOG.md +42 -0
- package/CODE_OF_CONDUCT.md +39 -0
- package/CONTRIBUTING.md +78 -0
- package/MAINTAINERS.md +43 -0
- package/README.md +111 -55
- package/ROADMAP.md +58 -0
- package/SECURITY.md +37 -0
- package/dist/access/evaluator.d.ts +3 -0
- package/dist/access/evaluator.d.ts.map +1 -0
- package/dist/access/index.d.ts +3 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +4 -0
- package/dist/access/types.d.ts +38 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access-CGrsDMwu.js +61 -0
- package/dist/access-CGrsDMwu.js.map +1 -0
- package/dist/api-client/api-config.server.d.ts +27 -0
- package/dist/api-client/api-config.server.d.ts.map +1 -1
- package/dist/api-client/error-codes.d.ts +6 -31
- package/dist/api-client/error-codes.d.ts.map +1 -1
- package/dist/api-client/index.js +3 -3
- package/dist/api-client/server.d.ts +1 -1
- package/dist/api-client/server.d.ts.map +1 -1
- package/dist/api-client/server.js +4 -4
- package/dist/{api-client-DsrxK05Z.js → api-client-CgZUMukO.js} +9 -26
- package/dist/api-client-CgZUMukO.js.map +1 -0
- package/dist/auth/auth.server.d.ts +21 -16
- package/dist/auth/auth.server.d.ts.map +1 -1
- package/dist/auth/authorization-parameters.d.ts +5 -0
- package/dist/auth/authorization-parameters.d.ts.map +1 -0
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/redis-session-storage.server.d.ts +18 -1
- package/dist/auth/redis-session-storage.server.d.ts.map +1 -1
- package/dist/auth/route-protection.server.d.ts +20 -4
- package/dist/auth/route-protection.server.d.ts.map +1 -1
- package/dist/auth/server.d.ts +1 -1
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/server.js +5 -5
- package/dist/auth/types.d.ts +88 -21
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth.server-D5oiz9aF.js +1066 -0
- package/dist/auth.server-D5oiz9aF.js.map +1 -0
- package/dist/auth.server-lC94lmlx.js +5 -0
- package/dist/identity/identity-context.server.d.ts +2 -0
- package/dist/identity/identity-context.server.d.ts.map +1 -1
- package/dist/identity/identity-store.d.ts +19 -2
- package/dist/identity/identity-store.d.ts.map +1 -1
- package/dist/identity/index.js +2 -2
- package/dist/identity/server.js +3 -3
- package/dist/identity/types.d.ts +3 -1
- package/dist/identity/types.d.ts.map +1 -1
- package/dist/{identity-CFa1txoj.js → identity-C5un-xAs.js} +104 -60
- package/dist/identity-C5un-xAs.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/logging/index.js +1 -1
- package/dist/{logging-_h2mi3L9.js → logging-DviiAkLx.js} +2 -2
- package/dist/{logging-_h2mi3L9.js.map → logging-DviiAkLx.js.map} +1 -1
- package/dist/permission-service-CvGpwI0d.js +95 -0
- package/dist/permission-service-CvGpwI0d.js.map +1 -0
- package/dist/permissions/constants.d.ts +3 -0
- package/dist/permissions/constants.d.ts.map +1 -1
- package/dist/permissions/index.js +2 -1
- package/dist/permissions/permission-store.d.ts +1 -1
- package/dist/permissions/types.d.ts +1 -1
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/{permissions-D5ScGGnd.js → permissions-_n1hIG7Q.js} +8 -106
- package/dist/permissions-_n1hIG7Q.js.map +1 -0
- package/dist/query-client/index.js +1 -1
- package/dist/query-client/types.d.ts +9 -9
- package/dist/query-client/types.d.ts.map +1 -1
- package/dist/{query-client-Bpe08pk9.js → query-client-Bh3TyB8W.js} +2 -2
- package/dist/query-client-Bh3TyB8W.js.map +1 -0
- package/dist/react-router/index.js +12 -10
- package/dist/react-router/server.js +19 -17
- package/dist/redis-session-storage.server-v2VcpFsQ.js +500 -0
- package/dist/redis-session-storage.server-v2VcpFsQ.js.map +1 -0
- package/dist/{server-BHfKHKF_.js → server-B6PCxc-T.js} +39 -87
- package/dist/server-B6PCxc-T.js.map +1 -0
- package/dist/{server-DZmcd2Lr.js → server-C96NMFBy.js} +18 -10
- package/dist/server-C96NMFBy.js.map +1 -0
- package/dist/{server-BxvRxjCK.js → server-Ca-3DJf2.js} +44 -11
- package/dist/server-Ca-3DJf2.js.map +1 -0
- package/dist/{server-C3L8J7iE.js → server-Dxsww9v9.js} +6 -5
- package/dist/server-Dxsww9v9.js.map +1 -0
- package/dist/{server-CouYoZiw.js → server-hGrElJMG.js} +2 -2
- package/dist/{server-CouYoZiw.js.map → server-hGrElJMG.js.map} +1 -1
- package/dist/server.js +19 -17
- package/dist/signalr/index.js +1 -1
- package/dist/signalr/types.d.ts +1 -1
- package/dist/{signalr-SJ-1sIxw.js → signalr-k-70VHKo.js} +3 -2
- package/dist/signalr-k-70VHKo.js.map +1 -0
- package/dist/tenant/index.js +2 -2
- package/dist/tenant/server.js +4 -4
- package/dist/tenant/tenant-context.d.ts +17 -1
- package/dist/tenant/tenant-context.d.ts.map +1 -1
- package/dist/tenant/tenant-cookie.server.d.ts.map +1 -1
- package/dist/tenant/tenant-store.d.ts +54 -9
- package/dist/tenant/tenant-store.d.ts.map +1 -1
- package/dist/tenant/tenant.server.d.ts +20 -1
- package/dist/tenant/tenant.server.d.ts.map +1 -1
- package/dist/tenant/types.d.ts +29 -9
- package/dist/tenant/types.d.ts.map +1 -1
- package/dist/tenant-DfLemb6n.js +384 -0
- package/dist/tenant-DfLemb6n.js.map +1 -0
- package/dist/{types-BnKaWdfe.js → types-BaaE6wjY.js} +1 -1
- package/dist/{types-BnKaWdfe.js.map → types-BaaE6wjY.js.map} +1 -1
- package/docs/adapters.md +87 -0
- package/docs/architecture.md +193 -0
- package/docs/backend-adaptation.md +249 -0
- package/docs/codex-for-oss-application.md +31 -0
- package/docs/codex-maintenance.md +47 -0
- package/docs/installation.md +213 -0
- package/docs/module-reference.md +207 -0
- package/docs/releasing.md +92 -0
- package/package.json +27 -4
- package/dist/api-client-DsrxK05Z.js.map +0 -1
- package/dist/auth.server-BPiCtlJE.js +0 -5
- package/dist/auth.server-Cx2h2qu3.js +0 -724
- package/dist/auth.server-Cx2h2qu3.js.map +0 -1
- package/dist/identity-CFa1txoj.js.map +0 -1
- package/dist/permissions-D5ScGGnd.js.map +0 -1
- package/dist/query-client-Bpe08pk9.js.map +0 -1
- package/dist/redis-session-storage.server-BR_0y-QO.js +0 -250
- package/dist/redis-session-storage.server-BR_0y-QO.js.map +0 -1
- package/dist/server-BHfKHKF_.js.map +0 -1
- package/dist/server-BxvRxjCK.js.map +0 -1
- package/dist/server-C3L8J7iE.js.map +0 -1
- package/dist/server-DZmcd2Lr.js.map +0 -1
- package/dist/signalr-SJ-1sIxw.js.map +0 -1
- package/dist/tenant-C6KPaSCX.js +0 -256
- package/dist/tenant-C6KPaSCX.js.map +0 -1
- /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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
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"}
|
package/docs/adapters.md
ADDED
|
@@ -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?
|