@flowselections/core 1.0.16 → 1.0.19

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 (41) hide show
  1. package/README.md +113 -0
  2. package/dist/auth/handover.functions.d.ts +16 -0
  3. package/dist/auth/handover.functions.d.ts.map +1 -0
  4. package/dist/auth/handover.functions.js +23 -0
  5. package/dist/components/layout/HandoverPage.d.ts +8 -0
  6. package/dist/components/layout/HandoverPage.d.ts.map +1 -0
  7. package/dist/components/layout/HandoverPage.js +70 -0
  8. package/dist/components/layout/LoginPage.d.ts +2 -1
  9. package/dist/components/layout/LoginPage.d.ts.map +1 -1
  10. package/dist/components/layout/LoginPage.js +23 -4
  11. package/dist/components/layout/Sidebar.d.ts +1 -1
  12. package/dist/components/layout/Sidebar.d.ts.map +1 -1
  13. package/dist/components/layout/Sidebar.js +4 -1
  14. package/dist/extensions/ModuleGuardSlot.d.ts +10 -0
  15. package/dist/extensions/ModuleGuardSlot.d.ts.map +1 -0
  16. package/dist/extensions/ModuleGuardSlot.js +13 -0
  17. package/dist/extensions/ShellExtensions.d.ts +31 -0
  18. package/dist/extensions/ShellExtensions.d.ts.map +1 -0
  19. package/dist/extensions/ShellExtensions.js +45 -0
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +8 -0
  23. package/dist/permissions/ModuleAccessGuard.d.ts +19 -0
  24. package/dist/permissions/ModuleAccessGuard.d.ts.map +1 -0
  25. package/dist/permissions/ModuleAccessGuard.js +20 -0
  26. package/dist/permissions/useModulePermission.d.ts +217 -0
  27. package/dist/permissions/useModulePermission.d.ts.map +1 -0
  28. package/dist/permissions/useModulePermission.js +137 -0
  29. package/dist/server/index.d.ts +4 -0
  30. package/dist/server/index.d.ts.map +1 -1
  31. package/dist/server/index.js +4 -0
  32. package/dist/server/permissions.server.d.ts +16 -0
  33. package/dist/server/permissions.server.d.ts.map +1 -0
  34. package/dist/server/permissions.server.js +41 -0
  35. package/dist/server/provider-token.d.ts +17 -0
  36. package/dist/server/provider-token.d.ts.map +1 -1
  37. package/dist/server/provider-token.js +53 -6
  38. package/dist/types/module.d.ts +23 -0
  39. package/dist/types/module.d.ts.map +1 -1
  40. package/dist/types/module.js +24 -1
  41. package/package.json +6 -3
@@ -0,0 +1,217 @@
1
+ import type { ModuleListTransform } from "../extensions/ShellExtensions.js";
2
+ export type PermissionLevel = "none" | "view" | "edit";
3
+ export interface ModulePermissionRow {
4
+ moduleId: string;
5
+ featureId: string | null;
6
+ permission: PermissionLevel;
7
+ }
8
+ /**
9
+ * Leest de rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
10
+ *
11
+ * Ontbreekt de RPC (add-on niet geïnstalleerd) of faalt de call, dan is het
12
+ * resultaat een LEGE lijst zonder zichtbare fout en zonder retry — en dat
13
+ * betekent bij alle consumers default-allow.
14
+ */
15
+ export declare function useMyModulePermissions(): {
16
+ permissions: ModulePermissionRow[];
17
+ /** True zolang we niets weten; callers behandelen dit als allow. */
18
+ isUnknown: boolean;
19
+ data: ModulePermissionRow[];
20
+ error: Error;
21
+ isError: true;
22
+ isPending: false;
23
+ isLoading: false;
24
+ isLoadingError: false;
25
+ isRefetchError: true;
26
+ isSuccess: false;
27
+ isPlaceholderData: false;
28
+ status: "error";
29
+ dataUpdatedAt: number;
30
+ errorUpdatedAt: number;
31
+ failureCount: number;
32
+ failureReason: Error | null;
33
+ errorUpdateCount: number;
34
+ isFetched: boolean;
35
+ isFetchedAfterMount: boolean;
36
+ isFetching: boolean;
37
+ isInitialLoading: boolean;
38
+ isPaused: boolean;
39
+ isRefetching: boolean;
40
+ isStale: boolean;
41
+ isEnabled: boolean;
42
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
43
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
44
+ promise: Promise<ModulePermissionRow[]>;
45
+ } | {
46
+ permissions: ModulePermissionRow[];
47
+ /** True zolang we niets weten; callers behandelen dit als allow. */
48
+ isUnknown: boolean;
49
+ data: ModulePermissionRow[];
50
+ error: null;
51
+ isError: false;
52
+ isPending: false;
53
+ isLoading: false;
54
+ isLoadingError: false;
55
+ isRefetchError: false;
56
+ isSuccess: true;
57
+ isPlaceholderData: false;
58
+ status: "success";
59
+ dataUpdatedAt: number;
60
+ errorUpdatedAt: number;
61
+ failureCount: number;
62
+ failureReason: Error | null;
63
+ errorUpdateCount: number;
64
+ isFetched: boolean;
65
+ isFetchedAfterMount: boolean;
66
+ isFetching: boolean;
67
+ isInitialLoading: boolean;
68
+ isPaused: boolean;
69
+ isRefetching: boolean;
70
+ isStale: boolean;
71
+ isEnabled: boolean;
72
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
73
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
74
+ promise: Promise<ModulePermissionRow[]>;
75
+ } | {
76
+ permissions: ModulePermissionRow[];
77
+ /** True zolang we niets weten; callers behandelen dit als allow. */
78
+ isUnknown: boolean;
79
+ data: undefined;
80
+ error: Error;
81
+ isError: true;
82
+ isPending: false;
83
+ isLoading: false;
84
+ isLoadingError: true;
85
+ isRefetchError: false;
86
+ isSuccess: false;
87
+ isPlaceholderData: false;
88
+ status: "error";
89
+ dataUpdatedAt: number;
90
+ errorUpdatedAt: number;
91
+ failureCount: number;
92
+ failureReason: Error | null;
93
+ errorUpdateCount: number;
94
+ isFetched: boolean;
95
+ isFetchedAfterMount: boolean;
96
+ isFetching: boolean;
97
+ isInitialLoading: boolean;
98
+ isPaused: boolean;
99
+ isRefetching: boolean;
100
+ isStale: boolean;
101
+ isEnabled: boolean;
102
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
103
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
104
+ promise: Promise<ModulePermissionRow[]>;
105
+ } | {
106
+ permissions: ModulePermissionRow[];
107
+ /** True zolang we niets weten; callers behandelen dit als allow. */
108
+ isUnknown: boolean;
109
+ data: undefined;
110
+ error: null;
111
+ isError: false;
112
+ isPending: true;
113
+ isLoading: true;
114
+ isLoadingError: false;
115
+ isRefetchError: false;
116
+ isSuccess: false;
117
+ isPlaceholderData: false;
118
+ status: "pending";
119
+ dataUpdatedAt: number;
120
+ errorUpdatedAt: number;
121
+ failureCount: number;
122
+ failureReason: Error | null;
123
+ errorUpdateCount: number;
124
+ isFetched: boolean;
125
+ isFetchedAfterMount: boolean;
126
+ isFetching: boolean;
127
+ isInitialLoading: boolean;
128
+ isPaused: boolean;
129
+ isRefetching: boolean;
130
+ isStale: boolean;
131
+ isEnabled: boolean;
132
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
133
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
134
+ promise: Promise<ModulePermissionRow[]>;
135
+ } | {
136
+ permissions: ModulePermissionRow[];
137
+ /** True zolang we niets weten; callers behandelen dit als allow. */
138
+ isUnknown: boolean;
139
+ data: undefined;
140
+ error: null;
141
+ isError: false;
142
+ isPending: true;
143
+ isLoadingError: false;
144
+ isRefetchError: false;
145
+ isSuccess: false;
146
+ isPlaceholderData: false;
147
+ status: "pending";
148
+ dataUpdatedAt: number;
149
+ errorUpdatedAt: number;
150
+ failureCount: number;
151
+ failureReason: Error | null;
152
+ errorUpdateCount: number;
153
+ isFetched: boolean;
154
+ isFetchedAfterMount: boolean;
155
+ isFetching: boolean;
156
+ isLoading: boolean;
157
+ isInitialLoading: boolean;
158
+ isPaused: boolean;
159
+ isRefetching: boolean;
160
+ isStale: boolean;
161
+ isEnabled: boolean;
162
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
163
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
164
+ promise: Promise<ModulePermissionRow[]>;
165
+ } | {
166
+ permissions: ModulePermissionRow[];
167
+ /** True zolang we niets weten; callers behandelen dit als allow. */
168
+ isUnknown: boolean;
169
+ data: ModulePermissionRow[];
170
+ isError: false;
171
+ error: null;
172
+ isPending: false;
173
+ isLoading: false;
174
+ isLoadingError: false;
175
+ isRefetchError: false;
176
+ isSuccess: true;
177
+ isPlaceholderData: true;
178
+ status: "success";
179
+ dataUpdatedAt: number;
180
+ errorUpdatedAt: number;
181
+ failureCount: number;
182
+ failureReason: Error | null;
183
+ errorUpdateCount: number;
184
+ isFetched: boolean;
185
+ isFetchedAfterMount: boolean;
186
+ isFetching: boolean;
187
+ isInitialLoading: boolean;
188
+ isPaused: boolean;
189
+ isRefetching: boolean;
190
+ isStale: boolean;
191
+ isEnabled: boolean;
192
+ refetch: (options?: import("@tanstack/query-core").RefetchOptions) => Promise<import("@tanstack/query-core").QueryObserverResult<ModulePermissionRow[], Error>>;
193
+ fetchStatus: import("@tanstack/query-core").FetchStatus;
194
+ promise: Promise<ModulePermissionRow[]>;
195
+ };
196
+ /**
197
+ * Rechtenniveau voor één module (optioneel één feature). Tijdens laden en bij
198
+ * elke fout is het antwoord `edit` (fail-open).
199
+ */
200
+ export declare function useModulePermission(moduleId: string, featureId?: string | null): {
201
+ level: PermissionLevel;
202
+ canView: boolean;
203
+ canEdit: boolean;
204
+ isLoading: boolean;
205
+ };
206
+ /**
207
+ * ModuleListTransform die zijbalkitems met `none` weglaat.
208
+ *
209
+ * - Tijdens laden / bij fouten: lijst ongewijzigd.
210
+ * - Modules zonder rij in het rechtenschema: default-allow.
211
+ * - Groepen: de kinderen bepalen zichtbaarheid. Blijft er geen kind over, dan
212
+ * valt de groep weg. Is de parent-sleutel geweigerd maar een kind toegestaan,
213
+ * dan blijft de groep staan met alleen de toegestane kinderen — de parent-href
214
+ * van een groep is een uitklapknop, geen bestemming.
215
+ */
216
+ export declare function useModulePermissionTransform(): ModuleListTransform;
217
+ //# sourceMappingURL=useModulePermission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useModulePermission.d.ts","sourceRoot":"","sources":["../../src/core/permissions/useModulePermission.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,eAAe,CAAC;CAC7B;AAuCD;;;;;;GAMG;AACH,wBAAgB,sBAAsB;;IA0BlC,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAApE,oEAAoE;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGvE;AAyBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;EAW9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,IAAI,mBAAmB,CAyBlE"}
@@ -0,0 +1,137 @@
1
+ import { useCallback } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { supabase } from "../supabase/client.js";
4
+ import { moduleKey, navKey } from "../types/module.js";
5
+ function normalizeLevel(value) {
6
+ const v = typeof value === "string" ? value.toLowerCase() : "";
7
+ if (v === "none" || v === "view" || v === "edit")
8
+ return v;
9
+ // Onbekende waarde → fail-open (zichtbaarheid is geen beveiliging).
10
+ return "edit";
11
+ }
12
+ function normalizeRows(data) {
13
+ if (!Array.isArray(data))
14
+ return [];
15
+ return data.map((raw) => {
16
+ const row = (raw ?? {});
17
+ const rawModule = (row["module_id"] ?? row["moduleId"] ?? "");
18
+ const rawFeature = (row["feature_id"] ?? row["featureId"] ?? null);
19
+ return {
20
+ moduleId: navKey(String(rawModule ?? "")),
21
+ featureId: rawFeature ? String(rawFeature) : null,
22
+ permission: normalizeLevel(row["permission"] ?? row["level"] ?? row["permission_level"]),
23
+ };
24
+ });
25
+ }
26
+ /** Waar of de fout betekent "de rechten-add-on staat niet op deze database". */
27
+ function isMissingRpc(error) {
28
+ const e = (error ?? {});
29
+ const code = e.code ?? "";
30
+ const message = (e.message ?? "").toLowerCase();
31
+ return (code === "42883" ||
32
+ code === "PGRST202" ||
33
+ code === "PGRST302" ||
34
+ e.status === 404 ||
35
+ message.includes("does not exist") ||
36
+ message.includes("could not find the function") ||
37
+ message.includes("schema cache"));
38
+ }
39
+ /**
40
+ * Leest de rechten van de ingelogde gebruiker via RPC `my_module_permissions()`.
41
+ *
42
+ * Ontbreekt de RPC (add-on niet geïnstalleerd) of faalt de call, dan is het
43
+ * resultaat een LEGE lijst zonder zichtbare fout en zonder retry — en dat
44
+ * betekent bij alle consumers default-allow.
45
+ */
46
+ export function useMyModulePermissions() {
47
+ const query = useQuery({
48
+ queryKey: ["my-module-permissions"],
49
+ queryFn: async () => {
50
+ const { data, error } = await supabase.rpc("my_module_permissions");
51
+ if (error) {
52
+ if (isMissingRpc(error))
53
+ return [];
54
+ // Ook andere fouten mogen niet blokkeren: fail-open, geen retry.
55
+ return [];
56
+ }
57
+ return normalizeRows(data);
58
+ },
59
+ // GEEN retry: een ontbrekende RPC mag nooit een retry-storm veroorzaken.
60
+ retry: false,
61
+ refetchOnWindowFocus: false,
62
+ staleTime: 5 * 60 * 1000,
63
+ });
64
+ return {
65
+ ...query,
66
+ permissions: query.data ?? [],
67
+ /** True zolang we niets weten; callers behandelen dit als allow. */
68
+ isUnknown: query.isLoading || query.data === undefined,
69
+ };
70
+ }
71
+ function resolveLevel(rows, moduleId, featureId) {
72
+ const key = navKey(moduleId);
73
+ const forModule = rows.filter((r) => r.moduleId === key);
74
+ // Geen rij voor deze module → default-allow.
75
+ if (forModule.length === 0)
76
+ return "edit";
77
+ if (featureId) {
78
+ const feature = forModule.find((r) => r.featureId === featureId);
79
+ if (feature)
80
+ return feature.permission;
81
+ }
82
+ const moduleLevel = forModule.find((r) => !r.featureId);
83
+ if (moduleLevel)
84
+ return moduleLevel.permission;
85
+ // Alleen feature-rijen bekend: hoogste feature-recht bepaalt moduletoegang.
86
+ if (forModule.some((r) => r.permission === "edit"))
87
+ return "edit";
88
+ if (forModule.some((r) => r.permission === "view"))
89
+ return "view";
90
+ return "none";
91
+ }
92
+ /**
93
+ * Rechtenniveau voor één module (optioneel één feature). Tijdens laden en bij
94
+ * elke fout is het antwoord `edit` (fail-open).
95
+ */
96
+ export function useModulePermission(moduleId, featureId) {
97
+ const { permissions, isUnknown } = useMyModulePermissions();
98
+ const level = isUnknown ? "edit" : resolveLevel(permissions, moduleId, featureId);
99
+ return {
100
+ level,
101
+ canView: level === "view" || level === "edit",
102
+ canEdit: level === "edit",
103
+ isLoading: isUnknown,
104
+ };
105
+ }
106
+ /**
107
+ * ModuleListTransform die zijbalkitems met `none` weglaat.
108
+ *
109
+ * - Tijdens laden / bij fouten: lijst ongewijzigd.
110
+ * - Modules zonder rij in het rechtenschema: default-allow.
111
+ * - Groepen: de kinderen bepalen zichtbaarheid. Blijft er geen kind over, dan
112
+ * valt de groep weg. Is de parent-sleutel geweigerd maar een kind toegestaan,
113
+ * dan blijft de groep staan met alleen de toegestane kinderen — de parent-href
114
+ * van een groep is een uitklapknop, geen bestemming.
115
+ */
116
+ export function useModulePermissionTransform() {
117
+ const { permissions, isUnknown } = useMyModulePermissions();
118
+ return useCallback((modules) => {
119
+ if (isUnknown || permissions.length === 0)
120
+ return modules;
121
+ const result = [];
122
+ for (const mod of modules) {
123
+ const children = mod.nav.children;
124
+ if (children?.length) {
125
+ const allowed = children.filter((child) => resolveLevel(permissions, navKey(child.href)) !== "none");
126
+ if (allowed.length === 0)
127
+ continue;
128
+ result.push({ ...mod, nav: { ...mod.nav, children: allowed } });
129
+ continue;
130
+ }
131
+ if (resolveLevel(permissions, moduleKey(mod)) === "none")
132
+ continue;
133
+ result.push(mod);
134
+ }
135
+ return result;
136
+ }, [permissions, isUnknown]);
137
+ }
@@ -1,4 +1,8 @@
1
1
  export { getProviderToken, listProviderAccounts, revokeProviderAccount } from "./provider-token.js";
2
2
  export type { ProviderAccount } from "./provider-token.js";
3
+ export { exchangeHandoverCode, HandoverExchangeError } from "./provider-token.js";
4
+ export type { HandoverSession } from "./provider-token.js";
3
5
  export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
6
+ export { requireModulePermission } from "./permissions.server.js";
7
+ export type { PermissionLevel } from "./permissions.server.js";
4
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACpG,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAClF,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC"}
@@ -1,2 +1,6 @@
1
1
  export { getProviderToken, listProviderAccounts, revokeProviderAccount } from "./provider-token.js";
2
+ export { exchangeHandoverCode, HandoverExchangeError } from "./provider-token.js";
2
3
  export { CentralAuthConfigError, ProviderNotConnectedError, ProviderUpstreamError, CentralAuthUnavailableError, AmbiguousAccountError, } from "./provider-token.js";
4
+ // Server-only rechtencheck. Uitsluitend via het "./server" subpath, nooit via
5
+ // de hoofdbarrel.
6
+ export { requireModulePermission } from "./permissions.server.js";
@@ -0,0 +1,16 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export type PermissionLevel = "none" | "view" | "edit";
3
+ /**
4
+ * Server-side rechtencheck via RPC `effective_module_permission()`.
5
+ *
6
+ * Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
7
+ * client uit `context.supabase` mee.
8
+ *
9
+ * - Ontbreekt de RPC (rechten-add-on niet geïnstalleerd) → toegestaan
10
+ * (default-allow), zodat bestaande shells blijven werken.
11
+ * - Onvoldoende rechten → gooit een `Response` met status 403.
12
+ *
13
+ * Core raakt géén enkele tabel uit het rechtenschema; alleen deze RPC.
14
+ */
15
+ export declare function requireModulePermission(supabase: SupabaseClient<any, any, any>, moduleId: string, featureId?: string | null, level?: PermissionLevel): Promise<PermissionLevel>;
16
+ //# sourceMappingURL=permissions.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.server.d.ts","sourceRoot":"","sources":["../../src/core/server/permissions.server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAkBvD;;;;;;;;;;;GAWG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAM,GAAG,IAAW,EAC/B,KAAK,GAAE,eAAwB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAuB1B"}
@@ -0,0 +1,41 @@
1
+ const RANK = { none: 0, view: 1, edit: 2 };
2
+ function isMissingRpc(error) {
3
+ const e = (error ?? {});
4
+ const code = e.code ?? "";
5
+ const message = (e.message ?? "").toLowerCase();
6
+ return (code === "42883" ||
7
+ code === "PGRST202" ||
8
+ code === "PGRST302" ||
9
+ e.status === 404 ||
10
+ message.includes("does not exist") ||
11
+ message.includes("could not find the function"));
12
+ }
13
+ /**
14
+ * Server-side rechtencheck via RPC `effective_module_permission()`.
15
+ *
16
+ * Bedoeld om te combineren met `requireSupabaseAuth`: geef de authenticated
17
+ * client uit `context.supabase` mee.
18
+ *
19
+ * - Ontbreekt de RPC (rechten-add-on niet geïnstalleerd) → toegestaan
20
+ * (default-allow), zodat bestaande shells blijven werken.
21
+ * - Onvoldoende rechten → gooit een `Response` met status 403.
22
+ *
23
+ * Core raakt géén enkele tabel uit het rechtenschema; alleen deze RPC.
24
+ */
25
+ export async function requireModulePermission(supabase, moduleId, featureId = null, level = "view") {
26
+ const { data, error } = await supabase.rpc("effective_module_permission", {
27
+ _module_id: moduleId,
28
+ _feature_id: featureId,
29
+ });
30
+ if (error) {
31
+ if (isMissingRpc(error))
32
+ return "edit";
33
+ throw new Response("Rechtencheck mislukt", { status: 500 });
34
+ }
35
+ const raw = typeof data === "string" ? data.toLowerCase() : "";
36
+ const effective = raw === "none" || raw === "view" || raw === "edit" ? raw : "edit";
37
+ if (RANK[effective] < RANK[level]) {
38
+ throw new Response("Forbidden: geen rechten voor deze module", { status: 403 });
39
+ }
40
+ return effective;
41
+ }
@@ -33,4 +33,21 @@ export declare function listProviderAccounts(provider: string, opts?: {
33
33
  export declare function revokeProviderAccount(provider: string, accountKey: string, opts?: {
34
34
  environment?: "staging" | "live";
35
35
  }): Promise<void>;
36
+ export declare class HandoverExchangeError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ export interface HandoverSession {
40
+ accessToken: string;
41
+ refreshToken: string;
42
+ /** Kan ongeldig zijn als central geen expires_at meestuurt; alleen informatief. */
43
+ expiresAt: Date;
44
+ }
45
+ /**
46
+ * POST {CENTRAL_AUTH_URL}/api/session/exchange met body { code }.
47
+ *
48
+ * Fail-closed: elke fout gooit. De code komt NOOIT in een logregel of
49
+ * foutmelding terecht, en het secret verlaat deze module niet.
50
+ * Uitsluitend beschikbaar via het "./server"-subpath.
51
+ */
52
+ export declare function exchangeHandoverCode(code: string): Promise<HandoverSession>;
36
53
  //# sourceMappingURL=provider-token.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-token.d.ts","sourceRoot":"","sources":["../../src/core/server/provider-token.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAOD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AA4LD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CA6BnD;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,eAAe,EAAE,CAAC,CA4C5B;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
1
+ {"version":3,"file":"provider-token.d.ts","sourceRoot":"","sources":["../../src/core/server/provider-token.ts"],"names":[],"mappings":"AAAA,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAOD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,SAAS,GAAG,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAqMD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CA6BnD;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,eAAe,EAAE,CAAC,CA4C5B;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAMD,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,SAAS,EAAE,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAmCjF"}
@@ -49,10 +49,12 @@ function getCentralAuthConfig() {
49
49
  }
50
50
  return { baseUrl, secret };
51
51
  }
52
- // segment is always one of the three literals above, never caller input —
52
+ // segment is always one of the literals above, never caller input —
53
53
  // the URL path must never be built from anything a caller supplies.
54
+ // provider === undefined betekent: provider-less endpoint, geen extra segment.
54
55
  function buildCentralUrl(baseUrl, segment, provider, query) {
55
- const base = `${baseUrl.replace(/\/+$/, "")}/api/${segment}/${encodeURIComponent(provider)}`;
56
+ const path = provider === undefined ? segment : `${segment}/${encodeURIComponent(provider)}`;
57
+ const base = `${baseUrl.replace(/\/+$/, "")}/api/${path}`;
56
58
  if (!query)
57
59
  return base;
58
60
  const qs = new URLSearchParams(query).toString();
@@ -97,9 +99,13 @@ async function mapCentralError(response, provider, environment, allowAmbiguousAc
97
99
  }
98
100
  throw new CentralAuthUnavailableError(`Central auth unavailable (provider=${provider}, environment=${environment}, status=${response.status}).`);
99
101
  }
100
- async function requestCentral(segment, provider, environment, init, allowAmbiguousAccount) {
102
+ async function requestCentral(segment,
103
+ // undefined bij provider-less endpoints; alleen voor de URL relevant.
104
+ provider, environment, init, allowAmbiguousAccount) {
101
105
  const { baseUrl, secret } = getCentralAuthConfig();
102
106
  const url = buildCentralUrl(baseUrl, segment, provider, init.query);
107
+ // Label uitsluitend voor logging en foutmeldingen — nooit voor de URL.
108
+ const providerLabel = provider ?? segment;
103
109
  const controller = new AbortController();
104
110
  let timedOut = false;
105
111
  const timeout = setTimeout(() => {
@@ -119,14 +125,14 @@ async function requestCentral(segment, provider, environment, init, allowAmbiguo
119
125
  });
120
126
  }
121
127
  catch {
122
- logAuthFailure(provider, environment, timedOut ? "timeout" : "network");
123
- throw new CentralAuthUnavailableError(`Central auth request failed (provider=${provider}, environment=${environment}, reason=${timedOut ? "timeout" : "network"}).`);
128
+ logAuthFailure(providerLabel, environment, timedOut ? "timeout" : "network");
129
+ throw new CentralAuthUnavailableError(`Central auth request failed (provider=${providerLabel}, environment=${environment}, reason=${timedOut ? "timeout" : "network"}).`);
124
130
  }
125
131
  finally {
126
132
  clearTimeout(timeout);
127
133
  }
128
134
  if (!response.ok) {
129
- await mapCentralError(response, provider, environment, allowAmbiguousAccount);
135
+ await mapCentralError(response, providerLabel, environment, allowAmbiguousAccount);
130
136
  }
131
137
  return response;
132
138
  }
@@ -231,3 +237,44 @@ export async function revokeProviderAccount(provider, accountKey, opts) {
231
237
  cache.delete(key);
232
238
  }
233
239
  }
240
+ // ---------------------------------------------------------------------------
241
+ // Handover: eenmalige code van central-auth inwisselen voor een Supabase-sessie.
242
+ // ---------------------------------------------------------------------------
243
+ export class HandoverExchangeError extends Error {
244
+ constructor(message) {
245
+ super(message);
246
+ this.name = "HandoverExchangeError";
247
+ }
248
+ }
249
+ /**
250
+ * POST {CENTRAL_AUTH_URL}/api/session/exchange met body { code }.
251
+ *
252
+ * Fail-closed: elke fout gooit. De code komt NOOIT in een logregel of
253
+ * foutmelding terecht, en het secret verlaat deze module niet.
254
+ * Uitsluitend beschikbaar via het "./server"-subpath.
255
+ */
256
+ export async function exchangeHandoverCode(code) {
257
+ if (typeof code !== "string" || code.trim() === "") {
258
+ throw new HandoverExchangeError("Handover code is missing.");
259
+ }
260
+ const response = await requestCentral("session/exchange",
261
+ /* provider */ undefined, "live", { method: "POST", body: { code } },
262
+ /* allowAmbiguousAccount */ false);
263
+ let body;
264
+ try {
265
+ body = (await response.json());
266
+ }
267
+ catch {
268
+ logAuthFailure("session/exchange", "live", response.status);
269
+ throw new CentralAuthUnavailableError(`Central auth returned an unparseable response (endpoint=session/exchange, status=${response.status}).`);
270
+ }
271
+ if (typeof body.access_token !== "string" || typeof body.refresh_token !== "string") {
272
+ logAuthFailure("session/exchange", "live", response.status);
273
+ throw new HandoverExchangeError(`Central auth returned an incomplete session (endpoint=session/exchange, status=${response.status}).`);
274
+ }
275
+ return {
276
+ accessToken: body.access_token,
277
+ refreshToken: body.refresh_token,
278
+ expiresAt: new Date(typeof body.expires_at === "string" ? body.expires_at : Date.now()),
279
+ };
280
+ }
@@ -16,5 +16,28 @@ export interface FlowModule {
16
16
  version: string;
17
17
  nav: NavItem;
18
18
  settingsCards: SettingsCard[];
19
+ /**
20
+ * Optionele expliciete sleutel waarmee deze module extern (module_registry,
21
+ * rechten) wordt geïdentificeerd. Zet deze wanneer de afgeleide sleutel niet
22
+ * gelijk is aan de registry-slug.
23
+ */
24
+ registryKey?: string;
19
25
  }
26
+ /**
27
+ * De externe modulesleutel.
28
+ *
29
+ * Regel (generiek, core kent geen enkele module- of domeinnaam):
30
+ * 1. `registryKey` als die is opgegeven;
31
+ * 2. anders het eerste padsegment van `nav.href`;
32
+ * 3. afsluitende underscores worden gestript.
33
+ *
34
+ * Waarom (3): een afsluitende underscore in een routepad is een TanStack
35
+ * un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment. Zonder
36
+ * strippen levert dezelfde module twee sleutels op (`voorraad` en `voorraad_`)
37
+ * en verdwijnt hij half uit de zijbalk. Strippen is gekozen boven "registryKey
38
+ * verplicht maken", omdat dat laatste bestaande modules stil zou breken.
39
+ */
40
+ export declare function moduleKey(mod: FlowModule): string;
41
+ /** Zelfde afleidingsregel, maar voor een los pad (nav-children, URL-pathname). */
42
+ export declare function navKey(href: string): string;
20
43
  //# sourceMappingURL=module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/core/types/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/core/types/module.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,aAAa,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAGjD;AAED,kFAAkF;AAClF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C"}
@@ -1 +1,24 @@
1
- export {};
1
+ /**
2
+ * De externe modulesleutel.
3
+ *
4
+ * Regel (generiek, core kent geen enkele module- of domeinnaam):
5
+ * 1. `registryKey` als die is opgegeven;
6
+ * 2. anders het eerste padsegment van `nav.href`;
7
+ * 3. afsluitende underscores worden gestript.
8
+ *
9
+ * Waarom (3): een afsluitende underscore in een routepad is een TanStack
10
+ * un-nesting-conventie (`/voorraad_/detail`), geen betekenisvol segment. Zonder
11
+ * strippen levert dezelfde module twee sleutels op (`voorraad` en `voorraad_`)
12
+ * en verdwijnt hij half uit de zijbalk. Strippen is gekozen boven "registryKey
13
+ * verplicht maken", omdat dat laatste bestaande modules stil zou breken.
14
+ */
15
+ export function moduleKey(mod) {
16
+ if (mod.registryKey)
17
+ return mod.registryKey;
18
+ return navKey(mod.nav.href);
19
+ }
20
+ /** Zelfde afleidingsregel, maar voor een los pad (nav-children, URL-pathname). */
21
+ export function navKey(href) {
22
+ const segment = href.split("?")[0].split("#")[0].split("/").filter(Boolean)[0] ?? "";
23
+ return segment.replace(/_+$/, "");
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/core",
3
- "version": "1.0.16",
3
+ "version": "1.0.19",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -16,7 +16,9 @@
16
16
  "default": "./dist/server/index.js"
17
17
  }
18
18
  },
19
- "files": ["dist"],
19
+ "files": [
20
+ "dist"
21
+ ],
20
22
  "scripts": {
21
23
  "dev": "vite dev",
22
24
  "build": "vite build",
@@ -70,6 +72,7 @@
70
72
  "embla-carousel-react": "^8.6.0",
71
73
  "input-otp": "^1.4.2",
72
74
  "lucide-react": "^0.575.0",
75
+ "nitro": "3.0.260603-beta",
73
76
  "react": "^19.2.0",
74
77
  "react-barcode": "^1.6.1",
75
78
  "react-day-picker": "^9.14.0",
@@ -88,7 +91,7 @@
88
91
  },
89
92
  "devDependencies": {
90
93
  "@eslint/js": "^9.32.0",
91
- "@lovable.dev/vite-tanstack-config": "^1.2.0",
94
+ "@lovable.dev/vite-tanstack-config": "2.13.1",
92
95
  "@types/node": "^22.16.5",
93
96
  "@types/react": "^19.2.0",
94
97
  "@types/react-dom": "^19.2.0",