@jskit-ai/kernel 0.1.38 → 0.1.40

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/kernel",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "typebox": "^1.0.81"
@@ -19,6 +19,7 @@
19
19
  "./server/actions": "./server/actions/index.js",
20
20
  "./server/http": "./server/http/index.js",
21
21
  "./server/platform": "./server/platform/index.js",
22
+ "./server/registries": "./server/registries/index.js",
22
23
  "./server/runtime": "./server/runtime/index.js",
23
24
  "./server/runtime/errors": "./server/runtime/errors.js",
24
25
  "./server/runtime/requestUrl": "./server/runtime/requestUrl.js",
@@ -3,6 +3,15 @@ import { ROUTE_VISIBILITY_PUBLIC, ROUTE_VISIBILITY_USER } from "./policies.js";
3
3
 
4
4
  const ROUTE_VISIBILITY_LEVELS = Object.freeze([ROUTE_VISIBILITY_PUBLIC, ROUTE_VISIBILITY_USER]);
5
5
  const ROUTE_VISIBILITY_LEVEL_SET = new Set(ROUTE_VISIBILITY_LEVELS);
6
+ const ROUTE_VISIBILITY_WORKSPACE = "workspace";
7
+ const ROUTE_VISIBILITY_WORKSPACE_USER = "workspace_user";
8
+ const ROUTE_VISIBILITY_TOKENS = Object.freeze([
9
+ ROUTE_VISIBILITY_PUBLIC,
10
+ ROUTE_VISIBILITY_USER,
11
+ ROUTE_VISIBILITY_WORKSPACE,
12
+ ROUTE_VISIBILITY_WORKSPACE_USER
13
+ ]);
14
+ const ROUTE_VISIBILITY_TOKEN_SET = new Set(ROUTE_VISIBILITY_TOKENS);
6
15
 
7
16
  function normalizeRouteVisibilityToken(value, { fallback = ROUTE_VISIBILITY_PUBLIC } = {}) {
8
17
  const normalized = normalizeText(value).toLowerCase();
@@ -34,6 +43,22 @@ function normalizeRouteVisibility(value, { fallback = ROUTE_VISIBILITY_PUBLIC }
34
43
  return ROUTE_VISIBILITY_PUBLIC;
35
44
  }
36
45
 
46
+ function checkRouteVisibility(value, { context = "checkRouteVisibility" } = {}) {
47
+ const normalized = normalizeRouteVisibilityToken(value);
48
+ if (ROUTE_VISIBILITY_TOKEN_SET.has(normalized)) {
49
+ return normalized;
50
+ }
51
+
52
+ throw new TypeError(`${context} must be one of: ${ROUTE_VISIBILITY_TOKENS.join(", ")}.`);
53
+ }
54
+
55
+ function isWorkspaceRouteVisibility(value = "") {
56
+ const normalized = checkRouteVisibility(value, {
57
+ context: "isWorkspaceRouteVisibility visibility"
58
+ });
59
+ return normalized === ROUTE_VISIBILITY_WORKSPACE || normalized === ROUTE_VISIBILITY_WORKSPACE_USER;
60
+ }
61
+
37
62
  function normalizeVisibilityScopeKind(value) {
38
63
  const normalized = normalizeText(value).toLowerCase();
39
64
  return normalized || null;
@@ -53,4 +78,16 @@ function normalizeVisibilityContext(value = {}) {
53
78
  });
54
79
  }
55
80
 
56
- export { ROUTE_VISIBILITY_LEVELS, normalizeRouteVisibilityToken, normalizeRouteVisibility, normalizeVisibilityContext };
81
+ export {
82
+ ROUTE_VISIBILITY_LEVELS,
83
+ ROUTE_VISIBILITY_PUBLIC,
84
+ ROUTE_VISIBILITY_USER,
85
+ ROUTE_VISIBILITY_WORKSPACE,
86
+ ROUTE_VISIBILITY_WORKSPACE_USER,
87
+ ROUTE_VISIBILITY_TOKENS,
88
+ normalizeRouteVisibilityToken,
89
+ normalizeRouteVisibility,
90
+ checkRouteVisibility,
91
+ isWorkspaceRouteVisibility,
92
+ normalizeVisibilityContext
93
+ };
@@ -1,6 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { normalizeRouteVisibilityToken, normalizeRouteVisibility, normalizeVisibilityContext } from "./visibility.js";
3
+ import {
4
+ checkRouteVisibility,
5
+ isWorkspaceRouteVisibility,
6
+ normalizeRouteVisibilityToken,
7
+ normalizeRouteVisibility,
8
+ normalizeVisibilityContext
9
+ } from "./visibility.js";
4
10
 
5
11
  test("normalizeRouteVisibility keeps kernel core visibility contract", () => {
6
12
  assert.equal(normalizeRouteVisibility("PUBLIC"), "public");
@@ -18,6 +24,18 @@ test("normalizeRouteVisibilityToken normalizes visibility tokens for module-leve
18
24
  assert.equal(normalizeRouteVisibilityToken("", { fallback: "workspace" }), "workspace");
19
25
  });
20
26
 
27
+ test("checkRouteVisibility accepts workspace visibility tokens", () => {
28
+ assert.equal(checkRouteVisibility("workspace"), "workspace");
29
+ assert.equal(checkRouteVisibility("workspace_user"), "workspace_user");
30
+ assert.throws(() => checkRouteVisibility("invalid"), /must be one of/);
31
+ });
32
+
33
+ test("isWorkspaceRouteVisibility matches only workspace-scoped tokens", () => {
34
+ assert.equal(isWorkspaceRouteVisibility("workspace"), true);
35
+ assert.equal(isWorkspaceRouteVisibility("workspace_user"), true);
36
+ assert.equal(isWorkspaceRouteVisibility("public"), false);
37
+ });
38
+
21
39
  test("normalizeVisibilityContext normalizes mode and owner identifiers", () => {
22
40
  assert.deepEqual(normalizeVisibilityContext({ visibility: "user", userId: "7" }), {
23
41
  visibility: "user",
@@ -7,6 +7,76 @@ const API_DOCS_PATH = `${API_PREFIX}/docs`;
7
7
  const API_REALTIME_PATH = `${API_PREFIX}/realtime`;
8
8
  const VERSIONED_API_PATH_PATTERN = /^\/api\/v[0-9]+(?:$|\/)/;
9
9
 
10
+ function normalizeRouteTemplateParams(params = null) {
11
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
12
+ return {};
13
+ }
14
+ return params;
15
+ }
16
+
17
+ function materializeRouteTemplate(routeTemplate = "/", { params = {}, strictParams = true, context = "materializeRouteTemplate" } = {}) {
18
+ const normalizedTemplate = normalizePathname(routeTemplate);
19
+ const normalizedParams = normalizeRouteTemplateParams(params);
20
+ const missingParams = new Set();
21
+ const outputPath = String(normalizedTemplate || "/").replace(/:([A-Za-z0-9_]+)/g, (_full, rawName) => {
22
+ const paramName = String(rawName || "").trim();
23
+ const rawValue = normalizedParams[paramName];
24
+ const paramValue = String(Array.isArray(rawValue) ? rawValue[0] : rawValue ?? "").trim();
25
+ if (!paramValue) {
26
+ missingParams.add(paramName);
27
+ return `:${paramName}`;
28
+ }
29
+ return encodeURIComponent(paramValue);
30
+ });
31
+
32
+ if (strictParams && missingParams.size > 0) {
33
+ throw new Error(`${context} missing required route params: ${[...missingParams].sort().join(", ")}.`);
34
+ }
35
+
36
+ return outputPath;
37
+ }
38
+
39
+ function resolveScopedRouteBase(routeBase = "/") {
40
+ const normalizedRouteBase = normalizePathname(routeBase);
41
+ const segments = normalizedRouteBase.split("/").filter(Boolean);
42
+ let lastDynamicIndex = -1;
43
+
44
+ for (let index = 0; index < segments.length; index += 1) {
45
+ const segment = segments[index];
46
+ if (segment.startsWith(":") && segment.length > 1) {
47
+ lastDynamicIndex = index;
48
+ }
49
+ }
50
+
51
+ if (lastDynamicIndex < 0) {
52
+ return "/";
53
+ }
54
+
55
+ return `/${segments.slice(0, lastDynamicIndex + 1).join("/")}`;
56
+ }
57
+
58
+ function resolveScopedApiBasePath({
59
+ routeBase = "/",
60
+ relativePath = "/",
61
+ params = {},
62
+ strictParams = true
63
+ } = {}) {
64
+ const scopedRouteBase = resolveScopedRouteBase(routeBase);
65
+ const materializedScopeBase = materializeRouteTemplate(scopedRouteBase, {
66
+ params,
67
+ strictParams,
68
+ context: "resolveScopedApiBasePath"
69
+ });
70
+ const basePath = materializedScopeBase === "/" ? API_BASE_PATH : `${API_BASE_PATH}${materializedScopeBase}`;
71
+ const normalizedRelativePath = normalizePathname(relativePath || "/");
72
+
73
+ if (normalizedRelativePath === "/") {
74
+ return basePath;
75
+ }
76
+
77
+ return `${basePath}${normalizedRelativePath}`;
78
+ }
79
+
10
80
  function isApiPath(pathname) {
11
81
  return matchesPathPrefix(pathname, API_BASE_PATH);
12
82
  }
@@ -74,11 +144,14 @@ export {
74
144
  API_PREFIX_SLASH,
75
145
  API_DOCS_PATH,
76
146
  API_REALTIME_PATH,
147
+ materializeRouteTemplate,
77
148
  normalizePathname,
78
149
  isApiPath,
79
150
  isVersionedApiPath,
80
151
  toVersionedApiPath,
81
152
  toVersionedApiPrefix,
82
153
  buildVersionedApiPath,
83
- isVersionedApiPrefixMatch
154
+ isVersionedApiPrefixMatch,
155
+ resolveScopedRouteBase,
156
+ resolveScopedApiBasePath
84
157
  };
@@ -0,0 +1,31 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolveScopedApiBasePath, resolveScopedRouteBase } from "./apiPaths.js";
4
+
5
+ test("resolveScopedRouteBase keeps the route prefix through the last dynamic segment", () => {
6
+ assert.equal(resolveScopedRouteBase("/"), "/");
7
+ assert.equal(resolveScopedRouteBase("/home"), "/");
8
+ assert.equal(resolveScopedRouteBase("/w/:workspaceSlug"), "/w/:workspaceSlug");
9
+ assert.equal(resolveScopedRouteBase("/w/:workspaceSlug/admin"), "/w/:workspaceSlug");
10
+ assert.equal(resolveScopedRouteBase("/org/:orgId/team/:teamId/admin"), "/org/:orgId/team/:teamId");
11
+ });
12
+
13
+ test("resolveScopedApiBasePath materializes scoped API paths from route templates", () => {
14
+ assert.equal(resolveScopedApiBasePath({ routeBase: "/home", relativePath: "/settings" }), "/api/settings");
15
+ assert.equal(
16
+ resolveScopedApiBasePath({
17
+ routeBase: "/w/:workspaceSlug/admin",
18
+ relativePath: "/members",
19
+ params: { workspaceSlug: "acme" }
20
+ }),
21
+ "/api/w/acme/members"
22
+ );
23
+ assert.throws(
24
+ () =>
25
+ resolveScopedApiBasePath({
26
+ routeBase: "/w/:workspaceSlug/admin",
27
+ relativePath: "/members"
28
+ }),
29
+ /missing required route params/
30
+ );
31
+ });
@@ -4,3 +4,7 @@ export {
4
4
  deriveSurfaceRouteBaseFromPagesRoot
5
5
  } from "./registry.js";
6
6
  export { createSurfacePathHelpers } from "./paths.js";
7
+ export {
8
+ resolveScopedRouteBase,
9
+ resolveScopedApiBasePath
10
+ } from "./apiPaths.js";