@arcote.tech/platform 0.4.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/platform",
3
3
  "type": "module",
4
- "version": "0.4.6",
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.6",
22
- "@arcote.tech/arc-react": "^0.4.6"
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.6",
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,
@@ -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: string[];
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 arc().
30
+ * Load all modules from manifest. Each module auto-registers via module().build().
13
31
  */
14
- export async function loadModules(baseUrl: string): Promise<ModuleManifest> {
15
- const res = await fetch(`${baseUrl}/api/modules`);
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 */ `${baseUrl}/modules/${mod}`),
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(baseUrl: string): Promise<ModuleManifest> {
58
+ export async function reloadModules(
59
+ baseUrl: string,
60
+ token?: string | null,
61
+ ): Promise<ModuleManifest> {
35
62
  clearModules();
36
63
 
37
- const res = await fetch(`${baseUrl}/api/modules`);
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 = `?t=${Date.now()}`;
73
+ const bust = String(Date.now());
43
74
 
44
75
  await Promise.all(
45
76
  manifest.modules.map(
46
- (mod) => import(/* @vite-ignore */ `${baseUrl}/modules/${mod}${bust}`),
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
  }
@@ -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
  // ---------------------------------------------------------------------------