@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.
|
|
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 {
|
|
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 {
|
|
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
|
+
});
|