@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.
- package/LICENSE +221 -0
- package/README.md +82 -0
- package/package.json +52 -0
- package/src/admin/listing.js +149 -0
- package/src/admin/routes.js +362 -0
- package/src/attic.js +168 -0
- package/src/auth/can.js +79 -0
- package/src/auth/csrf.js +74 -0
- package/src/auth/none.js +24 -0
- package/src/auth/password.js +281 -0
- package/src/auth/passwords.js +79 -0
- package/src/auth/rate-limit.js +66 -0
- package/src/auth/sessions.js +86 -0
- package/src/auth/token-lanes.js +72 -0
- package/src/boot.js +120 -0
- package/src/client.js +99 -0
- package/src/collections/index.js +397 -0
- package/src/collections/routes.js +166 -0
- package/src/create-host.js +351 -0
- package/src/derived/data-extractor.js +22 -0
- package/src/derived/index.js +121 -0
- package/src/documents/format-html.js +313 -0
- package/src/documents/replace.js +257 -0
- package/src/documents/root-attrs.js +171 -0
- package/src/documents/serve.js +137 -0
- package/src/documents/stale.js +20 -0
- package/src/index.js +12 -0
- package/src/inspect.js +136 -0
- package/src/json-errors.js +59 -0
- package/src/livesync.js +75 -0
- package/src/nodes/identity.js +14 -0
- package/src/nodes/names.js +57 -0
- package/src/nodes/ops.js +613 -0
- package/src/nodes/scanner.js +321 -0
- package/src/nodes/store.js +111 -0
- package/src/pages.js +166 -0
- package/src/paths.js +302 -0
- package/src/recovery/overlay.js +327 -0
- package/src/recovery/replay.js +236 -0
- package/src/recovery-ui.js +185 -0
- package/src/recovery.js +30 -0
- package/src/requests.js +73 -0
- package/src/routes/meta.js +62 -0
- package/src/routes/read.js +105 -0
- package/src/routes/save.js +102 -0
- package/src/routes/sync.js +118 -0
- package/src/routes/upload.js +128 -0
- package/src/share/index.js +207 -0
- package/src/share/save-tokens.js +65 -0
- package/src/spec/codes.js +42 -0
- package/src/spec/meta.js +52 -0
- package/src/spec/wire.js +115 -0
- package/src/store/index.js +29 -0
- package/src/store/migrations/001-init.sql +114 -0
- package/src/store/sqlite.js +540 -0
- package/src/templates.js +50 -0
- package/src/tenants/index.js +355 -0
- package/src/tenants/isolation.js +91 -0
- package/src/tenants/routes.js +131 -0
- package/src/ui.js +95 -0
- package/src/util/cookies.js +26 -0
- package/src/util/express.js +8 -0
- package/src/util/fsx.js +205 -0
- package/src/util/id.js +37 -0
- package/src/util/lockfile.js +52 -0
- package/src/util/locks.js +35 -0
- package/src/util/multipart.js +33 -0
- package/src/versions/files.js +307 -0
- package/src/versions/index.js +16 -0
- package/src/versions/naming.js +172 -0
- package/src/versions/routes.js +91 -0
- package/src/wire-compat.js +61 -0
- package/ui/app.css +164 -0
- package/ui/attic.html +198 -0
- package/ui/dashboard.html +456 -0
- package/ui/editor.html +156 -0
- package/ui/error.html +18 -0
- package/ui/login.html +58 -0
- package/ui/records.html +173 -0
- package/ui/recovery.html +152 -0
- package/ui/setup.html +61 -0
- package/ui/share-qr.html +44 -0
- package/ui/templates/blank.html +16 -0
- package/ui/templates/devlog.html +71 -0
- package/ui/templates/hackable-dashboard.html +145 -0
- package/ui/templates/kanban.html +85 -0
- package/ui/templates/landing.html +108 -0
- package/ui/templates/writer.html +50 -0
- package/ui/tenants.html +172 -0
- package/ui/trash.html +134 -0
- package/ui/versions.html +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
First Million Stays Yours License
|
|
2
|
+
Version 1.0
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Hyperspace Systems LLC
|
|
5
|
+
|
|
6
|
+
----------------------------------------------------------------------
|
|
7
|
+
LICENSE DETAILS FOR THIS RELEASE
|
|
8
|
+
|
|
9
|
+
Software: @makerclay/core 1.0.0
|
|
10
|
+
Released: 2026-08-20
|
|
11
|
+
MIT Conversion Date: 2028-02-20 (18 months after release)
|
|
12
|
+
|
|
13
|
+
Licensor: Hyperspace Systems LLC, and its successors
|
|
14
|
+
and assigns
|
|
15
|
+
Payment, program, plain answers: https://hyperclay.com/host-program
|
|
16
|
+
Questions, notices, Coverage Certificates: license@hyperclay.com
|
|
17
|
+
SPDX identifier: LicenseRef-First-Million-Stays-Yours-1.0
|
|
18
|
+
(MIT after the Conversion Date)
|
|
19
|
+
Where to start: Section 1.
|
|
20
|
+
----------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
1. THE CONDITION, AND WHO IT NEVER REACHES
|
|
24
|
+
|
|
25
|
+
This license gives You every right the MIT License gives You,
|
|
26
|
+
granted in Section 2, on one condition beyond MIT's: if products
|
|
27
|
+
and services that contain the Software, derive from it, or provide
|
|
28
|
+
its functionality to others by running it bring You, together with
|
|
29
|
+
every entity under common control with You, more than
|
|
30
|
+
US $1,000,000 in a calendar year, You pay Licensor 3% of the
|
|
31
|
+
amount above US $1,000,000. Almost nobody owes it. Where the
|
|
32
|
+
Software is one component of something larger, only the share
|
|
33
|
+
fairly attributable to it counts.
|
|
34
|
+
|
|
35
|
+
Under that line You owe nothing and condition (b) of Section 2 is
|
|
36
|
+
satisfied for the year with no act on Your part: nothing to pay,
|
|
37
|
+
report, register, or notify. Keep the copyright notice in copies,
|
|
38
|
+
as condition (a) requires, and You are finished with this license.
|
|
39
|
+
The line is tested year by year, so look again once a year. If You
|
|
40
|
+
are not sure whether something You sell is covered, Section 5
|
|
41
|
+
makes the answer free and binding within 30 days.
|
|
42
|
+
|
|
43
|
+
Crossing the line never makes the first US $1,000,000 payable,
|
|
44
|
+
never reaches back into an earlier year, and never ends this
|
|
45
|
+
license. Sections 4 through 6 say how Covered Revenue is counted
|
|
46
|
+
and the fee is paid. This section describes the condition; Section
|
|
47
|
+
2 states it and Sections 4 through 6 measure it. Where this
|
|
48
|
+
section and those differ, those govern, and nothing in this
|
|
49
|
+
section creates an obligation they do not.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
2. GRANT AND CONDITIONS
|
|
53
|
+
|
|
54
|
+
Permission is hereby granted, free of charge except as condition (b)
|
|
55
|
+
below provides, to any person obtaining a copy of this software and
|
|
56
|
+
associated documentation files (the "Software"), to deal in the
|
|
57
|
+
Software without restriction, including without limitation the rights
|
|
58
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
59
|
+
sell copies of the Software, and to permit persons to whom the
|
|
60
|
+
Software is furnished to do so, subject to the following conditions:
|
|
61
|
+
|
|
62
|
+
(a) The above copyright notice and this permission notice shall be
|
|
63
|
+
included in all copies or substantial portions of the Software.
|
|
64
|
+
|
|
65
|
+
(b) If in a calendar year the Covered Revenue of You and every
|
|
66
|
+
entity under common control with You exceeds US $1,000,000,
|
|
67
|
+
You shall pay Licensor 3% of the amount above US $1,000,000,
|
|
68
|
+
as Section 6 provides. "Covered Revenue" is receipts from
|
|
69
|
+
products and services You supply that themselves contain the
|
|
70
|
+
Software, are derived from it, or provide its functionality
|
|
71
|
+
to others by running it, less refunds, chargebacks, and
|
|
72
|
+
transaction taxes, attributed as Section 4 provides.
|
|
73
|
+
Otherwise this condition is satisfied for the year with no
|
|
74
|
+
act on Your part.
|
|
75
|
+
|
|
76
|
+
Both (a) and (b) are conditions of this grant, not independent
|
|
77
|
+
covenants. Exercising the rights granted here without satisfying
|
|
78
|
+
them, subject to the cure and good-faith provisions of Section 6, is
|
|
79
|
+
outside the scope of this license.
|
|
80
|
+
|
|
81
|
+
In this license, the "Software" is the version of the work identified
|
|
82
|
+
in the License Details above, in source or binary form, including
|
|
83
|
+
modifications of it and works derived from it; a work is derived
|
|
84
|
+
from the Software only if it would infringe copyright in the
|
|
85
|
+
Software without this license; the "Package" is the software line of
|
|
86
|
+
which this Software is one version; and "You" means the person or
|
|
87
|
+
legal entity exercising these rights.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
3. CONVERSION TO THE MIT LICENSE
|
|
91
|
+
|
|
92
|
+
On the MIT Conversion Date stated above, this version of the
|
|
93
|
+
Software becomes licensed to everyone under the MIT License,
|
|
94
|
+
automatically and without notice or any further act. That happens by
|
|
95
|
+
operation of this text. On that date:
|
|
96
|
+
|
|
97
|
+
(a) in Section 2, the words "free of charge except as condition
|
|
98
|
+
(b) below provides" read simply "free of charge";
|
|
99
|
+
|
|
100
|
+
(b) in Section 2, condition (b), the label "(a)", the paragraph
|
|
101
|
+
beginning "Both (a) and (b)", and the paragraph beginning "In
|
|
102
|
+
this license" are deleted;
|
|
103
|
+
|
|
104
|
+
(c) Section 1 and Sections 4 through 6 cease to have effect;
|
|
105
|
+
|
|
106
|
+
(d) this Section 3 ceases to have effect, the conversion it makes
|
|
107
|
+
having already taken effect irrevocably; and
|
|
108
|
+
|
|
109
|
+
(e) the License Details block, the section headings, and the
|
|
110
|
+
section numbering become a record of this release and have no
|
|
111
|
+
operative effect.
|
|
112
|
+
|
|
113
|
+
What then remains, Section 2 as amended and the disclaimer in
|
|
114
|
+
Section 7, under the copyright notice above, is the MIT License
|
|
115
|
+
(SPDX identifier: MIT), word for word. From that date copies of this
|
|
116
|
+
version may also carry the standard MIT License text in place of
|
|
117
|
+
this document.
|
|
118
|
+
|
|
119
|
+
This conversion is automatic, unconditional, and irrevocable, and
|
|
120
|
+
binds Licensor and its successors and assigns. It takes effect
|
|
121
|
+
whether or not Licensor republishes this version under a separate
|
|
122
|
+
MIT license file.
|
|
123
|
+
|
|
124
|
+
The same conversion happens immediately, without notice or any
|
|
125
|
+
further act, if for any period of 12 consecutive months Licensor
|
|
126
|
+
neither makes a public release of the Package nor provides any
|
|
127
|
+
security response to a responsibly reported vulnerability in it, or
|
|
128
|
+
if Licensor ceases business operations without a successor that
|
|
129
|
+
assumes this license.
|
|
130
|
+
|
|
131
|
+
The terms of this license, including the MIT Conversion Date, are
|
|
132
|
+
fixed for this version of the Software at its release and will never
|
|
133
|
+
change. Later versions of the Package may ship under revised terms;
|
|
134
|
+
revised terms apply only to those later versions, never
|
|
135
|
+
retroactively.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
4. COUNTING COVERED REVENUE
|
|
139
|
+
|
|
140
|
+
"Control" means (i) the power, direct or indirect, to cause the
|
|
141
|
+
direction or management of an entity, whether by contract or
|
|
142
|
+
otherwise, (ii) ownership of fifty percent (50%) or more of an
|
|
143
|
+
entity's outstanding shares, or (iii) beneficial ownership of an
|
|
144
|
+
entity. Aggregation across common control exists solely to prevent
|
|
145
|
+
splitting one business across entities to multiply the
|
|
146
|
+
US $1,000,000 exemption; there is no other affiliate test in this
|
|
147
|
+
license. Aggregated Covered Revenue is counted once, and one
|
|
148
|
+
self-certification and payment by any of the aggregated entities
|
|
149
|
+
discharges condition (b) and Section 6 for all of them.
|
|
150
|
+
|
|
151
|
+
The fee is charged at most once for a product or service. It never
|
|
152
|
+
stacks across Licensor's other packages under this License used
|
|
153
|
+
together in one offering, across versions of the Software, or across
|
|
154
|
+
copies and instances, and revenue counted once under this or any other
|
|
155
|
+
First Million Stays Yours License is never counted again.
|
|
156
|
+
|
|
157
|
+
If the Software, a work derived from it, or a product or service
|
|
158
|
+
that contains it is one component of a larger product, service,
|
|
159
|
+
bundle, or subscription, whether or not that component is
|
|
160
|
+
separately priced or sold, its Covered Revenue is the portion of
|
|
161
|
+
the receipts fairly attributable to that component: its actual
|
|
162
|
+
standalone price if You also sell it on a standalone basis;
|
|
163
|
+
otherwise, any reasonable allocation method You apply consistently
|
|
164
|
+
in Your own financial statements or management accounts. An
|
|
165
|
+
allocation that complies with this paragraph binds Licensor for the
|
|
166
|
+
calendar year it covers, and allocated Covered Revenue never
|
|
167
|
+
exceeds the actual receipts of the whole.
|
|
168
|
+
|
|
169
|
+
Where the customer holds its own copy of the Software under this
|
|
170
|
+
license, Your receipts for installing, configuring, or maintaining
|
|
171
|
+
that copy are not Covered Revenue.
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
5. COVERAGE CERTIFICATES
|
|
175
|
+
|
|
176
|
+
At any time and at no charge, You may send Licensor, at the address
|
|
177
|
+
above, a complete and accurate written description of an offering
|
|
178
|
+
and, if applicable, a proposed allocation. Licensor shall respond
|
|
179
|
+
within 30 days with a written determination of whether, and to what
|
|
180
|
+
extent, receipts from the offering are Covered Revenue and whether
|
|
181
|
+
the allocation complies with Section 4. Silence for 30 days accepts
|
|
182
|
+
the request as submitted, except as to facts the request omitted or
|
|
183
|
+
misstated.
|
|
184
|
+
|
|
185
|
+
A determination, including a deemed acceptance, binds Licensor and
|
|
186
|
+
its successors for the versions of the Package it names, for as long
|
|
187
|
+
as the material facts described in the request remain accurate.
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
6. REPORTING, PAYMENT, AUDIT, AND CURE
|
|
191
|
+
|
|
192
|
+
If a fee is due under condition (b) for a calendar year, You shall,
|
|
193
|
+
within 90 days after the end of that year, send Licensor a
|
|
194
|
+
self-certification stating the Covered Revenue and the fee due, and
|
|
195
|
+
pay the fee through the address or URL in the License Details
|
|
196
|
+
above.
|
|
197
|
+
|
|
198
|
+
Licensor may audit the calculation at most once per calendar year,
|
|
199
|
+
at Licensor's expense, through an independent accountant under
|
|
200
|
+
confidentiality, on 30 days' written notice. If an audit finds
|
|
201
|
+
underpayment exceeding 5% of the amount due, You shall also
|
|
202
|
+
reimburse the reasonable cost of that audit.
|
|
203
|
+
|
|
204
|
+
A good-faith underpayment, late report, or late payment does not
|
|
205
|
+
terminate this license and is not copyright infringement; any
|
|
206
|
+
unpaid amount remains a debt owed to Licensor. If You fail to pay
|
|
207
|
+
or report as this section requires, Licensor shall give You written
|
|
208
|
+
notice, and You have 30 days from that notice to cure. Only a
|
|
209
|
+
failure to cure within those 30 days is a failure of condition (b).
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
7. DISCLAIMER
|
|
213
|
+
|
|
214
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
215
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
216
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
217
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
218
|
+
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
219
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
220
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
221
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @makerclay/core
|
|
2
|
+
|
|
3
|
+
> **License: First Million Stays Yours License.** Free to use, no matter who you are or what you use it for. It only ever costs money if you sell or host a product built on this software and that product makes you more than $1M in a year. Under that line you owe nothing, sign nothing, register nowhere.
|
|
4
|
+
>
|
|
5
|
+
> | Free under | Above it | Becomes MIT |
|
|
6
|
+
> |---|---|---|
|
|
7
|
+
> | $1M/year from products built on it | 3% of the excess | 2028-02-20 (this version) |
|
|
8
|
+
>
|
|
9
|
+
> Plain answers: [hyperclay.com/host-program](https://hyperclay.com/host-program) · binding text in [LICENSE](LICENSE) · questions: license@hyperclay.com
|
|
10
|
+
|
|
11
|
+
The host for malleable HTML files, as an embeddable Express app.
|
|
12
|
+
|
|
13
|
+
This is the engine behind [`makerclay`](https://www.npmjs.com/package/makerclay).
|
|
14
|
+
If you want to run a server, install that instead. Install this when you are
|
|
15
|
+
building the server: a desktop app, a different CLI, a host with its own auth.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import express from 'express';
|
|
19
|
+
import { createHost, createSqliteStore } from '@makerclay/core';
|
|
20
|
+
import { ownerPassword } from '@makerclay/core/auth/password';
|
|
21
|
+
|
|
22
|
+
const host = createHost({
|
|
23
|
+
data: './my-data',
|
|
24
|
+
store: createSqliteStore(),
|
|
25
|
+
auth: ownerPassword(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await host.ready;
|
|
29
|
+
express().use(host.app).listen(4600);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`host.app` is an Express app. `host.ready` resolves once the boot scan and any
|
|
33
|
+
crash recovery have finished. `host.close()` releases the single-writer lock.
|
|
34
|
+
|
|
35
|
+
## What `createHost` gives you
|
|
36
|
+
|
|
37
|
+
Everything under `/_/` (dashboard, editor, versions, trash, uploads, share links,
|
|
38
|
+
collections, signups, the spec's `/_/meta` and `/_/save` lanes), plus document
|
|
39
|
+
serving for everything else. It owns one directory:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
<data>/files/ documents
|
|
43
|
+
<data>/tenants/ one folder per signup
|
|
44
|
+
<data>/versions/ history, keyed by document identity
|
|
45
|
+
<data>/trash/ soft deletes
|
|
46
|
+
<data>/records/ collection submissions
|
|
47
|
+
<data>/makerclay.db a sparse overlay
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**The store is an overlay, not a mirror.** Rows exist only for state a file cannot
|
|
51
|
+
hold: which document is private, which is trashed, who is signed in. Lose the
|
|
52
|
+
database and the overlay rebuilds itself from the durable records under
|
|
53
|
+
`versions/`, minus the credentials, which are reissued rather than invented. With
|
|
54
|
+
state on disk and no records left to read it by, the host refuses to serve.
|
|
55
|
+
|
|
56
|
+
## The pieces you can replace
|
|
57
|
+
|
|
58
|
+
| Option | What it is |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `auth` | An identity provider. `none()` makes every request the owner (embedded/desktop). `ownerPassword()` is the single-owner password provider with first-run setup. Write your own: it needs `identify(req)` and, optionally, `routes()`. |
|
|
61
|
+
| `store` | The metadata overlay. `createSqliteStore()` uses the built-in `node:sqlite`. `null` is supported and turns off everything that needs a row. |
|
|
62
|
+
| `isolation` | `off`, `untrusted` (the default: tenant documents and any file the host never saved) or `all`. |
|
|
63
|
+
| `features` | Switches for the dashboard, editor, uploads, collections, share links, signups. |
|
|
64
|
+
| `wire` | Compatibility with older clients. See `src/wire-compat.js`, which names the deployed client behind every shim. |
|
|
65
|
+
|
|
66
|
+
Also exported: `can()` and the capability matrix, the path resolver, the spec's
|
|
67
|
+
etag and header helpers, the root-attribute reader and writer, and `ulid()`.
|
|
68
|
+
|
|
69
|
+
## Invariants it will not let you break
|
|
70
|
+
|
|
71
|
+
- Every document mutation goes through `host.documents.replace`. Nothing else
|
|
72
|
+
writes a managed file.
|
|
73
|
+
- The document root is never served statically. Every read passes the gate.
|
|
74
|
+
- Serving never mutates a stored file. Serve-time injection happens on the
|
|
75
|
+
response bytes, and both injected attributes are stripped on the way back in.
|
|
76
|
+
- A response on any save lane is a JSON object, on every status.
|
|
77
|
+
|
|
78
|
+
## Requirements
|
|
79
|
+
|
|
80
|
+
Node 22.13 or newer, for `node:sqlite`, which is behind a flag before then.
|
|
81
|
+
|
|
82
|
+
First Million Stays Yours License; see the matrix at the top of this README and the LICENSE file.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@makerclay/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The embeddable single-tenant host for malleable HTML files",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./paths": "./src/paths.js",
|
|
11
|
+
"./store/sqlite": "./src/store/sqlite.js",
|
|
12
|
+
"./auth/password": "./src/auth/password.js",
|
|
13
|
+
"./auth/none": "./src/auth/none.js",
|
|
14
|
+
"./auth/can": "./src/auth/can.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src/",
|
|
18
|
+
"ui/",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@panphora/clayjs": "^1.2.0",
|
|
27
|
+
"busboy": "^1.6.0",
|
|
28
|
+
"cheerio": "1.2.0",
|
|
29
|
+
"chokidar": "^4.0.0",
|
|
30
|
+
"codemirror": "^5.65.21",
|
|
31
|
+
"express": "^4.21.2",
|
|
32
|
+
"hyper-html-api": "0.8.0",
|
|
33
|
+
"js-beautify": "^1.15.1",
|
|
34
|
+
"livesync-hyperclay": "^0.15.1",
|
|
35
|
+
"qrcode-generator": "^1.5.2",
|
|
36
|
+
"upath": "^2.0.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"tailwind-hyperclay": "*"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"tailwind-hyperclay": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.13.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"prepublishOnly": "python3 ../../scripts/ablation-check.py LICENSE"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from 'upath';
|
|
2
|
+
import { statIfExists, readdirIfExists } from '../util/fsx.js';
|
|
3
|
+
import { isDocumentPath, isReservedNodePath } from '../paths.js';
|
|
4
|
+
|
|
5
|
+
// The `/_/nodes` serializer. Ordering and formatting are ported from the
|
|
6
|
+
// platform's dashboard-nodes.js; the data model underneath is different, and
|
|
7
|
+
// deliberately so: the DIRECTORY is the truth and the store only decorates it.
|
|
8
|
+
// List a folder with the db deleted and you still get every file, just without
|
|
9
|
+
// the private/shared badges.
|
|
10
|
+
|
|
11
|
+
const SORTS = new Set(['name', 'modified', 'size', 'kind']);
|
|
12
|
+
|
|
13
|
+
function formatSize(bytes) {
|
|
14
|
+
if (bytes === null || bytes === undefined) return '';
|
|
15
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
16
|
+
const units = ['KB', 'MB', 'GB'];
|
|
17
|
+
let value = bytes / 1024;
|
|
18
|
+
let unit = 0;
|
|
19
|
+
while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; }
|
|
20
|
+
return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildBreadcrumbs(dir) {
|
|
24
|
+
const crumbs = [{ name: 'Home', path: '' }];
|
|
25
|
+
if (!dir) return crumbs;
|
|
26
|
+
const segments = dir.split('/').filter(Boolean);
|
|
27
|
+
let accumulated = '';
|
|
28
|
+
for (const segment of segments) {
|
|
29
|
+
accumulated = accumulated ? `${accumulated}/${segment}` : segment;
|
|
30
|
+
crumbs.push({ name: segment, path: accumulated });
|
|
31
|
+
}
|
|
32
|
+
return crumbs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Folders always lead, whatever the sort. A file browser that interleaves them
|
|
36
|
+
// makes the tree unreadable, and every listing anyone has ever liked does this.
|
|
37
|
+
function sortEntries(entries, sort = 'name', direction = 'asc') {
|
|
38
|
+
const key = SORTS.has(sort) ? sort : 'name';
|
|
39
|
+
const sign = direction === 'desc' ? -1 : 1;
|
|
40
|
+
const compare = {
|
|
41
|
+
name: (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
|
|
42
|
+
modified: (a, b) => (a.modified || 0) - (b.modified || 0),
|
|
43
|
+
size: (a, b) => (a.size || 0) - (b.size || 0),
|
|
44
|
+
kind: (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
|
|
45
|
+
}[key];
|
|
46
|
+
|
|
47
|
+
return [...entries].sort((a, b) => {
|
|
48
|
+
if (a.kind === 'dir' && b.kind !== 'dir') return -1;
|
|
49
|
+
if (b.kind === 'dir' && a.kind !== 'dir') return 1;
|
|
50
|
+
return compare(a, b) * sign || a.name.localeCompare(b.name);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createListing({ paths, nodes, isolation = null, store = null }) {
|
|
55
|
+
function decorate(entry, row) {
|
|
56
|
+
if (!row) return entry;
|
|
57
|
+
return {
|
|
58
|
+
...entry,
|
|
59
|
+
id: row.id,
|
|
60
|
+
private: !!row.private,
|
|
61
|
+
signups: !!row.signups,
|
|
62
|
+
isolated: isolation ? isolation.isolatedFor(row) : false,
|
|
63
|
+
shared: store ? store.listShares(row.id).filter((share) => !share.revokedAt).length : 0,
|
|
64
|
+
collection: store ? (store.getCollection(row.id)?.slug ?? null) : null,
|
|
65
|
+
etag: row.etag ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function list({ owner = '', dir = '', q = '', sort = 'name', direction = 'asc' } = {}) {
|
|
70
|
+
let target = paths.realmRoot(owner);
|
|
71
|
+
if (dir) {
|
|
72
|
+
// The same resolution every other reader uses, rather than a bare join:
|
|
73
|
+
// it refuses `..`, dotfiles, an absolute path and any symlink that leaves
|
|
74
|
+
// the realm. A join would answer `?dir=../../../../etc` with a listing of
|
|
75
|
+
// the machine.
|
|
76
|
+
try {
|
|
77
|
+
target = await paths.resolveRead(owner, dir);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const stat = await statIfExists(target);
|
|
83
|
+
if (!stat?.isDirectory()) return null;
|
|
84
|
+
|
|
85
|
+
const needle = String(q || '').trim().toLowerCase();
|
|
86
|
+
const entries = [];
|
|
87
|
+
for (const item of await readdirIfExists(target, { withFileTypes: true })) {
|
|
88
|
+
if (item.name.startsWith('.')) continue;
|
|
89
|
+
// A symlink inside a managed tree is refused everywhere else, so listing
|
|
90
|
+
// one would advertise a file that 404s and leak its target's size.
|
|
91
|
+
if (item.isSymbolicLink()) continue;
|
|
92
|
+
if (needle && !item.name.toLowerCase().includes(needle)) continue;
|
|
93
|
+
const full = path.join(target, item.name);
|
|
94
|
+
const info = await statIfExists(full);
|
|
95
|
+
if (!info) continue;
|
|
96
|
+
const relPath = dir ? `${dir}/${item.name}` : item.name;
|
|
97
|
+
const kind = info.isDirectory() ? 'dir' : (isDocumentPath(relPath) ? 'doc' : 'file');
|
|
98
|
+
entries.push(decorate({
|
|
99
|
+
name: item.name,
|
|
100
|
+
path: relPath,
|
|
101
|
+
kind,
|
|
102
|
+
size: info.isDirectory() ? null : info.size,
|
|
103
|
+
sizeLabel: info.isDirectory() ? '' : formatSize(info.size),
|
|
104
|
+
modified: Math.round(info.mtimeMs),
|
|
105
|
+
url: `${owner ? `/~${owner}` : ''}/${relPath}`,
|
|
106
|
+
}, nodes.get(owner, relPath)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
owner,
|
|
111
|
+
dir,
|
|
112
|
+
breadcrumbs: buildBreadcrumbs(dir),
|
|
113
|
+
sort,
|
|
114
|
+
direction,
|
|
115
|
+
q: String(q || ''),
|
|
116
|
+
entries: sortEntries(entries, sort, direction),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function trash() {
|
|
121
|
+
if (!store) return { entries: [] };
|
|
122
|
+
// A deleted collection record is a row in here too, but it is not a file and
|
|
123
|
+
// it comes back through the collection API, not this view.
|
|
124
|
+
const rows = store.listTrash().filter((row) => !isReservedNodePath(row.path));
|
|
125
|
+
const entries = [];
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
// A child of a trashed folder went in with its parent and comes back with
|
|
128
|
+
// it, so only the roots are offered.
|
|
129
|
+
const parent = path.dirname(row.path);
|
|
130
|
+
if (parent !== '.' && rows.some((other) => other.id !== row.id && other.path === parent)) continue;
|
|
131
|
+
const stored = paths.trashPath(row.id, path.basename(row.path));
|
|
132
|
+
const info = await statIfExists(stored);
|
|
133
|
+
entries.push({
|
|
134
|
+
id: row.id,
|
|
135
|
+
name: path.basename(row.path),
|
|
136
|
+
path: row.path,
|
|
137
|
+
owner: row.owner,
|
|
138
|
+
kind: row.kind,
|
|
139
|
+
deletedAt: row.deletedAt,
|
|
140
|
+
size: info && !info.isDirectory() ? info.size : null,
|
|
141
|
+
sizeLabel: info && !info.isDirectory() ? formatSize(info.size) : '',
|
|
142
|
+
recoverable: !!info,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { entries };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { list, trash };
|
|
149
|
+
}
|