@valbuild/core 0.136.1 → 0.137.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/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @valbuild/core
2
2
 
3
+ ## 0.137.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#676](https://github.com/valbuild/val/pull/676) [`43b58f2`](https://github.com/valbuild/val/commit/43b58f2962840883badf91950da2c51b89d1c7cb) Thanks [@freekh](https://github.com/freekh)! - External page keys are now validated, and `mailto:` and `tel:` are allowed.
8
+
9
+ `externalPageRouter` documented a rule — a key must start with `https://` or
10
+ `http://` — and never enforced it: the validator collected the errors and then
11
+ returned an empty list, so a key like `discord.gg/val` or `/about` was accepted
12
+ in silence and behaved as a relative link on the site.
13
+
14
+ Fixing that made the rule itself worth looking at, because it was a rule about
15
+ the protocol standing in for a rule about links. A contact page's email address
16
+ and phone number are external pages to whoever maintains the list, and they
17
+ were refused. So the rule that is now enforced is a deny list rather than an
18
+ allow list: a key must have a scheme, and the scheme must not be one of
19
+ `javascript:`, `data:`, `vbscript:`, `file:` or `blob:` — which are not links
20
+ at all. An external page key ends up in an `href` in your site's own markup, so
21
+ those stay refused however the router is configured.
22
+
23
+ A project that wants to be stricter says so:
24
+
25
+ ```ts
26
+ s.record(item).router(externalPageRouter({ schemes: ["https", "mailto"] }));
27
+ ```
28
+
29
+ `initVal()` from `@valbuild/next` and `@valbuild/tanstack` hands you the
30
+ callable form, so the narrowed version typechecks in an app.
31
+
32
+ Val Studio applies the same rule while you type — it calls the same function —
33
+ and shows a narrowed project's own list in the message.
34
+
35
+ If your project has a key with no scheme at all, it will start showing a
36
+ validation error on that entry. Give it one, or move it out of the external
37
+ router.
38
+
39
+ ## 0.136.8
40
+
41
+ ### Patch Changes
42
+
43
+ - [#737](https://github.com/valbuild/val/pull/737) [`35577be`](https://github.com/valbuild/val/commit/35577be50981fec35fc27ee9435a092a26909395) Thanks [@freekh](https://github.com/freekh)! - After a publish, the Studio no longer loses edits or applies a list reorder twice while the new build is rolling out.
44
+
45
+ The Studio shows the content built into the page it loaded, with the unpublished changes on top. Which changes those are comes from the server, and the server lists them relative to its own build. For a while after a publish the two can be different builds: a tab opened before the publish keeps its page, and a managed project's new build reaches each location up to a minute apart. The Studio then applied one build's list of changes to another build's content. Published edits went missing, or were applied a second time, which is invisible for a text replacement but reorders a list again, or removes the wrong item.
46
+
47
+ The server already says which build answered (`sourcesSha`), and the Studio now computes the same fingerprint for the content it holds. When the two differ, it fetches the answering build's content and puts it in place together with that build's list of changes, in one step, so nothing on screen shows a half-updated state. Going back to its own build needs no fetch. When they match, which is almost always, nothing extra is requested.
48
+
3
49
  ## 0.136.1
4
50
 
5
51
  ### Patch Changes
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Which URL schemes an external page key may use.
3
+ *
4
+ * External pages are the URLs a site links OUT to, and a link is not only a web
5
+ * page: `mailto:` on a contact page and `tel:` on a phone number are the same
6
+ * kind of thing to whoever maintains the list, and were refused outright.
7
+ *
8
+ * So the default is a deny list rather than an allow list — anything with a
9
+ * scheme, except the handful that are not links at all. Listing every scheme
10
+ * worth linking to is a list that is wrong the moment someone wants
11
+ * `whatsapp:` or `msteams:`, and being wrong that way means an editor cannot
12
+ * store a URL their site genuinely uses.
13
+ *
14
+ * A project that wants to be stricter says so:
15
+ *
16
+ * ```ts
17
+ * s.router(externalPageRouter({ schemes: ["https", "mailto"] }), item)
18
+ * ```
19
+ */
20
+ /**
21
+ * The schemes that are refused however wide the policy is.
22
+ *
23
+ * Not a style preference. An external page key ends up in an `href` in the
24
+ * consuming site's own markup, so `javascript:` there is stored XSS on that
25
+ * site, and `data:` is the same trick wearing a document. `vbscript:` is the
26
+ * historical spelling of the first. `file:` and `blob:` address the visitor's
27
+ * own machine rather than anywhere on the internet.
28
+ *
29
+ * These stay refused even when a project lists them in `schemes`: a config
30
+ * option is not a good enough reason to let content inject script into the
31
+ * page that renders it.
32
+ */
33
+ export declare const UNSAFE_URL_SCHEMES: readonly ["javascript", "data", "vbscript", "file", "blob"];
34
+ export type ExternalUrlSchemePolicy = {
35
+ /**
36
+ * The only schemes allowed, without the colon: `["https", "mailto"]`.
37
+ *
38
+ * Absent means the wide default — anything but {@link UNSAFE_URL_SCHEMES}.
39
+ */
40
+ schemes?: readonly string[];
41
+ };
42
+ /** The scheme of a URL, lowercased and without the colon, or null. */
43
+ export declare function schemeOf(url: string): string | null;
44
+ export type SchemeRejection = {
45
+ reason: "no-scheme";
46
+ } | {
47
+ reason: "unsafe";
48
+ scheme: string;
49
+ } | {
50
+ reason: "not-allowed";
51
+ scheme: string;
52
+ allowed: readonly string[];
53
+ };
54
+ /** `null` when the URL's scheme is acceptable under `policy`. */
55
+ export declare function rejectScheme(url: string, policy?: ExternalUrlSchemePolicy): SchemeRejection | null;
56
+ /** What to tell whoever wrote the key. */
57
+ export declare function describeSchemeRejection(rejection: SchemeRejection): string;
58
+ /**
59
+ * Whether a link check can open this URL.
60
+ *
61
+ * Separate from whether it is ALLOWED, and the two are not the same question:
62
+ * `mailto:` is a perfectly good external page and there is no request to make
63
+ * of it. A URL that cannot be opened is reported as not checked, never as
64
+ * broken.
65
+ */
66
+ export declare function isCheckableScheme(url: string): boolean;
@@ -70,6 +70,21 @@ export type ValModuleShas = {
70
70
  * exactly. That is the property the promotion relies on: only the modules whose
71
71
  * source actually moved change anything.
72
72
  */
73
+ /**
74
+ * The `sourcesSha` fold on its own: path and source, in `val.modules` order.
75
+ *
76
+ * Separate from {@link computeValModuleShas} because the Studio needs it and
77
+ * has nothing else that fold takes. It is how a client tells whether the base
78
+ * it is showing is the base a server's `/stat` answer is relative to: the chain
79
+ * a server announces is the patches ITS build does not contain, so applying it
80
+ * to another build's source loses edits or applies them twice. Both sides call
81
+ * this one implementation, so the comparison is only ever between two runs of
82
+ * the same code.
83
+ */
84
+ export declare function computeSourcesSha(entries: readonly {
85
+ path: string;
86
+ source: unknown;
87
+ }[]): string;
73
88
  export declare function computeValModuleShas(config: ValModules["config"], entries: readonly ValModuleShaEntry[], moduleErrors: readonly ExtractedModuleError[]): ValModuleShas;
74
89
  /**
75
90
  * Extracts schemas and sources from a ValModules registry and computes the
@@ -1,6 +1,6 @@
1
1
  export { initVal, type ConfigDirectory } from "./initVal.js";
2
2
  export { modules, type ValModules } from "./modules.js";
3
- export { extractValModules, computeValModuleShas, type ExtractedValModules, type ValModuleShaEntry, type ValModuleShas, type ExtractedModuleError, } from "./extractValModules.js";
3
+ export { extractValModules, computeValModuleShas, computeSourcesSha, type ExtractedValModules, type ValModuleShaEntry, type ValModuleShas, type ExtractedModuleError, } from "./extractValModules.js";
4
4
  export type { InitVal, ValConfig, ValConstructor, ContentConstructor, } from "./initVal.js";
5
5
  export { Schema, type SerializedSchema, type SelectorOfSchema } from "./schema/index.js";
6
6
  export { hasRemoteFileSchema, hasMediaSchema, } from "./schema/hasRemoteFileSchema.js";
@@ -79,9 +79,19 @@ export { type SerializedLiteralSchema, LiteralSchema } from "./schema/literal.js
79
79
  export { deserializeSchema } from "./schema/deserialize.js";
80
80
  export { type PreviewItem, type ItemPreviewInput, type RecordPreview, type ArrayPreview, type ReifiedPreview, type PreviewScope, previewScope, } from "./preview.js";
81
81
  export { type InlineRender, type FieldRender, isInlineRender } from "./render.js";
82
- export type { ValRouter, RouteValidationError } from "./router.js";
82
+ export type { ValRouter, ExternalPageRouter, RouteValidationError, } from "./router.js";
83
83
  export { parseNextJsRoutePattern, parseTanStackRoutePattern, tanStackSegmentsOfRoutePath, validateUrlAgainstPattern, } from "./router.js";
84
84
  export { getSourcePathFromRoute, isPageRouter } from "./getSourcePathFromRoute.js";
85
+ /**
86
+ * Which URL schemes an external page key may carry.
87
+ *
88
+ * Exported from the package root rather than only through `Internal` because
89
+ * the Studio has to apply the same rule the router does, while the key is
90
+ * being typed - one implementation, two callers, no second copy to drift.
91
+ */
92
+ export { describeSchemeRejection, isCheckableScheme, rejectScheme, schemeOf, UNSAFE_URL_SCHEMES, } from "./externalUrlSchemes.js";
93
+ export type { ExternalUrlSchemePolicy, SchemeRejection, } from "./externalUrlSchemes.js";
94
+ import { describeSchemeRejection, isCheckableScheme, rejectScheme, schemeOf } from "./externalUrlSchemes.js";
85
95
  export declare const FATAL_ERROR_TYPES: readonly ["no-schema", "no-source", "invalid-id", "no-module", "invalid-patch"];
86
96
  export type FatalErrorType = (typeof FATAL_ERROR_TYPES)[number];
87
97
  export declare const DEFAULT_CONTENT_HOST = "https://content.val.build";
@@ -138,7 +148,12 @@ declare const Internal: {
138
148
  joinModuleFilePathAndModulePath: typeof joinModuleFilePathAndModulePath;
139
149
  nextAppRouter: import("./router.js").ValRouter;
140
150
  tanstackRouter: import("./router.js").ValRouter;
141
- externalPageRouter: import("./router.js").ValRouter;
151
+ externalPageRouter: import("./router.js").ExternalPageRouter;
152
+ describeSchemeRejection: typeof describeSchemeRejection;
153
+ isCheckableScheme: typeof isCheckableScheme;
154
+ rejectScheme: typeof rejectScheme;
155
+ schemeOf: typeof schemeOf;
156
+ UNSAFE_URL_SCHEMES: readonly ["javascript", "data", "vbscript", "file", "blob"];
142
157
  color: {
143
158
  parseColor: typeof parseColor;
144
159
  formatColor: typeof formatColor;
@@ -1,5 +1,31 @@
1
1
  import { ModuleFilePath } from "./val/index.js";
2
- export declare const externalPageRouter: ValRouter;
2
+ import { ExternalUrlSchemePolicy } from "./externalUrlSchemes.js";
3
+ /**
4
+ * The router whose keys are URLs to somewhere else.
5
+ *
6
+ * Used directly for the wide default, or called to narrow it:
7
+ *
8
+ * ```ts
9
+ * s.router(externalPageRouter, item) // anything safe
10
+ * s.router(externalPageRouter({ schemes: ["https"] }), item) // https only
11
+ * ```
12
+ *
13
+ * Its one rule is that a key has a scheme, and that the scheme is not one of
14
+ * the handful that are not links at all - see `externalUrlSchemes.ts`. That
15
+ * rule was WRITTEN here from the start and never enforced: the errors were
16
+ * collected into a list and then `return []` threw them away, so every key was
17
+ * accepted however it was spelled.
18
+ *
19
+ * `expectedPath` is null because there is nothing to suggest: a key that is
20
+ * not a URL could have been meant as any URL, and guessing `https://` in front
21
+ * of it would propose a different site as often as the right one.
22
+ */
23
+ export declare const externalPageRouter: ExternalPageRouter;
24
+ /**
25
+ * Callable AND a router, so `externalPageRouter` keeps working unchanged
26
+ * where nobody needs to configure it - which is almost everywhere.
27
+ */
28
+ export type ExternalPageRouter = ValRouter & ((policy: ExternalUrlSchemePolicy) => ValRouter);
3
29
  export type RouteValidationError = {
4
30
  error: {
5
31
  message: string;
@@ -70,5 +96,14 @@ export declare function tanStackSegmentsOfRoutePath(routePath: string): string[]
70
96
  export declare const tanstackRouter: ValRouter;
71
97
  export interface ValRouter {
72
98
  getRouterId(): string;
99
+ /**
100
+ * Which URL schemes this router's keys may use, where it restricts them.
101
+ *
102
+ * Serialized alongside the router id so the Studio can apply the SAME rule
103
+ * while someone types, rather than accepting a key and reporting it as a
104
+ * validation error afterwards. Absent on routers whose keys are paths rather
105
+ * than URLs, which is every other one.
106
+ */
107
+ getUrlSchemePolicy?(): ExternalUrlSchemePolicy;
73
108
  validate(moduleFilePath: ModuleFilePath, urlPaths: string[]): RouteValidationError[];
74
109
  }
@@ -31,6 +31,15 @@ export type SerializedRecordSchema = {
31
31
  /** Static layout config, carried whole in the serialized schema — see `render.ts`. */
32
32
  render?: FieldRender;
33
33
  router?: string;
34
+ /**
35
+ * The URL schemes this router's keys may use, where it restricts them.
36
+ *
37
+ * Carried so the Studio can apply the router's OWN rule while someone types
38
+ * rather than accepting a key and reporting it afterwards. Absent both when
39
+ * the router has no scheme rule (every path router) and when it allows the
40
+ * wide default, which the Studio knows on its own.
41
+ */
42
+ routerSchemes?: string[];
34
43
  customValidate?: boolean;
35
44
  mediaType?: "files" | "images";
36
45
  accept?: string;
@@ -5630,6 +5630,21 @@ function missingDeclaredKeys(declared, present) {
5630
5630
  });
5631
5631
  }
5632
5632
 
5633
+ /**
5634
+ * A router's scheme restriction, as a spreadable fragment.
5635
+ *
5636
+ * Spread rather than assigned so an unrestricted router adds no key at all:
5637
+ * `routerSchemes: undefined` would serialize as an explicit `undefined` and
5638
+ * show up in every schema diff for no reason.
5639
+ */
5640
+ function routerSchemes(router) {
5641
+ var _router$getUrlSchemeP;
5642
+ var schemes = router === null || router === void 0 || (_router$getUrlSchemeP = router.getUrlSchemePolicy) === null || _router$getUrlSchemeP === void 0 ? void 0 : _router$getUrlSchemeP.call(router).schemes;
5643
+ return schemes === undefined ? {} : {
5644
+ routerSchemes: _toConsumableArray(schemes)
5645
+ };
5646
+ }
5647
+
5633
5648
  /**
5634
5649
  * The source type of a `.jsonValues()` record: every entry value is EITHER a
5635
5650
  * lazily loaded {@link JsonSource} whose resolved content is the (loosened, see
@@ -6446,21 +6461,22 @@ var RecordSchema = /*#__PURE__*/function (_Schema) {
6446
6461
  key: "executeSerialize",
6447
6462
  value: function executeSerialize() {
6448
6463
  var _this$renderInput, _this$keySchema, _this$currentRouter, _this$customValidateF, _this$externalLabel;
6449
- var result = {
6464
+ var result = _objectSpread2(_objectSpread2({
6450
6465
  type: "record",
6451
6466
  render: (_this$renderInput = this.renderInput) !== null && _this$renderInput !== void 0 ? _this$renderInput : undefined,
6452
6467
  item: this.item["executeSerialize"](),
6453
6468
  key: (_this$keySchema = this.keySchema) === null || _this$keySchema === void 0 ? void 0 : _this$keySchema["executeSerialize"](),
6454
6469
  opt: this.opt,
6455
6470
  preview: this.previewInput ? true : undefined,
6456
- router: (_this$currentRouter = this.currentRouter) === null || _this$currentRouter === void 0 ? void 0 : _this$currentRouter.getRouterId(),
6471
+ router: (_this$currentRouter = this.currentRouter) === null || _this$currentRouter === void 0 ? void 0 : _this$currentRouter.getRouterId()
6472
+ }, routerSchemes(this.currentRouter)), {}, {
6457
6473
  customValidate: this.customValidateFunctions && ((_this$customValidateF = this.customValidateFunctions) === null || _this$customValidateF === void 0 ? void 0 : _this$customValidateF.length) > 0,
6458
6474
  jsonValues: this.isJsonValues ? true : undefined,
6459
6475
  external: (_this$externalLabel = this.externalLabel) !== null && _this$externalLabel !== void 0 ? _this$externalLabel : undefined,
6460
6476
  readonly: this.isReadonly,
6461
6477
  hidden: this.isHidden,
6462
6478
  description: this.description
6463
- };
6479
+ });
6464
6480
  if (this.mediaOptions) {
6465
6481
  result.mediaType = this.mediaOptions.type;
6466
6482
  result.accept = this.mediaOptions.accept;
@@ -12609,9 +12625,41 @@ function collectObjectRecursive(item, collector) {
12609
12625
  * exactly. That is the property the promotion relies on: only the modules whose
12610
12626
  * source actually moved change anything.
12611
12627
  */
12628
+ /**
12629
+ * The `sourcesSha` fold on its own: path and source, in `val.modules` order.
12630
+ *
12631
+ * Separate from {@link computeValModuleShas} because the Studio needs it and
12632
+ * has nothing else that fold takes. It is how a client tells whether the base
12633
+ * it is showing is the base a server's `/stat` answer is relative to: the chain
12634
+ * a server announces is the patches ITS build does not contain, so applying it
12635
+ * to another build's source loses edits or applies them twice. Both sides call
12636
+ * this one implementation, so the comparison is only ever between two runs of
12637
+ * the same code.
12638
+ */
12639
+ function computeSourcesSha(entries) {
12640
+ var sourcesSha = "";
12641
+ var _iterator = _createForOfIteratorHelper(entries),
12642
+ _step;
12643
+ try {
12644
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
12645
+ var _step$value = _step.value,
12646
+ path = _step$value.path,
12647
+ source = _step$value.source;
12648
+ sourcesSha = hash(sourcesSha + JSON.stringify({
12649
+ path: path,
12650
+ source: source
12651
+ }));
12652
+ }
12653
+ } catch (err) {
12654
+ _iterator.e(err);
12655
+ } finally {
12656
+ _iterator.f();
12657
+ }
12658
+ return sourcesSha;
12659
+ }
12612
12660
  function computeValModuleShas(config, entries, moduleErrors) {
12613
12661
  var configSha = hash(JSON.stringify(config));
12614
- var sourcesSha = "";
12662
+ var sourcesSha = computeSourcesSha(entries);
12615
12663
  var baseSha = configSha;
12616
12664
  // NOTE: schemaSha is deliberately NOT seeded with configSha. It is compared
12617
12665
  // across bundles (the server extracts from the Node bundle, the editor SPA
@@ -12622,18 +12670,14 @@ function computeValModuleShas(config, entries, moduleErrors) {
12622
12670
  // server-only env vars and therefore `undefined` in the browser. Seeding with
12623
12671
  // them made the two sides disagree on every production load.
12624
12672
  var schemaSha = "";
12625
- var _iterator = _createForOfIteratorHelper(entries),
12626
- _step;
12673
+ var _iterator2 = _createForOfIteratorHelper(entries),
12674
+ _step2;
12627
12675
  try {
12628
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
12629
- var entry = _step.value;
12676
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
12677
+ var entry = _step2.value;
12630
12678
  var path = entry.path,
12631
12679
  source = entry.source,
12632
12680
  serializedSchema = entry.serializedSchema;
12633
- sourcesSha = hash(sourcesSha + JSON.stringify({
12634
- path: path,
12635
- source: source
12636
- }));
12637
12681
  baseSha = hash(baseSha + JSON.stringify({
12638
12682
  path: path,
12639
12683
  schema: serializedSchema,
@@ -12651,9 +12695,9 @@ function computeValModuleShas(config, entries, moduleErrors) {
12651
12695
  }));
12652
12696
  }
12653
12697
  } catch (err) {
12654
- _iterator.e(err);
12698
+ _iterator2.e(err);
12655
12699
  } finally {
12656
- _iterator.f();
12700
+ _iterator2.f();
12657
12701
  }
12658
12702
  return {
12659
12703
  baseSha: baseSha,
@@ -12675,7 +12719,7 @@ function extractValModules(_x) {
12675
12719
  }
12676
12720
  function _extractValModules() {
12677
12721
  _extractValModules = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(valModules) {
12678
- var moduleErrors, addModuleError, sources, schemas, serializedSchemas, shaEntries, moduleIdx, module, at, value, path, schema, source, serializedSchema, pathM, _iterator2, _step2, error, _iterator3, _step3, _error, _t, _t2;
12722
+ var moduleErrors, addModuleError, sources, schemas, serializedSchemas, shaEntries, moduleIdx, module, at, value, path, schema, source, serializedSchema, pathM, _iterator3, _step3, error, _iterator4, _step4, _error, _t, _t2;
12679
12723
  return _regenerator().w(function (_context) {
12680
12724
  while (1) switch (_context.p = _context.n) {
12681
12725
  case 0:
@@ -12822,10 +12866,10 @@ function _extractValModules() {
12822
12866
  // Appended AFTER the loop on purpose: each entry's `moduleErrorsAt` is the
12823
12867
  // error count at the time it was folded in, so the SHAs stay a function of
12824
12868
  // the modules themselves.
12825
- _iterator2 = _createForOfIteratorHelper(resolveSettingsModule(serializedSchemas).errors);
12869
+ _iterator3 = _createForOfIteratorHelper(resolveSettingsModule(serializedSchemas).errors);
12826
12870
  try {
12827
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
12828
- error = _step2.value;
12871
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
12872
+ error = _step3.value;
12829
12873
  moduleErrors.push(error);
12830
12874
  }
12831
12875
  // Same kind of rule, same reason it is here: a view cycle is a property of the
@@ -12833,20 +12877,20 @@ function _extractValModules() {
12833
12877
  // catches it either — a view stores a pointer, not content, so there is no
12834
12878
  // data cycle for a source walk to trip over.
12835
12879
  } catch (err) {
12836
- _iterator2.e(err);
12880
+ _iterator3.e(err);
12837
12881
  } finally {
12838
- _iterator2.f();
12882
+ _iterator3.f();
12839
12883
  }
12840
- _iterator3 = _createForOfIteratorHelper(viewCycleErrors(serializedSchemas));
12884
+ _iterator4 = _createForOfIteratorHelper(viewCycleErrors(serializedSchemas));
12841
12885
  try {
12842
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
12843
- _error = _step3.value;
12886
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
12887
+ _error = _step4.value;
12844
12888
  moduleErrors.push(_error);
12845
12889
  }
12846
12890
  } catch (err) {
12847
- _iterator3.e(err);
12891
+ _iterator4.e(err);
12848
12892
  } finally {
12849
- _iterator3.f();
12893
+ _iterator4.f();
12850
12894
  }
12851
12895
  return _context.a(2, _objectSpread2(_objectSpread2({
12852
12896
  sources: sources,
@@ -13737,35 +13781,165 @@ function isInlineRender(schema) {
13737
13781
  return false;
13738
13782
  }
13739
13783
 
13740
- var externalPageRouter = {
13741
- getRouterId: function getRouterId() {
13742
- return "external-url-router";
13743
- },
13744
- validate: function validate(_moduleFilePath, urlPaths) {
13745
- var errors = [];
13746
- var _iterator = _createForOfIteratorHelper(urlPaths),
13747
- _step;
13748
- try {
13749
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
13750
- var urlPath = _step.value;
13751
- if (!(urlPath.startsWith("https://") || urlPath.startsWith("http://"))) {
13752
- errors.push({
13753
- error: {
13754
- message: "URL path \"".concat(urlPath, "\" does not start with \"https://\" or \"http://\""),
13755
- expectedPath: null,
13756
- urlPath: urlPath
13757
- }
13758
- });
13784
+ /**
13785
+ * Which URL schemes an external page key may use.
13786
+ *
13787
+ * External pages are the URLs a site links OUT to, and a link is not only a web
13788
+ * page: `mailto:` on a contact page and `tel:` on a phone number are the same
13789
+ * kind of thing to whoever maintains the list, and were refused outright.
13790
+ *
13791
+ * So the default is a deny list rather than an allow list — anything with a
13792
+ * scheme, except the handful that are not links at all. Listing every scheme
13793
+ * worth linking to is a list that is wrong the moment someone wants
13794
+ * `whatsapp:` or `msteams:`, and being wrong that way means an editor cannot
13795
+ * store a URL their site genuinely uses.
13796
+ *
13797
+ * A project that wants to be stricter says so:
13798
+ *
13799
+ * ```ts
13800
+ * s.router(externalPageRouter({ schemes: ["https", "mailto"] }), item)
13801
+ * ```
13802
+ */
13803
+
13804
+ /**
13805
+ * The schemes that are refused however wide the policy is.
13806
+ *
13807
+ * Not a style preference. An external page key ends up in an `href` in the
13808
+ * consuming site's own markup, so `javascript:` there is stored XSS on that
13809
+ * site, and `data:` is the same trick wearing a document. `vbscript:` is the
13810
+ * historical spelling of the first. `file:` and `blob:` address the visitor's
13811
+ * own machine rather than anywhere on the internet.
13812
+ *
13813
+ * These stay refused even when a project lists them in `schemes`: a config
13814
+ * option is not a good enough reason to let content inject script into the
13815
+ * page that renders it.
13816
+ */
13817
+ var UNSAFE_URL_SCHEMES = ["javascript", "data", "vbscript", "file", "blob"];
13818
+ /** The scheme of a URL, lowercased and without the colon, or null. */
13819
+ function schemeOf(url) {
13820
+ var match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url.trim());
13821
+ return match === null ? null : match[1].toLowerCase();
13822
+ }
13823
+ /** `null` when the URL's scheme is acceptable under `policy`. */
13824
+ function rejectScheme(url) {
13825
+ var policy = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
13826
+ var scheme = schemeOf(url);
13827
+ if (scheme === null) {
13828
+ return {
13829
+ reason: "no-scheme"
13830
+ };
13831
+ }
13832
+ if (UNSAFE_URL_SCHEMES.includes(scheme)) {
13833
+ return {
13834
+ reason: "unsafe",
13835
+ scheme: scheme
13836
+ };
13837
+ }
13838
+ if (policy.schemes !== undefined) {
13839
+ var allowed = policy.schemes.map(function (s) {
13840
+ return s.toLowerCase();
13841
+ });
13842
+ if (!allowed.includes(scheme)) {
13843
+ return {
13844
+ reason: "not-allowed",
13845
+ scheme: scheme,
13846
+ allowed: policy.schemes
13847
+ };
13848
+ }
13849
+ }
13850
+ return null;
13851
+ }
13852
+
13853
+ /** What to tell whoever wrote the key. */
13854
+ function describeSchemeRejection(rejection) {
13855
+ switch (rejection.reason) {
13856
+ case "no-scheme":
13857
+ return "Must start with a scheme, like https:// or mailto:.";
13858
+ case "unsafe":
13859
+ return "\"".concat(rejection.scheme, ":\" cannot be used in a link: it runs code or reads the visitor's own machine rather than pointing at somewhere on the internet.");
13860
+ case "not-allowed":
13861
+ return "\"".concat(rejection.scheme, ":\" is not one of the schemes this router allows (").concat(rejection.allowed.join(", "), ").");
13862
+ }
13863
+ }
13864
+
13865
+ /**
13866
+ * Whether a link check can open this URL.
13867
+ *
13868
+ * Separate from whether it is ALLOWED, and the two are not the same question:
13869
+ * `mailto:` is a perfectly good external page and there is no request to make
13870
+ * of it. A URL that cannot be opened is reported as not checked, never as
13871
+ * broken.
13872
+ */
13873
+ function isCheckableScheme(url) {
13874
+ var scheme = schemeOf(url);
13875
+ return scheme === "http" || scheme === "https";
13876
+ }
13877
+
13878
+ /**
13879
+ * The router whose keys are URLs to somewhere else.
13880
+ *
13881
+ * Used directly for the wide default, or called to narrow it:
13882
+ *
13883
+ * ```ts
13884
+ * s.router(externalPageRouter, item) // anything safe
13885
+ * s.router(externalPageRouter({ schemes: ["https"] }), item) // https only
13886
+ * ```
13887
+ *
13888
+ * Its one rule is that a key has a scheme, and that the scheme is not one of
13889
+ * the handful that are not links at all - see `externalUrlSchemes.ts`. That
13890
+ * rule was WRITTEN here from the start and never enforced: the errors were
13891
+ * collected into a list and then `return []` threw them away, so every key was
13892
+ * accepted however it was spelled.
13893
+ *
13894
+ * `expectedPath` is null because there is nothing to suggest: a key that is
13895
+ * not a URL could have been meant as any URL, and guessing `https://` in front
13896
+ * of it would propose a different site as often as the right one.
13897
+ */
13898
+ var externalPageRouter = Object.assign(function (policy) {
13899
+ return createExternalPageRouter(policy);
13900
+ }, createExternalPageRouter());
13901
+
13902
+ /**
13903
+ * Callable AND a router, so `externalPageRouter` keeps working unchanged
13904
+ * where nobody needs to configure it - which is almost everywhere.
13905
+ */
13906
+
13907
+ function createExternalPageRouter() {
13908
+ var policy = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
13909
+ return {
13910
+ getRouterId: function getRouterId() {
13911
+ return "external-url-router";
13912
+ },
13913
+ getUrlSchemePolicy: function getUrlSchemePolicy() {
13914
+ return policy;
13915
+ },
13916
+ validate: function validate(_moduleFilePath, urlPaths) {
13917
+ var errors = [];
13918
+ var _iterator = _createForOfIteratorHelper(urlPaths),
13919
+ _step;
13920
+ try {
13921
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
13922
+ var urlPath = _step.value;
13923
+ var rejection = rejectScheme(urlPath, policy);
13924
+ if (rejection !== null) {
13925
+ errors.push({
13926
+ error: {
13927
+ message: "URL \"".concat(urlPath, "\" cannot be used here. ").concat(describeSchemeRejection(rejection)),
13928
+ expectedPath: null,
13929
+ urlPath: urlPath
13930
+ }
13931
+ });
13932
+ }
13759
13933
  }
13934
+ } catch (err) {
13935
+ _iterator.e(err);
13936
+ } finally {
13937
+ _iterator.f();
13760
13938
  }
13761
- } catch (err) {
13762
- _iterator.e(err);
13763
- } finally {
13764
- _iterator.f();
13939
+ return errors;
13765
13940
  }
13766
- return [];
13767
- }
13768
- };
13941
+ };
13942
+ }
13769
13943
  // Helper function to validate a URL path against a route pattern
13770
13944
  function validateUrlAgainstPattern(urlPath, routePattern) {
13771
13945
  // Remove leading slash and split URL path
@@ -14165,7 +14339,7 @@ function isPageRouter(routerId) {
14165
14339
 
14166
14340
  var corePackageJson = {
14167
14341
  name: "@valbuild/core",
14168
- version: "0.136.1",
14342
+ version: "0.137.0",
14169
14343
  "private": false,
14170
14344
  description: "Val - supercharged hard-coded content",
14171
14345
  repository: {
@@ -14279,6 +14453,11 @@ var Internal = {
14279
14453
  nextAppRouter: nextAppRouter,
14280
14454
  tanstackRouter: tanstackRouter,
14281
14455
  externalPageRouter: externalPageRouter,
14456
+ describeSchemeRejection: describeSchemeRejection,
14457
+ isCheckableScheme: isCheckableScheme,
14458
+ rejectScheme: rejectScheme,
14459
+ schemeOf: schemeOf,
14460
+ UNSAFE_URL_SCHEMES: UNSAFE_URL_SCHEMES,
14282
14461
  color: {
14283
14462
  parseColor: parseColor,
14284
14463
  formatColor: formatColor,
@@ -14371,4 +14550,4 @@ function tryJsonParse(str) {
14371
14550
  }
14372
14551
  }
14373
14552
 
14374
- export { DateTimeSchema as $, ArraySchema as A, BooleanSchema as B, resolveSettingsModule as C, DEFAULT_CONTENT_HOST as D, localeTagError as E, FATAL_ERROR_TYPES as F, GenericSelector as G, localeOfValue as H, Internal as I, declaredKeySetOf as J, missingDeclaredKeys as K, LocaleSchema as L, ModuleFilePathSep as M, NumberSchema as N, ObjectSchema as O, PatchError as P, discriminatedUnionBranchOf as Q, RecordSchema as R, Schema as S, THEME_LOGO_DIRECTORY as T, ColorSchema as U, VAL_EXTENSION as V, CodeSchema as W, CODE_LANGUAGES as X, COLOR_FORMATS as Y, DEFAULT_COLOR_FORMAT as Z, _typeof as _, _slicedToArray as a, KeyOfSchema as a0, ValViewSchema as a1, isValViewSource as a2, RouteSchema as a3, RichTextSchema as a4, DiscriminatedUnionSchema as a5, EnumSchema as a6, LiteralSchema as a7, deserializeSchema as a8, previewScope as a9, isInlineRender as aa, parseNextJsRoutePattern as ab, parseTanStackRoutePattern as ac, tanStackSegmentsOfRoutePath as ad, validateUrlAgainstPattern as ae, getSourcePathFromRoute as af, isPageRouter as ag, _createClass as b, _classCallCheck as c, _toConsumableArray as d, DEFAULT_APP_HOST as e, DEFAULT_STATIC_HOST as f, extractValModules as g, computeValModuleShas as h, initVal as i, hasRemoteFileSchema as j, hasMediaSchema as k, DEFAULT_VAL_REMOTE_HOST as l, modules as m, derefPatch as n, StringSchema as o, ImageSchema as p, FileSchema as q, DateSchema as r, splitModuleFilePathAndModulePath as s, SettingsSchema as t, ASSISTANT_SETTINGS_MAX_LENGTH as u, THEME_RADIUS_STEPS as v, THEME_RADIUS_LENGTHS as w, assistantAvailability as x, SETTINGS_MODULE_CONVENTION as y, isRootModuleFilePath as z };
14553
+ export { DEFAULT_COLOR_FORMAT as $, ArraySchema as A, BooleanSchema as B, isRootModuleFilePath as C, DEFAULT_CONTENT_HOST as D, resolveSettingsModule as E, FATAL_ERROR_TYPES as F, GenericSelector as G, localeTagError as H, Internal as I, localeOfValue as J, declaredKeySetOf as K, LocaleSchema as L, ModuleFilePathSep as M, NumberSchema as N, ObjectSchema as O, PatchError as P, missingDeclaredKeys as Q, RecordSchema as R, Schema as S, THEME_LOGO_DIRECTORY as T, discriminatedUnionBranchOf as U, VAL_EXTENSION as V, ColorSchema as W, CodeSchema as X, CODE_LANGUAGES as Y, COLOR_FORMATS as Z, _typeof as _, _slicedToArray as a, DateTimeSchema as a0, KeyOfSchema as a1, ValViewSchema as a2, isValViewSource as a3, RouteSchema as a4, RichTextSchema as a5, DiscriminatedUnionSchema as a6, EnumSchema as a7, LiteralSchema as a8, deserializeSchema as a9, previewScope as aa, isInlineRender as ab, parseNextJsRoutePattern as ac, parseTanStackRoutePattern as ad, tanStackSegmentsOfRoutePath as ae, validateUrlAgainstPattern as af, getSourcePathFromRoute as ag, isPageRouter as ah, describeSchemeRejection as ai, isCheckableScheme as aj, rejectScheme as ak, schemeOf as al, UNSAFE_URL_SCHEMES as am, _createClass as b, _classCallCheck as c, _toConsumableArray as d, DEFAULT_APP_HOST as e, DEFAULT_STATIC_HOST as f, extractValModules as g, computeValModuleShas as h, initVal as i, computeSourcesSha as j, hasRemoteFileSchema as k, hasMediaSchema as l, modules as m, DEFAULT_VAL_REMOTE_HOST as n, derefPatch as o, StringSchema as p, ImageSchema as q, FileSchema as r, splitModuleFilePathAndModulePath as s, DateSchema as t, SettingsSchema as u, ASSISTANT_SETTINGS_MAX_LENGTH as v, THEME_RADIUS_STEPS as w, THEME_RADIUS_LENGTHS as x, assistantAvailability as y, SETTINGS_MODULE_CONVENTION as z };