@silverbulletmd/silverbullet 2.4.2 → 2.6.1

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.
Files changed (97) hide show
  1. package/README.md +19 -4
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +5 -4
  5. package/client/plugos/hooks/code_widget.ts +3 -8
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -15
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -8
  11. package/client/plugos/hooks/slash_command.ts +13 -28
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -6
  15. package/client/plugos/plug_compile.ts +79 -78
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
  19. package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
  20. package/client/plugos/syscalls/asset.ts +1 -3
  21. package/client/plugos/syscalls/code_widget.ts +1 -3
  22. package/client/plugos/syscalls/config.ts +1 -5
  23. package/client/plugos/syscalls/datastore.ts +1 -1
  24. package/client/plugos/syscalls/editor.ts +72 -69
  25. package/client/plugos/syscalls/event.ts +9 -12
  26. package/client/plugos/syscalls/fetch.ts +31 -23
  27. package/client/plugos/syscalls/index.ts +10 -1
  28. package/client/plugos/syscalls/jsonschema.ts +72 -32
  29. package/client/plugos/syscalls/language.ts +9 -5
  30. package/client/plugos/syscalls/markdown.ts +29 -7
  31. package/client/plugos/syscalls/mq.ts +4 -12
  32. package/client/plugos/syscalls/service_registry.ts +1 -4
  33. package/client/plugos/syscalls/shell.ts +2 -5
  34. package/client/plugos/syscalls/space.ts +1 -1
  35. package/client/plugos/syscalls/sync.ts +69 -60
  36. package/client/plugos/syscalls/system.ts +2 -3
  37. package/client/plugos/system.ts +6 -12
  38. package/client/plugos/worker_runtime.ts +12 -33
  39. package/client/space_lua/aggregates.ts +782 -0
  40. package/client/space_lua/ast.ts +42 -8
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +886 -575
  43. package/client/space_lua/labels.ts +7 -12
  44. package/client/space_lua/liq_null.ts +6 -0
  45. package/client/space_lua/numeric.ts +5 -8
  46. package/client/space_lua/parse.ts +346 -120
  47. package/client/space_lua/query_collection.ts +926 -82
  48. package/client/space_lua/query_env.ts +26 -0
  49. package/client/space_lua/render_lua_markdown.ts +369 -0
  50. package/client/space_lua/rp.ts +5 -4
  51. package/client/space_lua/runtime.ts +288 -155
  52. package/client/space_lua/stdlib/format.ts +53 -39
  53. package/client/space_lua/stdlib/js.ts +3 -7
  54. package/client/space_lua/stdlib/load.ts +1 -3
  55. package/client/space_lua/stdlib/math.ts +84 -58
  56. package/client/space_lua/stdlib/net.ts +27 -17
  57. package/client/space_lua/stdlib/os.ts +81 -85
  58. package/client/space_lua/stdlib/pattern.ts +695 -0
  59. package/client/space_lua/stdlib/prng.ts +148 -0
  60. package/client/space_lua/stdlib/space_lua.ts +17 -23
  61. package/client/space_lua/stdlib/string.ts +102 -190
  62. package/client/space_lua/stdlib/string_pack.ts +490 -0
  63. package/client/space_lua/stdlib/table.ts +76 -16
  64. package/client/space_lua/stdlib.ts +53 -39
  65. package/client/space_lua/tonumber.ts +82 -42
  66. package/client/space_lua/util.ts +53 -15
  67. package/dist/plug-compile.js +55 -98
  68. package/package.json +27 -20
  69. package/plug-api/constants.ts +0 -32
  70. package/plug-api/lib/async.ts +20 -7
  71. package/plug-api/lib/crypto.ts +16 -17
  72. package/plug-api/lib/dates.ts +15 -7
  73. package/plug-api/lib/json.ts +11 -5
  74. package/plug-api/lib/limited_map.ts +1 -1
  75. package/plug-api/lib/native_fetch.ts +2 -0
  76. package/plug-api/lib/ref.ts +23 -23
  77. package/plug-api/lib/resolve.ts +7 -11
  78. package/plug-api/lib/tags.ts +13 -4
  79. package/plug-api/lib/transclusion.ts +10 -21
  80. package/plug-api/lib/tree.ts +165 -45
  81. package/plug-api/lib/yaml.ts +35 -25
  82. package/plug-api/syscalls/asset.ts +1 -1
  83. package/plug-api/syscalls/config.ts +1 -4
  84. package/plug-api/syscalls/editor.ts +15 -15
  85. package/plug-api/syscalls/jsonschema.ts +1 -3
  86. package/plug-api/syscalls/lua.ts +3 -9
  87. package/plug-api/syscalls/mq.ts +1 -4
  88. package/plug-api/syscalls/shell.ts +4 -1
  89. package/plug-api/syscalls/space.ts +3 -10
  90. package/plug-api/syscalls/system.ts +1 -4
  91. package/plug-api/syscalls/yaml.ts +2 -6
  92. package/plug-api/system_mock.ts +0 -1
  93. package/plug-api/types/client.ts +16 -1
  94. package/plug-api/types/event.ts +6 -4
  95. package/plug-api/types/manifest.ts +8 -9
  96. package/plugs/builtin_plugs.ts +2 -2
  97. package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
@@ -18,10 +18,12 @@ import {
18
18
  } from "@codemirror/commands";
19
19
  import type { Transaction } from "@codemirror/state";
20
20
  import { EditorView } from "@codemirror/view";
21
- import { getCM as vimGetCm, Vim } from "@replit/codemirror-vim";
21
+ import { getVimModule } from "../../vim_loader.ts";
22
22
  import type { SysCallMapping } from "../system.ts";
23
23
  import type {
24
24
  FilterOption,
25
+ NotificationAction,
26
+ NotificationType,
25
27
  UploadFile,
26
28
  } from "@silverbulletmd/silverbullet/type/client";
27
29
  import { openSearchPanel } from "@codemirror/search";
@@ -48,11 +50,11 @@ export function editorSyscalls(client: Client): SysCallMapping {
48
50
  return client.currentPath();
49
51
  },
50
52
  "editor.getCurrentEditor": (): string => {
51
- return client.documentEditor?.name || "page";
53
+ return client.contentManager.documentEditor?.name || "page";
52
54
  },
53
55
  "editor.getRecentlyOpenedPages": (): PageMeta[] => {
54
- return client.ui.viewState.allPages.sort((a, b) =>
55
- (b.lastOpened || 0) - (a.lastOpened || 0)
56
+ return client.ui.viewState.allPages.sort(
57
+ (a, b) => (b.lastOpened || 0) - (a.lastOpened || 0),
56
58
  );
57
59
  },
58
60
  "editor.getText": () => {
@@ -68,12 +70,14 @@ export function editorSyscalls(client: Client): SysCallMapping {
68
70
  const line = client.editorView.state.doc.lineAt(pos);
69
71
  return {
70
72
  ...line,
71
- textWithCursor: line.text.slice(0, pos - line.from) + "|^|" +
73
+ textWithCursor:
74
+ line.text.slice(0, pos - line.from) +
75
+ "|^|" +
72
76
  line.text.slice(pos - line.from),
73
77
  };
74
78
  },
75
79
  "editor.setText": (_ctx, newText: string, shouldIsolateHistory = false) => {
76
- client.setEditorText(newText, shouldIsolateHistory);
80
+ client.contentManager.setEditorText(newText, shouldIsolateHistory);
77
81
  },
78
82
  "editor.getCursor": (): number => {
79
83
  return client.editorView.state.selection.main.from;
@@ -110,7 +114,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
110
114
  }
111
115
 
112
116
  if (
113
- // @ts-ignore: Legacy support
117
+ // @ts-expect-error: Legacy support
114
118
  ref.page !== undefined
115
119
  ) {
116
120
  console.warn(
@@ -127,7 +131,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
127
131
 
128
132
  legacyRef.kind ??= "page";
129
133
 
130
- let details: Ref["details"] = undefined;
134
+ let details: Ref["details"];
131
135
 
132
136
  if (typeof legacyRef.pos === "number") {
133
137
  details = {
@@ -160,13 +164,9 @@ export function editorSyscalls(client: Client): SysCallMapping {
160
164
  // some validation library. Didn't want to use jsonschemas here tho.
161
165
  // Ideally this would be moved into a function too
162
166
  if (!isValidPath(ref.path) && ref.path !== "") {
163
- throw new Error(
164
- "Path passed in ref to `editor.navigate` is invalid",
165
- );
167
+ throw new Error("Path passed in ref to `editor.navigate` is invalid");
166
168
  } else if (typeof ref.meta !== "boolean" && ref.meta !== undefined) {
167
- throw new Error(
168
- "ref.meta has to be of type `boolean`",
169
- );
169
+ throw new Error("ref.meta has to be of type `boolean`");
170
170
  } else if (ref.details !== undefined && typeof ref.details !== "object") {
171
171
  throw new Error(
172
172
  "ref.details has to be of type `object` or `undefined`",
@@ -181,17 +181,15 @@ export function editorSyscalls(client: Client): SysCallMapping {
181
181
  }
182
182
 
183
183
  if (
184
- ref.details?.type === "position" && typeof ref.details.pos !== "number"
184
+ ref.details?.type === "position" &&
185
+ typeof ref.details.pos !== "number"
185
186
  ) {
186
- throw new Error(
187
- "ref.details.pos has to be of type `number`",
188
- );
187
+ throw new Error("ref.details.pos has to be of type `number`");
189
188
  } else if (
190
- ref.details?.type === "header" && typeof ref.details.header !== "string"
189
+ ref.details?.type === "header" &&
190
+ typeof ref.details.header !== "string"
191
191
  ) {
192
- throw new Error(
193
- "ref.details.header has to be of type `string`",
194
- );
192
+ throw new Error("ref.details.header has to be of type `string`");
195
193
  } else if (
196
194
  ref.details?.type === "linecolumn" &&
197
195
  typeof ref.details.line !== "number" &&
@@ -214,14 +212,12 @@ export function editorSyscalls(client: Client): SysCallMapping {
214
212
  client.rebuildEditorState();
215
213
  },
216
214
  "editor.reloadConfigAndCommands": async () => {
217
- await client.clientSystem.system.localSyscall(
218
- "system.loadScripts",
219
- [],
220
- );
215
+ await client.clientSystem.system.localSyscall("system.loadScripts", []);
221
216
  await client.clientSystem.system.localSyscall(
222
217
  "system.loadSpaceStyles",
223
218
  [],
224
219
  );
220
+ client.reconfigureLanguage();
225
221
  },
226
222
  "editor.invokeCommand": (_ctx, name: string, args?: string[]) => {
227
223
  return client.runCommandByName(name, args);
@@ -232,7 +228,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
232
228
  "editor.newWindow": () => {
233
229
  globalThis.open(
234
230
  location.href,
235
- "rnd" + Math.random(),
231
+ `rnd${Math.random()}`,
236
232
  `width=${globalThis.innerWidth},heigh=${globalThis.innerHeight}`,
237
233
  );
238
234
  },
@@ -268,7 +264,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
268
264
  const reader = new FileReader();
269
265
  reader.readAsArrayBuffer(file);
270
266
  reader.onloadend = async (evt) => {
271
- if (evt.target?.readyState == FileReader.DONE) {
267
+ if (evt.target?.readyState === FileReader.DONE) {
272
268
  resolve({
273
269
  name: file.name,
274
270
  contentType: file.type,
@@ -288,15 +284,19 @@ export function editorSyscalls(client: Client): SysCallMapping {
288
284
  reject(e);
289
285
  };
290
286
 
287
+ input.style.display = "none";
288
+ document.body.appendChild(input);
291
289
  input.click();
290
+ setTimeout(() => document.body.removeChild(input), 1000);
292
291
  });
293
292
  },
294
293
  "editor.flashNotification": (
295
294
  _ctx,
296
295
  message: string,
297
- type: "error" | "info" = "info",
296
+ type: NotificationType = "info",
297
+ options?: { timeout?: number; actions?: NotificationAction[] },
298
298
  ) => {
299
- client.flashNotification(message, type);
299
+ client.ui.flashNotification(message, type, options);
300
300
  },
301
301
  "editor.filterBox": (
302
302
  _ctx,
@@ -305,13 +305,13 @@ export function editorSyscalls(client: Client): SysCallMapping {
305
305
  helpText = "",
306
306
  placeHolder = "",
307
307
  ): Promise<FilterOption | undefined> => {
308
- return client.filterBox(label, options, helpText, placeHolder);
308
+ return client.ui.filterBox(label, options, helpText, placeHolder);
309
309
  },
310
310
  "editor.showPanel": (
311
311
  _ctx,
312
312
  id: string,
313
313
  mode: number,
314
- html: string,
314
+ html: HTMLElement | HTMLElement[] | string,
315
315
  script: string,
316
316
  ) => {
317
317
  client.ui.viewDispatch({
@@ -339,7 +339,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
339
339
  progressPercentage?: number,
340
340
  progressType?: "sync" | "index",
341
341
  ) => {
342
- client.showProgress(progressPercentage, progressType);
342
+ client.ui.showProgress(progressPercentage, progressType);
343
343
  },
344
344
  "editor.insertAtPos": (
345
345
  _ctx,
@@ -351,7 +351,8 @@ export function editorSyscalls(client: Client): SysCallMapping {
351
351
  if (cursorPlaceHolder) {
352
352
  cursorPlaceholderPos = text.indexOf("|^|");
353
353
  if (cursorPlaceholderPos !== -1) {
354
- text = text.slice(0, cursorPlaceholderPos) +
354
+ text =
355
+ text.slice(0, cursorPlaceholderPos) +
355
356
  text.slice(cursorPlaceholderPos + 3);
356
357
  } else {
357
358
  cursorPlaceHolder = false;
@@ -369,9 +370,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
369
370
  selection: {
370
371
  anchor: cursorPos,
371
372
  },
372
- effects: [
373
- EditorView.scrollIntoView(cursorPos),
374
- ],
373
+ effects: [EditorView.scrollIntoView(cursorPos)],
375
374
  });
376
375
  }
377
376
  },
@@ -385,7 +384,8 @@ export function editorSyscalls(client: Client): SysCallMapping {
385
384
  let cursorPlaceholderPos = -1;
386
385
  if (cursorPlaceHolder) {
387
386
  cursorPlaceholderPos = text.indexOf("|^|");
388
- text = text.slice(0, cursorPlaceholderPos) +
387
+ text =
388
+ text.slice(0, cursorPlaceholderPos) +
389
389
  text.slice(cursorPlaceholderPos + 3);
390
390
  }
391
391
  client.editorView.dispatch({
@@ -401,9 +401,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
401
401
  selection: {
402
402
  anchor: cursorPos,
403
403
  },
404
- effects: [
405
- EditorView.scrollIntoView(cursorPos),
406
- ],
404
+ effects: [EditorView.scrollIntoView(cursorPos)],
407
405
  });
408
406
  }
409
407
  },
@@ -416,12 +414,9 @@ export function editorSyscalls(client: Client): SysCallMapping {
416
414
  if (center) {
417
415
  client.editorView.dispatch({
418
416
  effects: [
419
- EditorView.scrollIntoView(
420
- pos,
421
- {
422
- y: "center",
423
- },
424
- ),
417
+ EditorView.scrollIntoView(pos, {
418
+ y: "center",
419
+ }),
425
420
  ],
426
421
  });
427
422
  }
@@ -459,7 +454,8 @@ export function editorSyscalls(client: Client): SysCallMapping {
459
454
  const from = editorView.state.selection.main.from;
460
455
  const cursorPlaceholderPos = text.indexOf("|^|");
461
456
  if (cursorPlaceHolder && cursorPlaceholderPos !== -1) {
462
- text = text.slice(0, cursorPlaceholderPos) +
457
+ text =
458
+ text.slice(0, cursorPlaceholderPos) +
463
459
  text.slice(cursorPlaceholderPos + 3);
464
460
  } else {
465
461
  cursorPlaceHolder = false;
@@ -485,10 +481,10 @@ export function editorSyscalls(client: Client): SysCallMapping {
485
481
  message: string,
486
482
  defaultValue = "",
487
483
  ): Promise<string | undefined> => {
488
- return client.prompt(message, defaultValue);
484
+ return client.ui.prompt(message, defaultValue);
489
485
  },
490
486
  "editor.confirm": (_ctx, message: string): Promise<boolean> => {
491
- return client.confirm(message);
487
+ return client.ui.confirm(message);
492
488
  },
493
489
  "editor.alert": (_ctx, message: string) => {
494
490
  alert(message);
@@ -502,17 +498,26 @@ export function editorSyscalls(client: Client): SysCallMapping {
502
498
  key,
503
499
  value,
504
500
  });
505
- client.reloadEditor();
501
+ void client.reloadEditor();
506
502
  },
507
503
  "editor.vimEx": (_ctx, exCommand: string) => {
508
- const cm = vimGetCm(client.editorView);
509
- if (cm && cm.state.vim) {
510
- return Vim.handleEx(cm as any, exCommand);
504
+ const vimMod = getVimModule();
505
+ if (!vimMod) {
506
+ throw new Error("Vim module not loaded.");
507
+ }
508
+ const cm = vimMod.getCM(client.editorView);
509
+ if (cm?.state.vim) {
510
+ return vimMod.Vim.handleEx(cm as any, exCommand);
511
511
  } else {
512
512
  throw new Error("Vim mode not active or not initialized.");
513
513
  }
514
514
  },
515
515
  "editor.configureVimMode": () => {
516
+ const vimMod = getVimModule();
517
+ if (!vimMod) {
518
+ throw new Error("Vim module not loaded.");
519
+ }
520
+ const { Vim } = vimMod;
516
521
  // Override the default "o" binding to be more intelligent and follow the markdown editor's behavior
517
522
  Vim.mapCommand("o", "action", "newline-continue-markup", {}, {});
518
523
  Vim.mapCommand("O", "action", "back-newline-continue-markup", {}, {});
@@ -555,31 +560,29 @@ export function editorSyscalls(client: Client): SysCallMapping {
555
560
  if (config) {
556
561
  config.unmap?.forEach((binding) => {
557
562
  if (typeof binding === "string") {
558
- console.log("Unmapping " + binding);
559
- // @ts-ignore: unmap expects a string for the mode, this is problematic with Ex mappings which requires undefined or false
563
+ console.log(`Unmapping ${binding}`);
564
+ // @ts-expect-error: unmap expects a string for the mode, this is problematic with Ex mappings which requires undefined or false
560
565
  Vim.unmap(binding, undefined);
561
566
  } else if (binding.key) {
562
567
  console.log(
563
- "Unmapping " + binding.key + " in " + (binding.mode ?? "normal"),
568
+ `Unmapping ${binding.key} in ${binding.mode ?? "normal"}`,
564
569
  );
565
570
  Vim.unmap(binding.key, binding.mode ?? "normal");
566
571
  }
567
572
  });
568
573
  config.map?.forEach(({ map, to, mode }) => {
569
- console.log(
570
- "Mapping " + map + " to " + to + " for " + (mode ?? "normal"),
571
- );
574
+ console.log(`Mapping ${map} to ${to} for ${mode ?? "normal"}`);
572
575
  Vim.map(map, to, mode ?? "normal");
573
576
  });
574
577
  config.noremap?.forEach(({ map, to, mode }) => {
575
- console.log(
576
- "Noremapping " + map + " to " + to + " for " + (mode ?? "normal"),
577
- );
578
+ console.log(`Noremapping ${map} to ${to} for ${mode ?? "normal"}`);
578
579
  Vim.noremap(map, to, mode ?? "normal");
579
580
  });
580
581
  config.commands?.forEach(({ ex, command }) => {
581
- console.log("Mapping command '" + command + "' to Ex " + ex);
582
- Vim.defineEx(ex, "", () => client.runCommandByName(command));
582
+ console.log(`Mapping command '${command}' to Ex ${ex}`);
583
+ Vim.defineEx(ex, "", (_, params) =>
584
+ client.runCommandByName(command, params.args),
585
+ );
583
586
  });
584
587
  } else {
585
588
  console.log("No vim config found");
@@ -592,7 +595,7 @@ export function editorSyscalls(client: Client): SysCallMapping {
592
595
  client.startPageNavigate(mode);
593
596
  },
594
597
  "editor.openCommandPalette": () => {
595
- client.startCommandPalette();
598
+ void client.startCommandPalette();
596
599
  },
597
600
  "editor.deleteLine": () => {
598
601
  deleteLine(client.editorView);
@@ -651,13 +654,13 @@ export function editorSyscalls(client: Client): SysCallMapping {
651
654
  }
652
655
  } catch (e) {
653
656
  console.error(e);
654
- client.flashNotification(`Could not copy to clipboard: ${e}`);
657
+ client.ui.flashNotification(`Could not copy to clipboard: ${e}`);
655
658
  }
656
659
  },
657
660
  "editor.sendMessage": (_ctx, type: string, data: any) => {
658
- if (!client.isDocumentEditor()) return;
661
+ if (!client.contentManager.isDocumentEditor()) return;
659
662
 
660
- client.documentEditor.sendPublicMessage({
663
+ client.contentManager.documentEditor.sendPublicMessage({
661
664
  type,
662
665
  data,
663
666
  });
@@ -18,19 +18,16 @@ export function eventSyscalls(
18
18
  /**
19
19
  * Define a Lua event listener
20
20
  */
21
- "event.listen": (
22
- _ctx,
23
- def: EventSubscription,
24
- ) => {
21
+ "event.listen": (_ctx, def: EventSubscription) => {
25
22
  // console.log("Registering Lua event listener: ", def.name);
26
- client.config.insert([
27
- "eventListeners",
28
- def.name,
29
- ], async (...args: any[]) => {
30
- // Convert return value to JS
31
- const val = await def.run(...args);
32
- return luaValueToJS(val, LuaStackFrame.lostFrame);
33
- });
23
+ client.config.insert(
24
+ ["eventListeners", def.name],
25
+ async (...args: any[]) => {
26
+ // Convert return value to JS
27
+ const val = await def.run(...args);
28
+ return luaValueToJS(val, LuaStackFrame.lostFrame);
29
+ },
30
+ );
34
31
  },
35
32
  };
36
33
  }
@@ -12,9 +12,7 @@ import {
12
12
  } from "@silverbulletmd/silverbullet/lib/crypto";
13
13
  import { fsEndpoint } from "../../spaces/constants.ts";
14
14
 
15
- export function sandboxFetchSyscalls(
16
- client: Client,
17
- ): SysCallMapping {
15
+ export function sandboxFetchSyscalls(client: Client): SysCallMapping {
18
16
  return {
19
17
  // For use in Lua
20
18
  "http.request": async (
@@ -25,17 +23,18 @@ export function sandboxFetchSyscalls(
25
23
  console.warn("Deprecated: use net.proxyFetch() instead");
26
24
  // JSONify any non-serializable body
27
25
  if (
28
- options?.body && typeof options.body !== "string" &&
26
+ options?.body &&
27
+ typeof options.body !== "string" &&
29
28
  !(options.body instanceof Uint8Array)
30
29
  ) {
31
30
  options.body = JSON.stringify(options.body);
32
31
  }
33
32
  const fetchOptions = options
34
33
  ? {
35
- method: options.method,
36
- headers: {} as Record<string, string>,
37
- body: options.body,
38
- }
34
+ method: options.method,
35
+ headers: {} as Record<string, string>,
36
+ body: options.body,
37
+ }
39
38
  : {};
40
39
 
41
40
  fetchOptions.headers = buildProxyHeaders(options?.headers);
@@ -45,19 +44,27 @@ export function sandboxFetchSyscalls(
45
44
  fetchOptions,
46
45
  );
47
46
  // Do sensible things with the body based on the content type
47
+ // Read as ArrayBuffer first to safely handle empty responses (e.g.
48
+ // PUT/DELETE returning 204 with Content-Type: application/json).
49
+ // resp.arrayBuffer() never throws on an empty body, whereas
50
+ // resp.json() would throw a SyntaxError.
51
+ const rawBytes = new Uint8Array(await resp.arrayBuffer());
48
52
  let body: any;
49
- const contentTypeHeader = options.responseEncoding ||
53
+ const contentTypeHeader =
54
+ options.responseEncoding ||
50
55
  resp.headers.get("x-proxy-header-content-type");
51
56
  const statusCode = +(resp.headers.get("x-proxy-status-code") || "200");
52
- if (contentTypeHeader?.startsWith("application/json")) {
53
- body = await resp.json();
57
+ if (rawBytes.length === 0) {
58
+ body = null;
59
+ } else if (contentTypeHeader?.startsWith("application/json")) {
60
+ body = JSON.parse(new TextDecoder().decode(rawBytes));
54
61
  } else if (
55
62
  contentTypeHeader?.startsWith("application/xml") ||
56
63
  contentTypeHeader?.startsWith("text/")
57
64
  ) {
58
- body = await resp.text();
65
+ body = new TextDecoder().decode(rawBytes);
59
66
  } else {
60
- body = new Uint8Array(await resp.arrayBuffer());
67
+ body = rawBytes;
61
68
  }
62
69
  return {
63
70
  ok: resp.ok,
@@ -74,15 +81,15 @@ export function sandboxFetchSyscalls(
74
81
  // console.log("Got sandbox fetch ", url, op);
75
82
  const fetchOptions = options
76
83
  ? {
77
- method: options.method,
78
- headers: options.headers,
79
- body: options.base64Body && base64Decode(options.base64Body),
80
- }
84
+ method: options.method,
85
+ headers: options.headers,
86
+ body: options.base64Body && base64Decode(options.base64Body),
87
+ }
81
88
  : {};
82
89
  fetchOptions.headers = buildProxyHeaders(options?.headers);
83
90
  const resp = await client.httpSpacePrimitives.authenticatedFetch(
84
91
  buildProxyUrl(client, url),
85
- // Casting this to any because of weird Deno typing
92
+ // Casting to any due to TypeScript fetch type limitations
86
93
  fetchOptions as any,
87
94
  );
88
95
  const statusCode = +(resp.headers.get("x-proxy-status-code") || "200");
@@ -100,8 +107,11 @@ export function sandboxFetchSyscalls(
100
107
  function buildProxyUrl(client: Client, url: string) {
101
108
  url = url.replace(/^https?:\/\//, "");
102
109
  // Strip off the /.fs and replace with /.proxy
103
- return client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
104
- "/.proxy/" + url;
110
+ return (
111
+ client.httpSpacePrimitives.url.slice(0, -fsEndpoint.length) +
112
+ "/.proxy/" +
113
+ url
114
+ );
105
115
  }
106
116
 
107
117
  function buildProxyHeaders(headers?: Record<string, any>): Record<string, any> {
@@ -115,9 +125,7 @@ function buildProxyHeaders(headers?: Record<string, any>): Record<string, any> {
115
125
  return newHeaders;
116
126
  }
117
127
 
118
- function extractProxyHeaders(
119
- headers: Headers,
120
- ): Record<string, any> {
128
+ function extractProxyHeaders(headers: Headers): Record<string, any> {
121
129
  const newHeaders: Record<string, any> = {};
122
130
  for (const [key, value] of headers.entries()) {
123
131
  if (key.toLowerCase().startsWith("x-proxy-header-")) {
@@ -20,7 +20,16 @@ export function indexSyscalls(
20
20
  "index.tag": (_ctx, tagName: string): LuaQueryCollection => {
21
21
  return objectIndex.tag(tagName);
22
22
  },
23
- "index.ensureFullIndex": (_ctx) => {
23
+ "index.contentPages": (): LuaQueryCollection => {
24
+ return objectIndex.contentPages();
25
+ },
26
+ "index.metaPages": (): LuaQueryCollection => {
27
+ return objectIndex.metaPages();
28
+ },
29
+ "index.aggregates": (): LuaQueryCollection => {
30
+ return objectIndex.aggregates();
31
+ },
32
+ "index.ensureFullIndex": () => {
24
33
  return objectIndex.ensureFullIndex(client.space);
25
34
  },
26
35
  "index.reindexSpace": () => {
@@ -1,40 +1,62 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
- import { Ajv, type ValidateFunction } from "ajv";
2
+ import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
3
3
 
4
- const ajv = new Ajv();
4
+ // Register custom formats
5
+ format.email = (data: string) => data.includes("@");
6
+ format["page-ref"] = (data: string) =>
7
+ data.startsWith("[[") && data.endsWith("]]");
5
8
 
6
- ajv.addFormat("email", {
7
- validate: (data: string) => {
8
- // TODO: Implement email validation
9
- return data.includes("@");
10
- },
11
- async: false,
12
- });
9
+ const schemaCache = new Map<string, Validator>();
13
10
 
14
- ajv.addFormat("page-ref", {
15
- validate: (data: string) => {
16
- return data.startsWith("[[") && data.endsWith("]]");
17
- },
18
- async: false,
19
- });
11
+ /**
12
+ * Deep-clone a value, replacing any functions with null.
13
+ * JSON schema can't validate functions, so we strip them before validation.
14
+ */
15
+ function stripFunctions(value: any): any {
16
+ if (typeof value === "function") return null;
17
+ if (value === null || value === undefined || typeof value !== "object") {
18
+ return value;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.map(stripFunctions);
22
+ }
23
+ const result: Record<string, any> = {};
24
+ for (const key of Object.keys(value)) {
25
+ result[key] = stripFunctions(value[key]);
26
+ }
27
+ return result;
28
+ }
20
29
 
21
- const schemaCache = new Map<string, ValidateFunction>();
30
+ function formatErrors(errors: OutputUnit[]): string {
31
+ // Filter out "properties" wrapper errors, keep only the specific leaf errors
32
+ const leafErrors = errors.filter((e) => e.keyword !== "properties");
33
+ const errorsToUse = leafErrors.length > 0 ? leafErrors : errors;
34
+
35
+ return errorsToUse
36
+ .map((e) => {
37
+ // Convert instanceLocation from "#/foo/bar" to "foo.bar"
38
+ const path =
39
+ e.instanceLocation === "#"
40
+ ? ""
41
+ : e.instanceLocation.slice(2).replaceAll("/", ".");
42
+ return path ? `${path}: ${e.error}` : e.error;
43
+ })
44
+ .join(", ");
45
+ }
22
46
 
23
47
  export function validateObject(schema: any, object: any): undefined | string {
24
48
  try {
25
49
  const schemaKey = JSON.stringify(schema);
26
50
  if (!schemaCache.has(schemaKey)) {
27
- const validate = ajv.compile(schema);
28
- schemaCache.set(schemaKey, validate);
51
+ const validator = new Validator(schema, "7");
52
+ schemaCache.set(schemaKey, validator);
29
53
  }
30
- const validate = schemaCache.get(schemaKey)!;
31
- if (validate(object)) {
54
+ const validator = schemaCache.get(schemaKey)!;
55
+ const result = validator.validate(stripFunctions(object));
56
+ if (result.valid) {
32
57
  return;
33
58
  } else {
34
- let text = ajv.errorsText(validate.errors);
35
- text = text.replaceAll("/", ".");
36
- text = text.replace(/^data[\.\s]/, "");
37
- return text;
59
+ return formatErrors(result.errors);
38
60
  }
39
61
  } catch (e: any) {
40
62
  return e.message;
@@ -42,12 +64,33 @@ export function validateObject(schema: any, object: any): undefined | string {
42
64
  }
43
65
 
44
66
  export function validateSchema(schema: any): undefined | string {
45
- const valid = ajv.validateSchema(schema);
46
- if (valid) {
67
+ if (schema === null || schema === undefined) {
68
+ return "schema must not be null or undefined";
69
+ }
70
+ if (typeof schema === "boolean") {
47
71
  return;
48
- } else {
49
- return ajv.errorsText(ajv.errors);
50
72
  }
73
+ if (typeof schema !== "object" || Array.isArray(schema)) {
74
+ return "schema must be an object or boolean";
75
+ }
76
+ if (schema.type !== undefined) {
77
+ const validTypes = [
78
+ "string",
79
+ "number",
80
+ "integer",
81
+ "boolean",
82
+ "object",
83
+ "array",
84
+ "null",
85
+ ];
86
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
87
+ for (const t of types) {
88
+ if (!validTypes.includes(t)) {
89
+ return `schema.type must be one of ${validTypes.join(", ")}`;
90
+ }
91
+ }
92
+ }
93
+ return;
51
94
  }
52
95
 
53
96
  export function jsonschemaSyscalls(): SysCallMapping {
@@ -59,10 +102,7 @@ export function jsonschemaSyscalls(): SysCallMapping {
59
102
  ): undefined | string => {
60
103
  return validateObject(schema, object);
61
104
  },
62
- "jsonschema.validateSchema": (
63
- _ctx,
64
- schema: any,
65
- ): undefined | string => {
105
+ "jsonschema.validateSchema": (_ctx, schema: any): undefined | string => {
66
106
  return validateSchema(schema);
67
107
  },
68
108
  };