@makerclay/core 1.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.
Files changed (91) hide show
  1. package/LICENSE +221 -0
  2. package/README.md +82 -0
  3. package/package.json +52 -0
  4. package/src/admin/listing.js +149 -0
  5. package/src/admin/routes.js +362 -0
  6. package/src/attic.js +168 -0
  7. package/src/auth/can.js +79 -0
  8. package/src/auth/csrf.js +74 -0
  9. package/src/auth/none.js +24 -0
  10. package/src/auth/password.js +281 -0
  11. package/src/auth/passwords.js +79 -0
  12. package/src/auth/rate-limit.js +66 -0
  13. package/src/auth/sessions.js +86 -0
  14. package/src/auth/token-lanes.js +72 -0
  15. package/src/boot.js +120 -0
  16. package/src/client.js +99 -0
  17. package/src/collections/index.js +397 -0
  18. package/src/collections/routes.js +166 -0
  19. package/src/create-host.js +351 -0
  20. package/src/derived/data-extractor.js +22 -0
  21. package/src/derived/index.js +121 -0
  22. package/src/documents/format-html.js +313 -0
  23. package/src/documents/replace.js +257 -0
  24. package/src/documents/root-attrs.js +171 -0
  25. package/src/documents/serve.js +137 -0
  26. package/src/documents/stale.js +20 -0
  27. package/src/index.js +12 -0
  28. package/src/inspect.js +136 -0
  29. package/src/json-errors.js +59 -0
  30. package/src/livesync.js +75 -0
  31. package/src/nodes/identity.js +14 -0
  32. package/src/nodes/names.js +57 -0
  33. package/src/nodes/ops.js +613 -0
  34. package/src/nodes/scanner.js +321 -0
  35. package/src/nodes/store.js +111 -0
  36. package/src/pages.js +166 -0
  37. package/src/paths.js +302 -0
  38. package/src/recovery/overlay.js +327 -0
  39. package/src/recovery/replay.js +236 -0
  40. package/src/recovery-ui.js +185 -0
  41. package/src/recovery.js +30 -0
  42. package/src/requests.js +73 -0
  43. package/src/routes/meta.js +62 -0
  44. package/src/routes/read.js +105 -0
  45. package/src/routes/save.js +102 -0
  46. package/src/routes/sync.js +118 -0
  47. package/src/routes/upload.js +128 -0
  48. package/src/share/index.js +207 -0
  49. package/src/share/save-tokens.js +65 -0
  50. package/src/spec/codes.js +42 -0
  51. package/src/spec/meta.js +52 -0
  52. package/src/spec/wire.js +115 -0
  53. package/src/store/index.js +29 -0
  54. package/src/store/migrations/001-init.sql +114 -0
  55. package/src/store/sqlite.js +540 -0
  56. package/src/templates.js +50 -0
  57. package/src/tenants/index.js +355 -0
  58. package/src/tenants/isolation.js +91 -0
  59. package/src/tenants/routes.js +131 -0
  60. package/src/ui.js +95 -0
  61. package/src/util/cookies.js +26 -0
  62. package/src/util/express.js +8 -0
  63. package/src/util/fsx.js +205 -0
  64. package/src/util/id.js +37 -0
  65. package/src/util/lockfile.js +52 -0
  66. package/src/util/locks.js +35 -0
  67. package/src/util/multipart.js +33 -0
  68. package/src/versions/files.js +307 -0
  69. package/src/versions/index.js +16 -0
  70. package/src/versions/naming.js +172 -0
  71. package/src/versions/routes.js +91 -0
  72. package/src/wire-compat.js +61 -0
  73. package/ui/app.css +164 -0
  74. package/ui/attic.html +198 -0
  75. package/ui/dashboard.html +456 -0
  76. package/ui/editor.html +156 -0
  77. package/ui/error.html +18 -0
  78. package/ui/login.html +58 -0
  79. package/ui/records.html +173 -0
  80. package/ui/recovery.html +152 -0
  81. package/ui/setup.html +61 -0
  82. package/ui/share-qr.html +44 -0
  83. package/ui/templates/blank.html +16 -0
  84. package/ui/templates/devlog.html +71 -0
  85. package/ui/templates/hackable-dashboard.html +145 -0
  86. package/ui/templates/kanban.html +85 -0
  87. package/ui/templates/landing.html +108 -0
  88. package/ui/templates/writer.html +50 -0
  89. package/ui/tenants.html +172 -0
  90. package/ui/trash.html +134 -0
  91. package/ui/versions.html +114 -0
package/ui/trash.html ADDED
@@ -0,0 +1,134 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{title}}</title>
7
+ <link rel="stylesheet" href="/_/ui.css">
8
+ </head>
9
+ <body>
10
+
11
+ <header class="bar">
12
+ <a class="btn" href="/_/" title="Back to your files">←</a>
13
+ <span class="brand">Trash</span>
14
+ <span class="spacer"></span>
15
+ <button id="empty-all" class="danger">Empty trash</button>
16
+ </header>
17
+
18
+ <main class="wrap">
19
+ <table>
20
+ <thead>
21
+ <tr><th>Name</th><th>Was at</th><th class="num">Deleted</th><th class="actions">Actions</th></tr>
22
+ </thead>
23
+ <tbody id="rows"></tbody>
24
+ </table>
25
+ <div class="empty" id="empty" hidden>The trash is empty.</div>
26
+ <p class="hint" style="margin-top:1.5rem">
27
+ Deleted files keep their version history until you purge them. Restoring puts
28
+ a file back where it came from, under a new name if that one has been taken.
29
+ History whose file is gone rather than deleted is kept in
30
+ <a href="/_/attic.html">the attic</a>.
31
+ </p>
32
+ </main>
33
+
34
+ <div class="toast" id="toast"></div>
35
+
36
+ <script>
37
+ const rows = document.getElementById('rows');
38
+ const empty = document.getElementById('empty');
39
+ const toastEl = document.getElementById('toast');
40
+
41
+ function toast(message, isError) {
42
+ toastEl.textContent = message;
43
+ toastEl.className = 'toast show' + (isError ? ' error' : '');
44
+ clearTimeout(toast.timer);
45
+ toast.timer = setTimeout(() => { toastEl.className = 'toast'; }, 2600);
46
+ }
47
+
48
+ async function api(path, body) {
49
+ const res = await fetch(path, {
50
+ method: body ? 'POST' : 'GET',
51
+ headers: body ? { 'Content-Type': 'application/json', Accept: 'application/json' } : { Accept: 'application/json' },
52
+ body: body ? JSON.stringify(body) : undefined,
53
+ });
54
+ const parsed = await res.json().catch(() => ({}));
55
+ if (!res.ok) throw new Error(parsed.msg || res.statusText);
56
+ return parsed;
57
+ }
58
+
59
+ async function load() {
60
+ try {
61
+ const body = await api('/_/trash');
62
+ rows.replaceChildren(...body.entries.map((entry) => {
63
+ const tr = document.createElement('tr');
64
+
65
+ const name = document.createElement('td');
66
+ name.textContent = (entry.kind === 'dir' ? '📁 ' : '📄 ') + entry.name;
67
+ if (!entry.recoverable) name.insertAdjacentHTML('beforeend', '<span class="tag private">bytes missing</span>');
68
+ tr.appendChild(name);
69
+
70
+ const from = document.createElement('td');
71
+ from.className = 'mono';
72
+ from.style.color = 'var(--muted)';
73
+ from.textContent = entry.path;
74
+ tr.appendChild(from);
75
+
76
+ const when = document.createElement('td');
77
+ when.className = 'num';
78
+ when.textContent = new Date(entry.deletedAt).toLocaleDateString();
79
+ tr.appendChild(when);
80
+
81
+ const actions = document.createElement('td');
82
+ actions.className = 'actions';
83
+
84
+ const restore = document.createElement('button');
85
+ restore.textContent = 'Restore';
86
+ restore.disabled = !entry.recoverable;
87
+ restore.addEventListener('click', () => run(() => api('/_/trash/restore', { ids: [entry.id] }), 'Restored'));
88
+ actions.appendChild(restore);
89
+
90
+ const purge = document.createElement('button');
91
+ purge.textContent = 'Delete forever';
92
+ purge.className = 'danger';
93
+ purge.addEventListener('click', () => {
94
+ if (!confirm(`Permanently delete “${entry.name}” and all of its history?`)) return;
95
+ run(() => api('/_/trash/purge', { ids: [entry.id] }), 'Deleted for good');
96
+ });
97
+ actions.appendChild(purge);
98
+
99
+ tr.appendChild(actions);
100
+ return tr;
101
+ }));
102
+ empty.hidden = body.entries.length > 0;
103
+ } catch (problem) {
104
+ toast(problem.message, true);
105
+ }
106
+ }
107
+
108
+ async function run(work, message) {
109
+ try {
110
+ await work();
111
+ toast(message);
112
+ load();
113
+ } catch (problem) {
114
+ toast(problem.message, true);
115
+ }
116
+ }
117
+
118
+ document.getElementById('empty-all').addEventListener('click', () => {
119
+ if (!confirm('Permanently delete everything in the trash, and all of its history?')) return;
120
+ // The route answers 200 with ok:false when it could not purge everything, so
121
+ // reading the status alone reports success over bytes that are still there.
122
+ run(async () => {
123
+ const body = await api('/_/trash/empty', {});
124
+ if (body.failed?.length) {
125
+ throw new Error(`Purged ${body.purged}. ${body.failed.length} could not be removed.`);
126
+ }
127
+ return body;
128
+ }, 'Trash emptied');
129
+ });
130
+
131
+ load();
132
+ </script>
133
+ </body>
134
+ </html>
@@ -0,0 +1,114 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{title}}</title>
7
+ <link rel="stylesheet" href="/_/ui.css">
8
+ </head>
9
+ <body data-path="{{path}}">
10
+
11
+ <header class="bar">
12
+ <a class="btn" href="/_/" title="Back to your files">←</a>
13
+ <span class="brand">History</span>
14
+ <span class="mono" style="color:var(--muted)">{{name}}</span>
15
+ <span class="spacer"></span>
16
+ <a class="btn" href="/{{path}}" target="_blank">View current</a>
17
+ </header>
18
+
19
+ <main class="wrap">
20
+ <table>
21
+ <thead>
22
+ <tr><th>Saved</th><th class="num">Size</th><th class="actions">Actions</th></tr>
23
+ </thead>
24
+ <tbody id="rows"></tbody>
25
+ </table>
26
+ <div class="empty" id="empty" hidden>No history yet. It starts at the first save.</div>
27
+ <p class="hint" style="margin-top:1.5rem">
28
+ Restoring backs up what is there now first, so restoring is itself undoable.
29
+ </p>
30
+ <p class="hint" style="margin-top:.75rem">
31
+ <a href="/_/attic.html">Why don't I have more history?</a>
32
+ </p>
33
+ </main>
34
+
35
+ <div class="toast" id="toast"></div>
36
+
37
+ <script>
38
+ const docPath = document.body.dataset.path;
39
+ const rows = document.getElementById('rows');
40
+ const empty = document.getElementById('empty');
41
+ const toastEl = document.getElementById('toast');
42
+
43
+ function toast(message, isError) {
44
+ toastEl.textContent = message;
45
+ toastEl.className = 'toast show' + (isError ? ' error' : '');
46
+ clearTimeout(toast.timer);
47
+ toast.timer = setTimeout(() => { toastEl.className = 'toast'; }, 2800);
48
+ }
49
+
50
+ const headers = { 'Document-URL': '/' + docPath, Accept: 'application/json' };
51
+ const bytes = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`);
52
+
53
+ async function load() {
54
+ const res = await fetch('/_/versions', { headers });
55
+ const body = await res.json().catch(() => ({}));
56
+ if (!res.ok) { toast(body.msg || 'Could not load history', true); return; }
57
+
58
+ rows.replaceChildren(...body.versions.map((version, index) => {
59
+ const tr = document.createElement('tr');
60
+
61
+ const when = document.createElement('td');
62
+ when.textContent = new Date(version.time).toLocaleString();
63
+ if (index === 0) when.insertAdjacentHTML('beforeend', '<span class="tag">current</span>');
64
+ tr.appendChild(when);
65
+
66
+ const size = document.createElement('td');
67
+ size.className = 'num';
68
+ size.textContent = bytes(version.size);
69
+ tr.appendChild(size);
70
+
71
+ const actions = document.createElement('td');
72
+ actions.className = 'actions';
73
+
74
+ const view = document.createElement('button');
75
+ view.textContent = 'View';
76
+ view.addEventListener('click', () => viewVersion(version.name));
77
+ actions.appendChild(view);
78
+
79
+ const restore = document.createElement('button');
80
+ restore.textContent = 'Restore';
81
+ restore.disabled = index === 0;
82
+ restore.addEventListener('click', () => restoreVersion(version.name));
83
+ actions.appendChild(restore);
84
+
85
+ tr.appendChild(actions);
86
+ return tr;
87
+ }));
88
+ empty.hidden = body.versions.length > 0;
89
+ }
90
+
91
+ // Opened as a blob so an old version can never be mistaken for the live URL,
92
+ // bookmarked, or shared by accident.
93
+ async function viewVersion(name) {
94
+ const res = await fetch(`/_/version/${encodeURIComponent(name)}`, { headers });
95
+ if (!res.ok) { toast('Could not read that version', true); return; }
96
+ const html = await res.text();
97
+ const url = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
98
+ window.open(url, '_blank');
99
+ setTimeout(() => URL.revokeObjectURL(url), 30000);
100
+ }
101
+
102
+ async function restoreVersion(name) {
103
+ if (!confirm('Restore this version? The current file is backed up first, so you can undo it.')) return;
104
+ const res = await fetch(`/_/restore/${encodeURIComponent(name)}`, { method: 'POST', headers });
105
+ const body = await res.json().catch(() => ({}));
106
+ if (!res.ok) { toast(body.msg || 'Restore failed', true); return; }
107
+ toast('Restored');
108
+ load();
109
+ }
110
+
111
+ load();
112
+ </script>
113
+ </body>
114
+ </html>