@vit-foundation/ui 0.17.0 → 0.19.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/dist/components/timeline/TimelineMilestone.svelte +8 -1
- package/dist/components/ui/Link.svelte +13 -1
- package/dist/components/ui/Link.svelte.d.ts +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -2
- package/dist/utils/paths.d.ts +24 -0
- package/dist/utils/paths.js +28 -0
- package/package.json +3 -1
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import EditPanel from '../../edit/chrome/EditPanel.svelte';
|
|
31
31
|
import Editable from '../../edit/Editable.svelte';
|
|
32
32
|
import { MILESTONE_CATEGORY_COLOR, milestoneCategoryLabel } from '../../utils/milestones.js';
|
|
33
|
+
import { isExternalUrl } from '../../utils/paths.js';
|
|
33
34
|
import CardMedia from '../ui/CardMedia.svelte';
|
|
34
35
|
import DraftBadge from '../../edit/chrome/DraftBadge.svelte';
|
|
35
36
|
import DateText from '../ui/DateText.svelte';
|
|
@@ -96,7 +97,13 @@
|
|
|
96
97
|
* predicate — they never route the value through Link, so nothing splices a
|
|
97
98
|
* locale into it.
|
|
98
99
|
*/
|
|
99
|
-
|
|
100
|
+
// An ALLOW-LIST, not "does it look absolute". The predicate here used to be
|
|
101
|
+
// /^(?:[a-z][a-z0-9+.-]*:)?\/\//i — it asks for '//', so any scheme
|
|
102
|
+
// without one ('javascript:alert(1)') read as INTERNAL and went to Link,
|
|
103
|
+
// which handed it to the locale resolver and got it back unchanged. A
|
|
104
|
+
// destination that is neither a path nor an http(s) URL now renders no
|
|
105
|
+
// link at all rather than choosing between two wrong branches.
|
|
106
|
+
const isExternal = $derived(isExternalUrl(milestone.linkUrl ?? ''));
|
|
100
107
|
</script>
|
|
101
108
|
|
|
102
109
|
<article style={`--milestone-color: ${color}`}>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
|
4
4
|
import type { Locale } from '../../config/types.js';
|
|
5
5
|
import { getUiConfig } from '../../config/context.js';
|
|
6
|
+
import { isInternalPath } from '../../utils/paths.js';
|
|
6
7
|
|
|
7
8
|
interface Props extends HTMLAnchorAttributes {
|
|
8
9
|
/**
|
|
@@ -13,6 +14,13 @@
|
|
|
13
14
|
* path can come back mangled, and no `rel` is emitted. A content column that may hold
|
|
14
15
|
* either (a milestone's `link_url`) branches at the call site and
|
|
15
16
|
* renders a bare anchor for the external case.
|
|
17
|
+
*
|
|
18
|
+
* This is ENFORCED, not assumed. The sentence above was here while the
|
|
19
|
+
* component handed anything at all to `config.href` — and a resolver
|
|
20
|
+
* that localizes paths returns 'javascript:alert(1)' unchanged, so an
|
|
21
|
+
* editor-written destination reached an <a href> intact. Anything that
|
|
22
|
+
* is not a root-relative path renders as plain text now: dropping a
|
|
23
|
+
* link is a visible bug someone reports, emitting a hostile one is not.
|
|
16
24
|
*/
|
|
17
25
|
href: string;
|
|
18
26
|
/** Target locale; defaults to the current request/page locale. */
|
|
@@ -24,7 +32,11 @@
|
|
|
24
32
|
|
|
25
33
|
const config = getUiConfig();
|
|
26
34
|
|
|
27
|
-
const
|
|
35
|
+
const internal = $derived(isInternalPath(href));
|
|
36
|
+
const resolvedHref = $derived(internal ? config.href(href, { locale }) : undefined);
|
|
28
37
|
</script>
|
|
29
38
|
|
|
39
|
+
<!-- No href rather than no element: an anchor without one is not a link and
|
|
40
|
+
navigates nowhere, while the surrounding CSS (which selects `a`) keeps
|
|
41
|
+
applying, so a refused destination costs the layout nothing. -->
|
|
30
42
|
<a href={resolvedHref} {...rest}>{@render children()}</a>
|
|
@@ -10,6 +10,13 @@ interface Props extends HTMLAnchorAttributes {
|
|
|
10
10
|
* path can come back mangled, and no `rel` is emitted. A content column that may hold
|
|
11
11
|
* either (a milestone's `link_url`) branches at the call site and
|
|
12
12
|
* renders a bare anchor for the external case.
|
|
13
|
+
*
|
|
14
|
+
* This is ENFORCED, not assumed. The sentence above was here while the
|
|
15
|
+
* component handed anything at all to `config.href` — and a resolver
|
|
16
|
+
* that localizes paths returns 'javascript:alert(1)' unchanged, so an
|
|
17
|
+
* editor-written destination reached an <a href> intact. Anything that
|
|
18
|
+
* is not a root-relative path renders as plain text now: dropping a
|
|
19
|
+
* link is a visible bug someone reports, emitting a hostile one is not.
|
|
13
20
|
*/
|
|
14
21
|
href: string;
|
|
15
22
|
/** Target locale; defaults to the current request/page locale. */
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -12,5 +12,13 @@ export * from './forms/index.js';
|
|
|
12
12
|
// App wiring: configuration context and the edit-mode contract.
|
|
13
13
|
export * from './config/index.js';
|
|
14
14
|
export * from './edit/index.js';
|
|
15
|
-
//
|
|
16
|
-
|
|
15
|
+
// The one path utility a host actually reads: fndvit-website re-exports
|
|
16
|
+
// `buildQueryString` from its own `utils/nav.ts`, so the URL a list mirrors
|
|
17
|
+
// and the hrefs it renders are spelled by this function in both codebases.
|
|
18
|
+
// The other three are NOT exported. `isInternalPath` and `isExternalUrl` are
|
|
19
|
+
// the destination classifiers `Link` and `TimelineMilestone` branch on, and
|
|
20
|
+
// `isPathUnder` is the prefix match `Nav` and `Sidebar` highlight the current
|
|
21
|
+
// section with — all four in-package readers reach them by deep import, and
|
|
22
|
+
// no host imports any of the three. Exporting them would pin their behaviour
|
|
23
|
+
// into semver for no reader; `paths.test.ts` is what holds them instead.
|
|
24
|
+
export { buildQueryString } from './utils/paths.js';
|
package/dist/utils/paths.d.ts
CHANGED
|
@@ -6,3 +6,27 @@ export declare function isPathUnder(pathname: string, prefix: string): boolean;
|
|
|
6
6
|
* mirrors and the hrefs it renders are spelled by the same function.
|
|
7
7
|
*/
|
|
8
8
|
export declare function buildQueryString(params: Record<string, string | null | undefined>): string;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a destination is a ROOT-RELATIVE path — the only shape `Link` will
|
|
11
|
+
* resolve, and what "internal" means everywhere in this package.
|
|
12
|
+
*
|
|
13
|
+
* One leading slash, and the second character must not be another slash:
|
|
14
|
+
* '//evil.example' is protocol-relative and leaves the site. A bare scheme is
|
|
15
|
+
* not a path either, so 'javascript:alert(1)' fails here — which is the point.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isInternalPath(href: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a destination is an EXTERNAL URL this package is willing to emit.
|
|
20
|
+
*
|
|
21
|
+
* An allow-list, not a deny-list, and it is the reason this function exists
|
|
22
|
+
* rather than a `!isInternalPath` at each call site: the classifier it
|
|
23
|
+
* replaced accepted ANY scheme followed by '//', so a scheme WITHOUT one —
|
|
24
|
+
* 'javascript:alert(1)' — fell through to the internal branch and reached the
|
|
25
|
+
* locale resolver, which returns it verbatim into an <a href>.
|
|
26
|
+
*
|
|
27
|
+
* Protocol-relative ('//host/path') stays external on purpose: it is another
|
|
28
|
+
* origin, so localizing it would splice a locale segment into someone else's
|
|
29
|
+
* URL, and it carries the page's own scheme rather than a new one. What is
|
|
30
|
+
* excluded is every scheme that is not http(s) — which is where the harm was.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isExternalUrl(href: string): boolean;
|
package/dist/utils/paths.js
CHANGED
|
@@ -16,3 +16,31 @@ export function buildQueryString(params) {
|
|
|
16
16
|
const encoded = search.toString();
|
|
17
17
|
return encoded ? `?${encoded}` : '';
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a destination is a ROOT-RELATIVE path — the only shape `Link` will
|
|
21
|
+
* resolve, and what "internal" means everywhere in this package.
|
|
22
|
+
*
|
|
23
|
+
* One leading slash, and the second character must not be another slash:
|
|
24
|
+
* '//evil.example' is protocol-relative and leaves the site. A bare scheme is
|
|
25
|
+
* not a path either, so 'javascript:alert(1)' fails here — which is the point.
|
|
26
|
+
*/
|
|
27
|
+
export function isInternalPath(href) {
|
|
28
|
+
return /^\/([^/]|$)/.test(href);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether a destination is an EXTERNAL URL this package is willing to emit.
|
|
32
|
+
*
|
|
33
|
+
* An allow-list, not a deny-list, and it is the reason this function exists
|
|
34
|
+
* rather than a `!isInternalPath` at each call site: the classifier it
|
|
35
|
+
* replaced accepted ANY scheme followed by '//', so a scheme WITHOUT one —
|
|
36
|
+
* 'javascript:alert(1)' — fell through to the internal branch and reached the
|
|
37
|
+
* locale resolver, which returns it verbatim into an <a href>.
|
|
38
|
+
*
|
|
39
|
+
* Protocol-relative ('//host/path') stays external on purpose: it is another
|
|
40
|
+
* origin, so localizing it would splice a locale segment into someone else's
|
|
41
|
+
* URL, and it carries the page's own scheme rather than a new one. What is
|
|
42
|
+
* excluded is every scheme that is not http(s) — which is where the harm was.
|
|
43
|
+
*/
|
|
44
|
+
export function isExternalUrl(href) {
|
|
45
|
+
return /^(?:https?:)?\/\/[^/]/i.test(href);
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vit-foundation/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
7
7
|
"preview": "vite preview",
|
|
8
8
|
"prepare": "svelte-kit sync || echo ''",
|
|
9
9
|
"prepack": "svelte-kit sync && svelte-package && publint",
|
|
10
|
+
"_comment:prepublishOnly": "Publishing is MANUAL — the release workflow that fired on every v* tag had no NPM_TOKEN, failed every time, and was deleted, so this is the ONLY publish gate. npm runs it before prepack.",
|
|
11
|
+
"prepublishOnly": "npm run lint && npm run check && npm run test:unit -- --run",
|
|
10
12
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
11
13
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
12
14
|
"format": "prettier --write .",
|