@arcote.tech/platform 0.4.5 → 0.4.7
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/package.json +4 -4
- package/src/arc.ts +18 -1
- package/src/index.ts +5 -0
- package/src/module-loader.ts +57 -15
- package/src/platform-app.tsx +1 -4
- package/src/registry.ts +18 -0
- package/src/types.ts +21 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/platform",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.7",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Przemysław Krasiński [arcote.tech]",
|
|
7
7
|
"description": "Arc Platform — module system, router, layout, theme, i18n, platform app shell",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"type-check": "tsc --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@arcote.tech/arc-ds": "^0.4.
|
|
22
|
-
"@arcote.tech/arc-react": "^0.4.
|
|
21
|
+
"@arcote.tech/arc-ds": "^0.4.7",
|
|
22
|
+
"@arcote.tech/arc-react": "^0.4.7"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@arcote.tech/arc": "^0.4.
|
|
25
|
+
"@arcote.tech/arc": "^0.4.7",
|
|
26
26
|
"@lingui/core": "^5.0.0",
|
|
27
27
|
"@lingui/react": "^5.0.0",
|
|
28
28
|
"framer-motion": "^12.0.0",
|
package/src/arc.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
BuiltModule,
|
|
9
9
|
ContextElementFragment,
|
|
10
10
|
ExtractPages,
|
|
11
|
+
ModuleAccessRule,
|
|
11
12
|
PageFragment,
|
|
12
13
|
PageOptions,
|
|
13
14
|
SlotFragment,
|
|
@@ -148,9 +149,22 @@ class ModuleBuilder<
|
|
|
148
149
|
private _pages: Record<string, string> | undefined;
|
|
149
150
|
private _ctx: { elements: readonly ArcContextElementAny[] } | undefined;
|
|
150
151
|
private _scope: string | undefined;
|
|
152
|
+
private _protectedBy: ModuleAccessRule[] = [];
|
|
151
153
|
|
|
152
154
|
constructor(readonly name: string) {}
|
|
153
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Restrict module loading to users holding the given token.
|
|
158
|
+
* Optional check function for granular access control.
|
|
159
|
+
*/
|
|
160
|
+
protectedBy(
|
|
161
|
+
token: { name: string },
|
|
162
|
+
check?: (tokenInstance: any) => boolean | Promise<boolean>,
|
|
163
|
+
): this {
|
|
164
|
+
this._protectedBy.push({ token, check });
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
154
168
|
/**
|
|
155
169
|
* Register the module's ArcContext and scope name.
|
|
156
170
|
* Context element fragments are auto-injected in build().
|
|
@@ -225,12 +239,14 @@ class ModuleBuilder<
|
|
|
225
239
|
|
|
226
240
|
const mod: Record<string, unknown> = {
|
|
227
241
|
id: moduleId,
|
|
242
|
+
name: this.name,
|
|
228
243
|
fragments: allFragments,
|
|
229
244
|
publicFragments: this._public,
|
|
230
245
|
pages: Object.freeze(this._pages ?? {}),
|
|
231
246
|
};
|
|
232
247
|
if (this._ctx !== undefined) mod.context = this._ctx;
|
|
233
248
|
if (this._scope !== undefined) mod.scope = this._scope;
|
|
249
|
+
if (this._protectedBy.length > 0) mod.access = { rules: this._protectedBy };
|
|
234
250
|
if (domain) Object.assign(mod, domain);
|
|
235
251
|
|
|
236
252
|
registerModule(mod as unknown as ArcModule);
|
|
@@ -285,12 +301,13 @@ export function arc(
|
|
|
285
301
|
// Context registration: factory returned an ArcContext
|
|
286
302
|
if (isArcContext(result)) {
|
|
287
303
|
setContext(result);
|
|
288
|
-
return { id: moduleId, fragments: [], publicFragments: [] };
|
|
304
|
+
return { id: moduleId, name: moduleId, fragments: [], publicFragments: [] };
|
|
289
305
|
}
|
|
290
306
|
|
|
291
307
|
const publicFragments: readonly ArcFragment[] = Array.isArray(result) ? result : [];
|
|
292
308
|
const mod: ArcModule = {
|
|
293
309
|
id: moduleId,
|
|
310
|
+
name: moduleId,
|
|
294
311
|
fragments: allFragments,
|
|
295
312
|
publicFragments,
|
|
296
313
|
};
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,10 @@ export {
|
|
|
8
8
|
clearModules,
|
|
9
9
|
clearRegistry,
|
|
10
10
|
getAllFragments,
|
|
11
|
+
getAllModuleAccess,
|
|
11
12
|
getAllModules,
|
|
12
13
|
getContext,
|
|
14
|
+
getModuleAccess,
|
|
13
15
|
getContextElementFragments,
|
|
14
16
|
getDefaultLayout,
|
|
15
17
|
getModule,
|
|
@@ -106,6 +108,9 @@ export type {
|
|
|
106
108
|
ArcModule,
|
|
107
109
|
BuiltModule,
|
|
108
110
|
ContextElementFragment,
|
|
111
|
+
ModuleAccess,
|
|
112
|
+
ModuleAccessRule,
|
|
113
|
+
ModuleDescriptor,
|
|
109
114
|
ExtractPages,
|
|
110
115
|
FragmentOrdering,
|
|
111
116
|
PageFragment,
|
package/src/module-loader.ts
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import { clearModules, getContext } from "./registry";
|
|
2
|
+
import type { ModuleDescriptor } from "./types";
|
|
2
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
4
|
|
|
4
5
|
export interface ModuleManifest {
|
|
5
|
-
modules:
|
|
6
|
+
modules: ModuleDescriptor[];
|
|
6
7
|
buildTime: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export type ModuleLoaderState = "loading" | "ready" | "error";
|
|
10
11
|
|
|
12
|
+
function moduleUrl(baseUrl: string, mod: ModuleDescriptor, bust?: string): string {
|
|
13
|
+
const base = mod.url ?? `${baseUrl}/modules/${mod.file}`;
|
|
14
|
+
return bust ? `${base}${base.includes("?") ? "&" : "?"}t=${bust}` : base;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read first available arc token from localStorage */
|
|
18
|
+
function getPersistedToken(): string | null {
|
|
19
|
+
if (typeof localStorage === "undefined") return null;
|
|
20
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
21
|
+
const key = localStorage.key(i);
|
|
22
|
+
if (key?.startsWith("arc:token:")) {
|
|
23
|
+
return localStorage.getItem(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
/**
|
|
12
|
-
* Load all modules from manifest. Each module auto-registers via
|
|
30
|
+
* Load all modules from manifest. Each module auto-registers via module().build().
|
|
13
31
|
*/
|
|
14
|
-
export async function loadModules(
|
|
15
|
-
|
|
32
|
+
export async function loadModules(
|
|
33
|
+
baseUrl: string,
|
|
34
|
+
token?: string | null,
|
|
35
|
+
): Promise<ModuleManifest> {
|
|
36
|
+
const resolvedToken = token ?? getPersistedToken();
|
|
37
|
+
const headers: HeadersInit = {};
|
|
38
|
+
if (resolvedToken) headers["Authorization"] = `Bearer ${resolvedToken}`;
|
|
39
|
+
|
|
40
|
+
const res = await fetch(`${baseUrl}/api/modules`, { headers });
|
|
16
41
|
if (!res.ok)
|
|
17
42
|
throw new Error(`Failed to fetch module manifest: ${res.status}`);
|
|
18
43
|
|
|
19
44
|
const manifest: ModuleManifest = await res.json();
|
|
20
45
|
|
|
21
|
-
// Import all modules — each calls arc() which auto-registers
|
|
22
46
|
await Promise.all(
|
|
23
47
|
manifest.modules.map(
|
|
24
|
-
(mod) => import(/* @vite-ignore */
|
|
48
|
+
(mod) => import(/* @vite-ignore */ moduleUrl(baseUrl, mod)),
|
|
25
49
|
),
|
|
26
50
|
);
|
|
27
51
|
|
|
@@ -31,19 +55,26 @@ export async function loadModules(baseUrl: string): Promise<ModuleManifest> {
|
|
|
31
55
|
/**
|
|
32
56
|
* Reload modules — clear registry, re-import with cache bust.
|
|
33
57
|
*/
|
|
34
|
-
export async function reloadModules(
|
|
58
|
+
export async function reloadModules(
|
|
59
|
+
baseUrl: string,
|
|
60
|
+
token?: string | null,
|
|
61
|
+
): Promise<ModuleManifest> {
|
|
35
62
|
clearModules();
|
|
36
63
|
|
|
37
|
-
const
|
|
64
|
+
const resolvedToken = token ?? getPersistedToken();
|
|
65
|
+
const headers: HeadersInit = {};
|
|
66
|
+
if (resolvedToken) headers["Authorization"] = `Bearer ${resolvedToken}`;
|
|
67
|
+
|
|
68
|
+
const res = await fetch(`${baseUrl}/api/modules`, { headers });
|
|
38
69
|
if (!res.ok)
|
|
39
70
|
throw new Error(`Failed to fetch module manifest: ${res.status}`);
|
|
40
71
|
|
|
41
72
|
const manifest: ModuleManifest = await res.json();
|
|
42
|
-
const bust =
|
|
73
|
+
const bust = String(Date.now());
|
|
43
74
|
|
|
44
75
|
await Promise.all(
|
|
45
76
|
manifest.modules.map(
|
|
46
|
-
(mod) => import(/* @vite-ignore */
|
|
77
|
+
(mod) => import(/* @vite-ignore */ moduleUrl(baseUrl, mod, bust)),
|
|
47
78
|
),
|
|
48
79
|
);
|
|
49
80
|
|
|
@@ -52,32 +83,35 @@ export async function reloadModules(baseUrl: string): Promise<ModuleManifest> {
|
|
|
52
83
|
|
|
53
84
|
/**
|
|
54
85
|
* Hook: loads modules on mount, provides state + context.
|
|
86
|
+
* Re-fetches manifest when token changes (login/logout).
|
|
55
87
|
*/
|
|
56
88
|
export function useModuleLoader(
|
|
57
89
|
baseUrl: string,
|
|
58
|
-
options: { skip?: boolean } = {},
|
|
90
|
+
options: { skip?: boolean; token?: string | null } = {},
|
|
59
91
|
) {
|
|
60
92
|
const [state, setState] = useState<ModuleLoaderState>(
|
|
61
93
|
options.skip ? "ready" : "loading",
|
|
62
94
|
);
|
|
63
95
|
const [error, setError] = useState<Error | null>(null);
|
|
64
96
|
const loaded = useRef(false);
|
|
97
|
+
const prevToken = useRef(options.token);
|
|
65
98
|
|
|
66
99
|
const reload = useCallback(async () => {
|
|
67
100
|
try {
|
|
68
|
-
await reloadModules(baseUrl);
|
|
101
|
+
await reloadModules(baseUrl, options.token);
|
|
69
102
|
setState("ready");
|
|
70
103
|
} catch (e) {
|
|
71
104
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
72
105
|
setState("error");
|
|
73
106
|
}
|
|
74
|
-
}, [baseUrl]);
|
|
107
|
+
}, [baseUrl, options.token]);
|
|
75
108
|
|
|
109
|
+
// Initial load
|
|
76
110
|
useEffect(() => {
|
|
77
111
|
if (options.skip || loaded.current) return;
|
|
78
112
|
loaded.current = true;
|
|
79
113
|
|
|
80
|
-
loadModules(baseUrl)
|
|
114
|
+
loadModules(baseUrl, options.token)
|
|
81
115
|
.then(() => setState("ready"))
|
|
82
116
|
.catch((e) => {
|
|
83
117
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -96,7 +130,15 @@ export function useModuleLoader(
|
|
|
96
130
|
};
|
|
97
131
|
|
|
98
132
|
return () => evtSource.close();
|
|
99
|
-
}, [baseUrl, reload, options.skip]);
|
|
133
|
+
}, [baseUrl, reload, options.skip, options.token]);
|
|
134
|
+
|
|
135
|
+
// Reload on token change (login/logout)
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!loaded.current) return;
|
|
138
|
+
if (prevToken.current === options.token) return;
|
|
139
|
+
prevToken.current = options.token;
|
|
140
|
+
reload();
|
|
141
|
+
}, [options.token, reload]);
|
|
100
142
|
|
|
101
143
|
return { state, error, context: getContext(), reload };
|
|
102
144
|
}
|
package/src/platform-app.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ArcContextAny } from "@arcote.tech/arc";
|
|
2
2
|
import type { DSVariantOverrides } from "@arcote.tech/arc-ds";
|
|
3
|
-
import { DesignSystemProvider } from "@arcote.tech/arc-ds";
|
|
3
|
+
import { DesignSystemProvider, TooltipProvider } from "@arcote.tech/arc-ds";
|
|
4
4
|
import { reactModel } from "@arcote.tech/arc-react";
|
|
5
5
|
import { Layout } from "@arcote.tech/arc-ds";
|
|
6
6
|
import { ArcLayoutProvider } from "./layout/layout-provider";
|
|
@@ -13,13 +13,10 @@ import {
|
|
|
13
13
|
setDefaultLayout,
|
|
14
14
|
} from "./registry";
|
|
15
15
|
import { ThemeProvider } from "./theme";
|
|
16
|
-
import { Tooltip as RadixTooltip } from "radix-ui";
|
|
17
16
|
import { useEffect, useMemo, useState } from "react";
|
|
18
17
|
import { useModuleLoader } from "./module-loader";
|
|
19
18
|
import { PlatformModelProvider } from "./platform-context";
|
|
20
19
|
|
|
21
|
-
const TooltipProvider = RadixTooltip.Provider;
|
|
22
|
-
|
|
23
20
|
export interface PlatformAppProps {
|
|
24
21
|
apiUrl?: string;
|
|
25
22
|
wsUrl?: string;
|
package/src/registry.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ArcLayoutComponent,
|
|
4
4
|
ArcModule,
|
|
5
5
|
ContextElementFragment,
|
|
6
|
+
ModuleAccess,
|
|
6
7
|
PageFragment,
|
|
7
8
|
SlotFragment,
|
|
8
9
|
SlotId,
|
|
@@ -186,6 +187,23 @@ export function getWrapperFragments(): WrapperFragment[] {
|
|
|
186
187
|
return sortByOrdering(fragments);
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/** Get access rules for a module by name. */
|
|
191
|
+
export function getModuleAccess(name: string): ModuleAccess | undefined {
|
|
192
|
+
for (const mod of modules.values()) {
|
|
193
|
+
if (mod.name === name && mod.access) return mod.access;
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Get access rules for all protected modules. */
|
|
199
|
+
export function getAllModuleAccess(): Map<string, ModuleAccess> {
|
|
200
|
+
const result = new Map<string, ModuleAccess>();
|
|
201
|
+
for (const mod of modules.values()) {
|
|
202
|
+
if (mod.access) result.set(mod.name, mod.access);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
189
207
|
/** Clear all modules (but keep context). Used for reload. */
|
|
190
208
|
export function clearModules(): void {
|
|
191
209
|
modules.clear();
|
package/src/types.ts
CHANGED
|
@@ -83,14 +83,35 @@ export type ArcFragment =
|
|
|
83
83
|
|
|
84
84
|
export type PublicArcFragment = ArcFragment;
|
|
85
85
|
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Module access control
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
export interface ModuleAccessRule {
|
|
91
|
+
readonly token: { name: string; [key: string]: any };
|
|
92
|
+
readonly check?: (tokenInstance: any) => boolean | Promise<boolean>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ModuleAccess {
|
|
96
|
+
readonly rules: readonly ModuleAccessRule[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModuleDescriptor {
|
|
100
|
+
readonly file: string;
|
|
101
|
+
readonly name: string;
|
|
102
|
+
readonly url?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
// ---------------------------------------------------------------------------
|
|
87
106
|
// Moduł
|
|
88
107
|
// ---------------------------------------------------------------------------
|
|
89
108
|
|
|
90
109
|
export interface ArcModule {
|
|
91
110
|
readonly id: string;
|
|
111
|
+
readonly name: string;
|
|
92
112
|
readonly fragments: readonly ArcFragment[];
|
|
93
113
|
readonly publicFragments: readonly ArcFragment[];
|
|
114
|
+
readonly access?: ModuleAccess;
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
// ---------------------------------------------------------------------------
|