@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/app.css ADDED
@@ -0,0 +1,164 @@
1
+ /* The makerclay host UI.
2
+ Edit this file and every host page changes. `makerclay repair --ui` puts the
3
+ shipped version back; `makerclay eject` moves these pages into your own tree
4
+ as ordinary, versioned, malleable documents. */
5
+
6
+ :root {
7
+ --bg: #fbfaf8;
8
+ --panel: #ffffff;
9
+ --ink: #1c1b19;
10
+ --muted: #6b6862;
11
+ --line: #e3dfd8;
12
+ --accent: #9a5b28;
13
+ --accent-ink: #fdfcfa;
14
+ --danger: #a3342a;
15
+ --ok: #2f6b46;
16
+ --radius: 8px;
17
+ color-scheme: light dark;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --bg: #171614;
23
+ --panel: #1f1e1b;
24
+ --ink: #ece8e1;
25
+ --muted: #9b968d;
26
+ --line: #322f2a;
27
+ --accent: #d98b4a;
28
+ --accent-ink: #1a1815;
29
+ --danger: #e07a6c;
30
+ --ok: #7bbd93;
31
+ }
32
+ }
33
+
34
+ * { box-sizing: border-box; }
35
+
36
+ body {
37
+ margin: 0;
38
+ background: var(--bg);
39
+ color: var(--ink);
40
+ font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
41
+ -webkit-font-smoothing: antialiased;
42
+ }
43
+
44
+ a { color: var(--accent); }
45
+
46
+ code, pre, .mono {
47
+ font-family: ui-monospace, "SF Mono", "Berkeley Mono", Menlo, monospace;
48
+ font-size: 0.92em;
49
+ }
50
+
51
+ .wrap { max-width: 68rem; margin: 0 auto; padding: 1.25rem; }
52
+ .narrow { max-width: 26rem; margin: 8vh auto; padding: 1.25rem; }
53
+
54
+ header.bar {
55
+ display: flex;
56
+ gap: .75rem;
57
+ align-items: center;
58
+ flex-wrap: wrap;
59
+ padding: .75rem 1.25rem;
60
+ border-bottom: 1px solid var(--line);
61
+ background: var(--panel);
62
+ position: sticky;
63
+ top: 0;
64
+ z-index: 5;
65
+ }
66
+
67
+ header.bar .brand { font-weight: 650; letter-spacing: -0.01em; margin-right: .25rem; }
68
+ header.bar .spacer { flex: 1; }
69
+
70
+ nav.crumbs { display: flex; gap: .35rem; flex-wrap: wrap; color: var(--muted); font-size: .9rem; }
71
+ nav.crumbs a { text-decoration: none; }
72
+ nav.crumbs span::after { content: "/"; margin-left: .35rem; color: var(--line); }
73
+
74
+ button, .btn {
75
+ font: inherit;
76
+ padding: .38rem .7rem;
77
+ border: 1px solid var(--line);
78
+ border-radius: var(--radius);
79
+ background: var(--panel);
80
+ color: var(--ink);
81
+ cursor: pointer;
82
+ text-decoration: none;
83
+ display: inline-block;
84
+ }
85
+ button:hover, .btn:hover { border-color: var(--muted); }
86
+ button.primary, .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 550; }
87
+ button.danger { color: var(--danger); }
88
+ button:disabled { opacity: .5; cursor: default; }
89
+
90
+ input[type=text], input[type=password], input[type=search], select, textarea {
91
+ font: inherit;
92
+ padding: .4rem .55rem;
93
+ border: 1px solid var(--line);
94
+ border-radius: var(--radius);
95
+ background: var(--panel);
96
+ color: var(--ink);
97
+ width: 100%;
98
+ }
99
+ input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
100
+
101
+ label { display: block; margin: .9rem 0 .3rem; font-weight: 550; font-size: .9rem; }
102
+ .hint { color: var(--muted); font-size: .86rem; margin: .35rem 0 0; }
103
+
104
+ table { width: 100%; border-collapse: collapse; }
105
+ th, td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); }
106
+ th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 600; }
107
+ tr:hover td { background: color-mix(in oklab, var(--accent) 6%, transparent); }
108
+ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); }
109
+ td.name a { text-decoration: none; font-weight: 500; }
110
+ td.actions { text-align: right; white-space: nowrap; }
111
+ td.actions button { padding: .2rem .45rem; font-size: .82rem; }
112
+
113
+ .icon { width: 1.1em; display: inline-block; color: var(--muted); }
114
+
115
+ .tag {
116
+ display: inline-block;
117
+ font-size: .72rem;
118
+ padding: .05rem .35rem;
119
+ border-radius: 999px;
120
+ border: 1px solid var(--line);
121
+ color: var(--muted);
122
+ margin-left: .35rem;
123
+ vertical-align: 1px;
124
+ }
125
+ .tag.private { color: var(--danger); border-color: color-mix(in oklab, var(--danger) 40%, var(--line)); }
126
+ .tag.shared { color: var(--ok); border-color: color-mix(in oklab, var(--ok) 40%, var(--line)); }
127
+
128
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem; }
129
+ .empty { color: var(--muted); padding: 2.5rem 1rem; text-align: center; }
130
+
131
+ .toast {
132
+ position: fixed;
133
+ bottom: 1rem;
134
+ left: 50%;
135
+ transform: translateX(-50%);
136
+ background: var(--ink);
137
+ color: var(--bg);
138
+ padding: .5rem .9rem;
139
+ border-radius: var(--radius);
140
+ font-size: .9rem;
141
+ opacity: 0;
142
+ transition: opacity .18s;
143
+ pointer-events: none;
144
+ z-index: 50;
145
+ }
146
+ .toast.show { opacity: 1; }
147
+ .toast.error { background: var(--danger); color: #fff; }
148
+
149
+ dialog {
150
+ border: 1px solid var(--line);
151
+ border-radius: var(--radius);
152
+ background: var(--panel);
153
+ color: var(--ink);
154
+ padding: 1.1rem;
155
+ width: min(24rem, 92vw);
156
+ }
157
+ dialog::backdrop { background: rgb(0 0 0 / .35); }
158
+ dialog h2 { margin: 0; font-size: 1.05rem; }
159
+ dialog label:first-of-type { margin-top: .7rem; }
160
+ dialog .buttons { display: flex; gap: .5rem; justify-content: flex-end; margin-top: 1.2rem; }
161
+
162
+ .row { display: flex; gap: .5rem; align-items: center; }
163
+ .grow { flex: 1; }
164
+ .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
package/ui/attic.html ADDED
@@ -0,0 +1,198 @@
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">The attic</span>
14
+ <span class="spacer"></span>
15
+ </header>
16
+
17
+ <main class="wrap">
18
+ <p class="hint" style="margin:0 0 1.25rem">
19
+ History belongs to a file, and this host follows a file when you rename it or
20
+ move it. If a file goes somewhere it cannot be followed, its history is kept
21
+ here instead of being thrown away. Put a history back on a file and the two
22
+ are joined again.
23
+ </p>
24
+
25
+ <table>
26
+ <thead>
27
+ <tr>
28
+ <th>Was</th><th class="num">Versions</th><th class="num">Last seen</th>
29
+ <th class="actions">Actions</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody id="rows"></tbody>
33
+ </table>
34
+ <div class="empty" id="empty" hidden>
35
+ Nothing up here. Every piece of history on this host is attached to a file,
36
+ which is how it should be.
37
+ </div>
38
+ </main>
39
+
40
+ <div class="toast" id="toast"></div>
41
+
42
+ <script>
43
+ const rows = document.getElementById('rows');
44
+ const empty = document.getElementById('empty');
45
+ const toastEl = document.getElementById('toast');
46
+
47
+ function toast(message, isError) {
48
+ toastEl.textContent = message;
49
+ toastEl.className = 'toast show' + (isError ? ' error' : '');
50
+ clearTimeout(toast.timer);
51
+ toast.timer = setTimeout(() => { toastEl.className = 'toast'; }, 2600);
52
+ }
53
+
54
+ const bytes = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`);
55
+
56
+ async function api(path, body) {
57
+ const res = await fetch(path, {
58
+ method: body ? 'POST' : 'GET',
59
+ headers: body ? { 'Content-Type': 'application/json', Accept: 'application/json' } : { Accept: 'application/json' },
60
+ body: body ? JSON.stringify(body) : undefined,
61
+ });
62
+ const parsed = await res.json().catch(() => ({}));
63
+ if (!res.ok) throw new Error(parsed.msg || res.statusText);
64
+ return parsed;
65
+ }
66
+
67
+ async function load() {
68
+ try {
69
+ const body = await api('/_/attic');
70
+ rows.replaceChildren(...body.entries.flatMap((entry) => {
71
+ const tr = document.createElement('tr');
72
+
73
+ const was = document.createElement('td');
74
+ const name = document.createElement('div');
75
+ name.textContent = entry.path.split('/').pop();
76
+ was.appendChild(name);
77
+ const where = document.createElement('div');
78
+ where.className = 'mono';
79
+ where.style.color = 'var(--muted)';
80
+ where.style.fontSize = '.82rem';
81
+ where.textContent = entry.path;
82
+ was.appendChild(where);
83
+ tr.appendChild(was);
84
+
85
+ const count = document.createElement('td');
86
+ count.className = 'num';
87
+ count.textContent = entry.versions;
88
+ tr.appendChild(count);
89
+
90
+ const seen = document.createElement('td');
91
+ seen.className = 'num';
92
+ seen.textContent = entry.lastSeen ? new Date(entry.lastSeen).toLocaleDateString() : '';
93
+ tr.appendChild(seen);
94
+
95
+ const actions = document.createElement('td');
96
+ actions.className = 'actions';
97
+
98
+ const back = document.createElement('button');
99
+ back.textContent = 'Put back';
100
+ back.addEventListener('click', () => {
101
+ const where = prompt('Put this history back on which file?', entry.path);
102
+ if (!where) return;
103
+ run(() => api('/_/attic/put-back', { id: entry.id, path: where }), 'Put back');
104
+ });
105
+ actions.appendChild(back);
106
+
107
+ const download = document.createElement('a');
108
+ download.className = 'btn';
109
+ download.textContent = 'Newest';
110
+ download.title = 'Download the newest version';
111
+ download.href = `/_/attic/${encodeURIComponent(entry.id)}/${encodeURIComponent(entry.newest)}`;
112
+ actions.appendChild(download);
113
+
114
+ const forget = document.createElement('button');
115
+ forget.textContent = 'Forget';
116
+ forget.className = 'danger';
117
+ forget.addEventListener('click', () => {
118
+ if (!confirm(`Forget the history of “${entry.path}”? All ${entry.versions} version(s) are deleted permanently and cannot be brought back.`)) return;
119
+ run(() => api('/_/attic/forget', { id: entry.id }), 'Forgotten');
120
+ });
121
+ actions.appendChild(forget);
122
+
123
+ tr.appendChild(actions);
124
+
125
+ const detail = document.createElement('tr');
126
+ detail.hidden = true;
127
+ const cell = document.createElement('td');
128
+ cell.colSpan = 4;
129
+ detail.appendChild(cell);
130
+
131
+ tr.style.cursor = 'pointer';
132
+ tr.title = 'Show every version';
133
+ tr.addEventListener('click', (event) => {
134
+ if (event.target.closest('.actions')) return;
135
+ expand(entry, detail, cell);
136
+ });
137
+
138
+ return [tr, detail];
139
+ }));
140
+ empty.hidden = body.entries.length > 0;
141
+ } catch (problem) {
142
+ toast(problem.message, true);
143
+ }
144
+ }
145
+
146
+ // The newest version is on the row because it is the one most people want, but
147
+ // a history nobody can walk back through is not a history, so every version is
148
+ // here. They are fetched per row rather than with the listing, which keeps the
149
+ // listing one directory walk on a host with a lot of orphans.
150
+ async function expand(entry, detail, cell) {
151
+ detail.hidden = !detail.hidden;
152
+ if (detail.hidden || cell.dataset.loaded) return;
153
+ try {
154
+ const body = await api(`/_/attic/${encodeURIComponent(entry.id)}`);
155
+ cell.replaceChildren(...body.versions.map((version) => {
156
+ const item = document.createElement('div');
157
+ item.className = 'row';
158
+
159
+ const when = document.createElement('span');
160
+ when.className = 'grow';
161
+ when.textContent = new Date(version.time).toLocaleString();
162
+ item.appendChild(when);
163
+
164
+ const size = document.createElement('span');
165
+ size.style.color = 'var(--muted)';
166
+ size.style.fontSize = '.82rem';
167
+ size.textContent = bytes(version.size);
168
+ item.appendChild(size);
169
+
170
+ const download = document.createElement('a');
171
+ download.className = 'btn';
172
+ download.textContent = 'Download';
173
+ download.href = `/_/attic/${encodeURIComponent(entry.id)}/${encodeURIComponent(version.name)}`;
174
+ item.appendChild(download);
175
+
176
+ return item;
177
+ }));
178
+ cell.dataset.loaded = '1';
179
+ } catch (problem) {
180
+ detail.hidden = true;
181
+ toast(problem.message, true);
182
+ }
183
+ }
184
+
185
+ async function run(work, message) {
186
+ try {
187
+ await work();
188
+ toast(message);
189
+ load();
190
+ } catch (problem) {
191
+ toast(problem.message, true);
192
+ }
193
+ }
194
+
195
+ load();
196
+ </script>
197
+ </body>
198
+ </html>