@cosmicdrift/kumiko-headless 0.165.0 → 2.0.0
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 +2 -2
- package/src/dispatcher/index.ts +1 -0
- package/src/dispatcher/types.ts +12 -0
- package/src/format/escape.ts +55 -0
- package/src/format/index.ts +1 -1
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "2.0.0",
|
|
40
40
|
"temporal-polyfill": "^0.3.2",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
package/src/dispatcher/index.ts
CHANGED
package/src/dispatcher/types.ts
CHANGED
|
@@ -153,6 +153,18 @@ export type StreamOpts = {
|
|
|
153
153
|
readonly signal?: AbortSignal;
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
+
// SSE frame event names for POST /api/stream — shared across the server route,
|
|
157
|
+
// the live-dispatcher wire parser, and their tests so a typo on one side
|
|
158
|
+
// fails the type-checker instead of the integration test.
|
|
159
|
+
export const StreamFrame = {
|
|
160
|
+
chunk: "chunk",
|
|
161
|
+
ping: "ping",
|
|
162
|
+
done: "done",
|
|
163
|
+
error: "error",
|
|
164
|
+
} as const;
|
|
165
|
+
|
|
166
|
+
export type StreamFrameEvent = (typeof StreamFrame)[keyof typeof StreamFrame];
|
|
167
|
+
|
|
156
168
|
// ---------------------------------------------------------------------------
|
|
157
169
|
// The contract
|
|
158
170
|
// ---------------------------------------------------------------------------
|
package/src/format/escape.ts
CHANGED
|
@@ -22,3 +22,58 @@ export function escapeXml(s: string): string {
|
|
|
22
22
|
.replace(/"/g, """)
|
|
23
23
|
.replace(/'/g, "'");
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
// Strips ASCII control chars + space (codepoints 0x00-0x20 and 0x7f) — the
|
|
27
|
+
// same characters browsers strip before scheme detection. `.trim()` alone
|
|
28
|
+
// only removes leading/trailing whitespace, so a scheme like
|
|
29
|
+
// "java\tscript:" (tab embedded mid-scheme) survives to isSafeHref's regex
|
|
30
|
+
// check below, breaks its character class before the ":", falls through to
|
|
31
|
+
// `return true`, and the browser then normalizes it back to "javascript:"
|
|
32
|
+
// and executes it. Written as a char-code filter rather than a regex range
|
|
33
|
+
// to avoid embedding literal control characters in source.
|
|
34
|
+
export function stripControlChars(value: string): string {
|
|
35
|
+
let result = "";
|
|
36
|
+
for (const ch of value) {
|
|
37
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
38
|
+
if (code > 0x20 && code !== 0x7f) result += ch;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// http(s)/mailto or scheme-less (relative/anchor) allowed; javascript:,
|
|
44
|
+
// data:, vbscript:, etc. rejected. Shared between renderer-web's Link
|
|
45
|
+
// primitive and page-render's server-side markdown renderer (both take
|
|
46
|
+
// untrusted tenant-authored hrefs) — no sanitizer dependency needed for a
|
|
47
|
+
// four-line regex check.
|
|
48
|
+
// Browsers decode HTML character references (:, :, :, 	,
|
|
49
|
+
// 	, ...) while parsing the href attribute, before the URL parser ever
|
|
50
|
+
// sees the value — so "javascript:alert(1)" and "java	script:"
|
|
51
|
+
// both normalize to an executable javascript: URL at click time even though
|
|
52
|
+
// neither contains a literal ":" for the regex below to catch. Decode
|
|
53
|
+
// (not delete) so the scheme check sees what the browser will execute —
|
|
54
|
+
// deleting ":" would leave no colon at all, which reads as a safe
|
|
55
|
+
// scheme-less href, exactly backwards.
|
|
56
|
+
const NAMED_HTML_ENTITY_CHARS: Record<string, string> = {
|
|
57
|
+
colon: ":",
|
|
58
|
+
tab: "\t",
|
|
59
|
+
newline: "\n",
|
|
60
|
+
semi: ";",
|
|
61
|
+
amp: "&",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function decodeHtmlEntities(value: string): string {
|
|
65
|
+
return value.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z][a-z0-9]*);/gi, (match, entity: string) => {
|
|
66
|
+
if (entity[0] === "#") {
|
|
67
|
+
const isHex = entity[1]?.toLowerCase() === "x";
|
|
68
|
+
const code = Number.parseInt(isHex ? entity.slice(2) : entity.slice(1), isHex ? 16 : 10);
|
|
69
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : match;
|
|
70
|
+
}
|
|
71
|
+
return NAMED_HTML_ENTITY_CHARS[entity.toLowerCase()] ?? match;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isSafeHref(href: string): boolean {
|
|
76
|
+
const trimmed = stripControlChars(decodeHtmlEntities(href)).toLowerCase();
|
|
77
|
+
if (!/^[a-z][a-z0-9+.-]*:/.test(trimmed)) return true;
|
|
78
|
+
return /^(?:https?|mailto):/.test(trimmed);
|
|
79
|
+
}
|
package/src/format/index.ts
CHANGED
|
@@ -67,7 +67,7 @@ function formatDateCell(
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export { escapeHtml, escapeHtmlAttr, escapeXml } from "./escape";
|
|
70
|
+
export { escapeHtml, escapeHtmlAttr, escapeXml, isSafeHref, stripControlChars } from "./escape";
|
|
71
71
|
export { type HtmlValue, html, RawHtml, raw } from "./html-template";
|
|
72
72
|
export function applyFormatSpec(
|
|
73
73
|
spec: { format: string } & Record<string, unknown>,
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export type {
|
|
|
38
38
|
WriteOpts,
|
|
39
39
|
WriteResult,
|
|
40
40
|
} from "./dispatcher";
|
|
41
|
+
export { StreamFrame, type StreamFrameEvent } from "./dispatcher";
|
|
41
42
|
export type {
|
|
42
43
|
FieldConditionPredicate,
|
|
43
44
|
FieldConditions,
|
|
@@ -59,8 +60,10 @@ export {
|
|
|
59
60
|
escapeXml,
|
|
60
61
|
type HtmlValue,
|
|
61
62
|
html,
|
|
63
|
+
isSafeHref,
|
|
62
64
|
RawHtml,
|
|
63
65
|
raw,
|
|
66
|
+
stripControlChars,
|
|
64
67
|
toInstant,
|
|
65
68
|
} from "./format";
|
|
66
69
|
export type {
|