yamlite 0.40.0 → 0.41.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/README.md +19 -17
- package/dist/cli.mjs +2 -2
- package/dist/core.d.mts +12 -4
- package/dist/core.mjs +2 -2
- package/dist/{index-CDLxve55.d.mts → index-Bd2ctbJm.d.mts} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-NOJw_gTE.mjs → src-ChJZPkg9.mjs} +228 -39
- package/dist/ui/assets/{ErdView-Dqs3Ly-E.js → ErdView-CWmAQgo6.js} +1 -1
- package/dist/ui/assets/{MarkdownField-Bg4zDZM_.js → MarkdownField-DI3OMg7I.js} +1 -1
- package/dist/ui/assets/PageView-BsEhcE48.js +1 -0
- package/dist/ui/assets/access-DV6K5419.js +1 -0
- package/dist/ui/assets/index-emolmw-f.js +126 -0
- package/dist/ui/assets/{load-ylhnQAWa.js → load-BsZuQtRZ.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/ui-embed/{ErdView-CgNnAf5I.js → ErdView-CmUxHybj.js} +2 -2
- package/dist/ui-embed/{HintPopover-8CeJLQO5.js → HintPopover-NOWaUpA_.js} +1622 -1612
- package/dist/ui-embed/PageView-Bptfb1IR.js +1154 -0
- package/dist/ui-embed/{activity-DecWPHxS.js → activity-v3qF9H6V.js} +1 -1
- package/dist/ui-embed/embed.js +5 -5
- package/dist/ui-embed/{pageFrame-ClOBfTt9.js → pageFrame-vCls4cAs.js} +1 -1
- package/dist/{workspace-Cp5-yHYp.mjs → workspace-D7xHlBPc.mjs} +75 -7
- package/package.json +1 -1
- package/dist/ui/assets/PageView-D0hs43d8.js +0 -1
- package/dist/ui/assets/access-DSnHSuXB.js +0 -1
- package/dist/ui/assets/index-LbUt_Zkt.js +0 -126
- package/dist/ui-embed/PageView-B29tXeqW.js +0 -835
package/README.md
CHANGED
|
@@ -255,28 +255,29 @@ pages:
|
|
|
255
255
|
access: { tasks: write, people: read } # tables and views; nothing else is reachable
|
|
256
256
|
sql: false # true allows SELECT over the names in access
|
|
257
257
|
network: [] # origins the page may load scripts, styles, images and fonts from, and connect to
|
|
258
|
-
files:
|
|
258
|
+
files: { .images/: read, .uploads/: write } # files the page may read (yamlite.files.read) or also write (yamlite.files.write); a folder ends in /
|
|
259
259
|
ai: true # lets the page call the host's AI model; { maxOutputTokens: 2048 } raises the default 1024 (at most 8192)
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
Start from [`examples/pages/kanban.html`](examples/pages/kanban.html): copy it into `.pages/`, set `TABLE`, `KEY`, `GROUP` and `TITLE` at the top, and declare it as above. [`examples/pages/summary.html`](examples/pages/summary.html) shows a page that asks AI to summarize a record. [`examples/pages/gallery.html`](examples/pages/gallery.html) lists a table's `image` column and shows each file with `yamlite.
|
|
262
|
+
Start from [`examples/pages/kanban.html`](examples/pages/kanban.html): copy it into `.pages/`, set `TABLE`, `KEY`, `GROUP` and `TITLE` at the top, and declare it as above. [`examples/pages/summary.html`](examples/pages/summary.html) shows a page that asks AI to summarize a record. [`examples/pages/gallery.html`](examples/pages/gallery.html) lists a table's `image` column and shows each file with `yamlite.files.read`.
|
|
263
263
|
|
|
264
264
|
To share a page with people who should see only that page, open it and click **Copy app link**. The link ends in `/-/apps/<name>/` and shows the page alone, with its own title and look: no sidebar, tables, SQL console, command menu or links into other records. Once there are edits, a bar at the bottom shows their count and **Send for review**; an embedding host can add its own controls at the left of that bar with the `appFooter` mount option. The part after `#` is where the page stands inside itself, which it sets with `yamlite.history`, so a reload, the back button and a copied address return there; changing it does not leave the app. It leaves the editor out of sight; it does not take it away, so anyone who removes `-/apps/<name>/` from the address can still reach the rest of the UI. With `serve`, the copied link has no token, and the login cookie is not sent when a link is opened from another site such as mail or chat: share `/-/apps/<name>/?token=…` instead, on the origin of the URL `serve` printed and with its token. `serve` is meant for trying an app out; to share one with a team, use a host that gives each viewer their own branch. An `export` has the same entry at `-/apps/<name>/` for each page it includes; open it with the trailing slash (static hosts add it), since without it the page's relative paths resolve one folder too high.
|
|
265
265
|
|
|
266
|
-
| Call
|
|
267
|
-
|
|
|
268
|
-
| `await yamlite.ready`
|
|
269
|
-
| `yamlite.rows(table, { filter, sort, search, limit, offset })`
|
|
270
|
-
| `yamlite.get(table, key)`
|
|
271
|
-
| `yamlite.create(table, key, values)` / `update(table, key, values, base)` / `remove(table, key)` | needs `write`; `update` fails with status 409, `extra.stale` (the fields) and `extra.current` (
|
|
272
|
-
| `yamlite.sql(select)`
|
|
273
|
-
| `await yamlite.
|
|
274
|
-
| `yamlite.
|
|
275
|
-
| `yamlite.
|
|
276
|
-
| `yamlite.
|
|
277
|
-
| `yamlite.ai.
|
|
278
|
-
| `yamlite.
|
|
279
|
-
| `yamlite.
|
|
266
|
+
| Call | Does |
|
|
267
|
+
| -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
268
|
+
| `await yamlite.ready` | `{ page, title, access, sql, network, files, readOnly, ai }`; `ai` is `{ provider, models }` when the page has `ai` and the host holds a key, else `null` |
|
|
269
|
+
| `yamlite.rows(table, { filter, sort, search, limit, offset })` | `{ rows, total }`, reads tables and views, same filters and search as the table screen; `limit` ≤ 500 |
|
|
270
|
+
| `yamlite.get(table, key)` | `{ row, file }`; tables only, a view gives 400 |
|
|
271
|
+
| `yamlite.create(table, key, values)` / `update(table, key, values, { base })` / `remove(table, key, { base })` | needs `write`; `base` is the record as the page last read it, or the fields of it the page relies on. `update` fails with status 409, `extra.stale` (the fields) and `extra.current` (the record now) if a field it saves changed since `base`; `remove` does if any field in `base` changed. Leaving `base` out skips the check |
|
|
272
|
+
| `yamlite.sql(select)` | needs `sql: true`; one SELECT over the names in `access` |
|
|
273
|
+
| `await yamlite.files.read(path)` | needs `path` to match one of the page's `files` entries (`read` or `write`) and not be a denied path; resolves to a `Blob` typed by extension (`image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/svg+xml`, `application/pdf`, `text/plain`, otherwise `application/octet-stream`), for `URL.createObjectURL`. Files on the branch or commit the viewer has open, at most 10 MB; 403 when `path` matches no entry, 404 when there is no such file, 413 over the limit |
|
|
274
|
+
| `await yamlite.files.write(path, blob, { base })` / `files.delete(path, { base })` | needs `path` to fall under a `write` entry; `write` stores a PNG, JPEG, GIF, WebP or PDF file (the bytes must match the extension, at most 10 MB), creating its folders, and resolves to `{ sha256 }`; `delete` removes the file. `base` is the `Blob` the page last got from `files.read`, the `sha256` its last `files.write` returned, or `null` when it expects no file there; when the file has changed since, the call fails with status 409 and `extra.current` (the SHA-256 of the content now; absent when the file is over 10 MB). Leaving `base` out skips the check |
|
|
275
|
+
| `yamlite.on("change", ({ tables, files }) => …)` | called after a sync changes a table or view the page can read (`tables`), or after a file under the page's `files` changes on disk (`files`, root-relative paths), whoever changed it |
|
|
276
|
+
| `yamlite.theme` / `yamlite.on("theme", ({ theme }) => …)` | the current theme (`light` or `dark`), and a call when it changes |
|
|
277
|
+
| `yamlite.ai.generate({ system, prompt, schema, model, maxOutputTokens })` | needs `ai` (a page without it gets 403); `{ text }`, or `{ object }` matching `schema` (a JSON Schema). `prompt` is a string or a list of `{ role: "user" \| "assistant", content }`; `model` is one of `ready`'s `ai.models`, the first by default; `maxOutputTokens` may lower the page's limit. Runs on the host's key, never one from the page. Fails with 503 and `extra.code: "no_key"` when the host has no key, 429 with `extra.retryAfter` (seconds) when busy, and 502 when the provider fails: `extra.provider` is the provider id and the provider's own text is in `message` |
|
|
278
|
+
| `yamlite.ai.stream({ system, prompt, model })` | the same without `schema`, as an async iterable of text deltas: `for await (const text of yamlite.ai.stream(…))`; leaving the loop early cancels the call |
|
|
279
|
+
| `yamlite.open(table, key)` | opens the record in YAMLite's own form; tables only, a view gives 400 |
|
|
280
|
+
| `yamlite.history` | where the page stands inside itself, with the `history` package's shape (`location`, `push`, `replace`, `go`, `back`, `forward`, `listen`, `createHref`) that routers take; in an app it is kept after the `#` of the address, and `back` and `go` stay within the app. `push(to, state)` and `replace(to, state)` throw for a path outside the page or longer than 2048 characters; `state` must be structured-cloneable, and comes back as `location.state` on back and forward, and as JSON after a reload |
|
|
280
281
|
|
|
281
282
|
Errors reject with `yamlite.YamliteError` (`status`, `message`, `extra`).
|
|
282
283
|
|
|
@@ -305,7 +306,8 @@ The variable and class names are part of the page API: renaming one is a breakin
|
|
|
305
306
|
- `network` is enforced with a Content Security Policy. It limits what the page loads and where it connects. Declaring an origin lets the page send data, including anything it can read through `access`, off your machine to that origin.
|
|
306
307
|
- `ai` lets the page send anything it can read through `access` to the host's AI provider, billed to the host's key.
|
|
307
308
|
- Every page, even with `network: []`, can leak data by navigating its own frame to another site with the data in the URL. YAMLite then disconnects the page, but the data has already left. Only declare pages you trust, as you would any script you run.
|
|
308
|
-
- `files` is a
|
|
309
|
+
- `files` is a grant like `access`: each entry is `read` or `write`, the page can read every file under a declared folder, and anything a page can read can leave through navigation, as for records. An entry ending in `/` is a folder, any other entry one exact file; `..`, absolute paths and entries inside `.yamlite/` or naming `yamlite.yaml` are config errors, as is any level but `read` or `write`. A page cannot read YAMLite's state folder, the database (and its `-wal`, `-shm` and `-journal` files) or the config files, whatever `files` says: a broad folder entry still gets 403 for them. Symlinks are followed and must stay inside a granted path.
|
|
310
|
+
- A `write` entry lets the page add, replace and delete files under it, but only pictures and PDFs whose bytes match their extension: never HTML, SVG or anything else that could run script where the files are later served. A page cannot write YAMLite's own files, or through a symlinked file; a symlinked folder must lead to a path the page may write. Written files land in the working tree like any other edit, so **Send for review** lists them. Grant `write` only on folders that hold nothing but what the page should manage.
|
|
309
311
|
- `serve` reads the working tree, so a `files` entry also covers files git does not track, such as a local `.env` or build output. Declare folders that hold only what the page should show.
|
|
310
312
|
- In an [export](#export) pages are read-only, and `access` does not hide data: the exported `db.sqlite` is downloadable, and `sql: true` reads every exported table. Pages whose `access` names a table left out of the export are skipped with a warning. An export also copies the files a page declares into `data/files/`, each stored with a `.bin` suffix so a host serves it as a download rather than as a page; deploying the export makes them public; a file over 10 MB is skipped with a warning.
|
|
311
313
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { B as check, I as createHandler, R as isLoopback, a as query, c as exportSite, i as formatTable, n as formatCsv, o as generateConfig, r as formatJson, s as init, t as open, z as loopbackHosts } from "./src-
|
|
3
|
-
import { n as guide, r as envKeys, t as createWorkspace } from "./workspace-
|
|
2
|
+
import { B as check, I as createHandler, R as isLoopback, a as query, c as exportSite, i as formatTable, n as formatCsv, o as generateConfig, r as formatJson, s as init, t as open, z as loopbackHosts } from "./src-ChJZPkg9.mjs";
|
|
3
|
+
import { n as guide, r as envKeys, t as createWorkspace } from "./workspace-D7xHlBPc.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { relative, resolve } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
package/dist/core.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as parseConfig, B as HistoryEntry, C as Decision, D as OpenOptions, E as DeclaredTable, F as ColumnType, G as Rec, H as IndexSpec, I as ConflictInfo, J as ViewRecord, K as SchemaChange, L as DbRow, M as Capture, N as Change, O as TableInput, P as ChangeOp, R as DbValue, S as BaseHashes, T as Config, U as PageAccess, V as HistoryPage, W as PageSpec, _ as check, a as formatCsv, b as Watcher, c as query, d as init, f as ExportOptions, g as CheckTable, h as CheckResult, i as QueryResult, j as readsFile, k as TableLocation, l as InitOptions, m as exportSite, n as open, o as formatJson, p as ExportResult, q as TableSpec, r as QueryOptions, s as formatTable, t as Yamlite, u as generateConfig, v as WatchHandlers, w as CONFIG_NAMES, x as TableResult, y as WatchOptions, z as FieldChange } from "./index-
|
|
1
|
+
import { A as parseConfig, B as HistoryEntry, C as Decision, D as OpenOptions, E as DeclaredTable, F as ColumnType, G as Rec, H as IndexSpec, I as ConflictInfo, J as ViewRecord, K as SchemaChange, L as DbRow, M as Capture, N as Change, O as TableInput, P as ChangeOp, R as DbValue, S as BaseHashes, T as Config, U as PageAccess, V as HistoryPage, W as PageSpec, _ as check, a as formatCsv, b as Watcher, c as query, d as init, f as ExportOptions, g as CheckTable, h as CheckResult, i as QueryResult, j as readsFile, k as TableLocation, l as InitOptions, m as exportSite, n as open, o as formatJson, p as ExportResult, q as TableSpec, r as QueryOptions, s as formatTable, t as Yamlite, u as generateConfig, v as WatchHandlers, w as CONFIG_NAMES, x as TableResult, y as WatchOptions, z as FieldChange } from "./index-Bd2ctbJm.mjs";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
3
|
import { LanguageModel, ModelMessage } from "ai";
|
|
4
4
|
//#region src/store.d.ts
|
|
@@ -59,7 +59,7 @@ interface PageRules {
|
|
|
59
59
|
access: Record<string, PageAccess>;
|
|
60
60
|
sql: boolean;
|
|
61
61
|
network: string[];
|
|
62
|
-
files: string
|
|
62
|
+
files: Record<string, PageAccess>;
|
|
63
63
|
ai: {
|
|
64
64
|
maxOutputTokens: number;
|
|
65
65
|
} | null;
|
|
@@ -67,7 +67,7 @@ interface PageRules {
|
|
|
67
67
|
export declare const DENIED_FILES: string[];
|
|
68
68
|
export declare const MAX_PAGE_FILE_BYTES = 10485760;
|
|
69
69
|
export declare function normalizeFilePath(path: string): string | null;
|
|
70
|
-
export declare const fileGranted: (files: string
|
|
70
|
+
export declare const fileGranted: (files: Record<string, PageAccess>, path: string) => boolean;
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region src/agent/providers.d.ts
|
|
73
73
|
interface Credentials {
|
|
@@ -349,6 +349,10 @@ type ServeEvent = {
|
|
|
349
349
|
type: "page";
|
|
350
350
|
at: string;
|
|
351
351
|
pages: string[];
|
|
352
|
+
} | {
|
|
353
|
+
type: "files";
|
|
354
|
+
at: string;
|
|
355
|
+
paths: string[];
|
|
352
356
|
};
|
|
353
357
|
//#endregion
|
|
354
358
|
//#region src/serve/events.d.ts
|
|
@@ -364,6 +368,7 @@ declare class EventHub {
|
|
|
364
368
|
warnings(): Record<string, string[]>;
|
|
365
369
|
configError(): string | null;
|
|
366
370
|
pagesChanged(pages: string[]): void;
|
|
371
|
+
filesChanged(paths: string[]): void;
|
|
367
372
|
stream(signal: AbortSignal): ReadableStream<Uint8Array>;
|
|
368
373
|
close(): void;
|
|
369
374
|
private onSync;
|
|
@@ -520,10 +525,13 @@ interface ApiRequest {
|
|
|
520
525
|
type Handler = (req: ApiRequest) => unknown;
|
|
521
526
|
declare class Router {
|
|
522
527
|
private readonly routes;
|
|
523
|
-
add(method: string, pattern: string, handler: Handler
|
|
528
|
+
add(method: string, pattern: string, handler: Handler, opts?: {
|
|
529
|
+
maxBody?: number;
|
|
530
|
+
}): this;
|
|
524
531
|
match(method: string, pathname: string): {
|
|
525
532
|
handler: Handler;
|
|
526
533
|
params: Record<string, string>;
|
|
534
|
+
maxBody: number;
|
|
527
535
|
} | "method" | null;
|
|
528
536
|
dispatch(request: Request): Promise<Response>;
|
|
529
537
|
}
|
package/dist/core.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { B as check, C as describeChange,
|
|
2
|
-
import { t as createWorkspace } from "./workspace-
|
|
1
|
+
import { B as check, C as describeChange, K as DENIED_FILES, L as HttpError, T as worthShowing, U as parseConfig, V as CONFIG_NAMES, W as readsFile, X as fileGranted, Z as normalizeFilePath, a as query, c as exportSite, f as ReviewRefused, h as templatePath, i as formatTable, n as formatCsv, o as generateConfig, q as MAX_PAGE_FILE_BYTES, r as formatJson, s as init, t as open, w as fieldChanges } from "./src-ChJZPkg9.mjs";
|
|
2
|
+
import { t as createWorkspace } from "./workspace-D7xHlBPc.mjs";
|
|
3
3
|
export { CONFIG_NAMES, DENIED_FILES, HttpError, MAX_PAGE_FILE_BYTES, ReviewRefused, check, createWorkspace, describeChange, exportSite, fieldChanges, fileGranted, formatCsv, formatJson, formatTable, generateConfig, init, normalizeFilePath, open, parseConfig, query, readsFile, templatePath, worthShowing };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as Decision, D as OpenOptions, F as ColumnType, H as IndexSpec, I as ConflictInfo, K as SchemaChange, N as Change, O as TableInput, P as ChangeOp, W as PageSpec, _ as check, a as formatCsv, b as Watcher, c as query, d as init, f as ExportOptions, g as CheckTable, h as CheckResult, i as QueryResult, l as InitOptions, m as exportSite, n as open, o as formatJson, p as ExportResult, q as TableSpec, r as QueryOptions, s as formatTable, t as Yamlite, u as generateConfig, v as WatchHandlers, x as TableResult, y as WatchOptions } from "./index-
|
|
1
|
+
import { C as Decision, D as OpenOptions, F as ColumnType, H as IndexSpec, I as ConflictInfo, K as SchemaChange, N as Change, O as TableInput, P as ChangeOp, W as PageSpec, _ as check, a as formatCsv, b as Watcher, c as query, d as init, f as ExportOptions, g as CheckTable, h as CheckResult, i as QueryResult, l as InitOptions, m as exportSite, n as open, o as formatJson, p as ExportResult, q as TableSpec, r as QueryOptions, s as formatTable, t as Yamlite, u as generateConfig, v as WatchHandlers, x as TableResult, y as WatchOptions } from "./index-Bd2ctbJm.mjs";
|
|
2
2
|
export { type Change, type ChangeOp, type CheckResult, type CheckTable, type ColumnType, type ConflictInfo, type Decision, type ExportOptions, type ExportResult, type IndexSpec, type InitOptions, type OpenOptions, type PageSpec, type QueryOptions, type QueryResult, type SchemaChange, type TableInput, type TableResult, type TableSpec, type WatchHandlers, type WatchOptions, type Watcher, Yamlite, check, exportSite, formatCsv, formatJson, formatTable, generateConfig, init, open, query };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { B as check, c as exportSite, n as formatCsv, r as formatJson, i as formatTable, o as generateConfig, s as init, t as open, a as query } from "./src-
|
|
1
|
+
export { B as check, c as exportSite, n as formatCsv, r as formatJson, i as formatTable, o as generateConfig, s as init, t as open, a as query } from "./src-ChJZPkg9.mjs";
|
|
@@ -1129,8 +1129,10 @@ function normalizeFilePath(path) {
|
|
|
1129
1129
|
if (kept.length === 0) return null;
|
|
1130
1130
|
return kept.join("/") + (path.endsWith("/") ? "/" : "");
|
|
1131
1131
|
}
|
|
1132
|
-
const
|
|
1133
|
-
const
|
|
1132
|
+
const entryCovers = (entry, path) => entry.endsWith("/") ? path.startsWith(entry) : path === entry;
|
|
1133
|
+
const fileGranted = (files, path) => Object.keys(files).some((entry) => entryCovers(entry, path));
|
|
1134
|
+
const fileWritable = (files, path) => Object.entries(files).some(([entry, level]) => level === "write" && entryCovers(entry, path));
|
|
1135
|
+
const deniedFile = (path, denied) => denied.some((d) => entryCovers(d.toLowerCase(), path.toLowerCase()));
|
|
1134
1136
|
const CONTENT_TYPES = {
|
|
1135
1137
|
png: "image/png",
|
|
1136
1138
|
jpg: "image/jpeg",
|
|
@@ -1141,6 +1143,50 @@ const CONTENT_TYPES = {
|
|
|
1141
1143
|
pdf: "application/pdf",
|
|
1142
1144
|
txt: "text/plain"
|
|
1143
1145
|
};
|
|
1146
|
+
const starts = (b, magic) => magic.every((m, i) => b[i] === m);
|
|
1147
|
+
const WRITABLE_TYPES = {
|
|
1148
|
+
"image/png": (b) => starts(b, [
|
|
1149
|
+
137,
|
|
1150
|
+
80,
|
|
1151
|
+
78,
|
|
1152
|
+
71,
|
|
1153
|
+
13,
|
|
1154
|
+
10,
|
|
1155
|
+
26,
|
|
1156
|
+
10
|
|
1157
|
+
]),
|
|
1158
|
+
"image/jpeg": (b) => starts(b, [
|
|
1159
|
+
255,
|
|
1160
|
+
216,
|
|
1161
|
+
255
|
|
1162
|
+
]),
|
|
1163
|
+
"image/gif": (b) => starts(b, [
|
|
1164
|
+
71,
|
|
1165
|
+
73,
|
|
1166
|
+
70,
|
|
1167
|
+
56
|
|
1168
|
+
]),
|
|
1169
|
+
"image/webp": (b) => starts(b, [
|
|
1170
|
+
82,
|
|
1171
|
+
73,
|
|
1172
|
+
70,
|
|
1173
|
+
70
|
|
1174
|
+
]) && starts(b.subarray(8), [
|
|
1175
|
+
87,
|
|
1176
|
+
69,
|
|
1177
|
+
66,
|
|
1178
|
+
80
|
|
1179
|
+
]),
|
|
1180
|
+
"application/pdf": (b) => starts(b, [
|
|
1181
|
+
37,
|
|
1182
|
+
80,
|
|
1183
|
+
68,
|
|
1184
|
+
70,
|
|
1185
|
+
45
|
|
1186
|
+
])
|
|
1187
|
+
};
|
|
1188
|
+
const writableType = (path) => Object.hasOwn(WRITABLE_TYPES, contentTypeOf(path));
|
|
1189
|
+
const bytesMatchType = (path, bytes) => WRITABLE_TYPES[contentTypeOf(path)]?.(bytes) ?? false;
|
|
1144
1190
|
function contentTypeOf(path) {
|
|
1145
1191
|
const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
1146
1192
|
return path.includes(".") && Object.hasOwn(CONTENT_TYPES, ext) ? CONTENT_TYPES[ext] : "application/octet-stream";
|
|
@@ -1686,14 +1732,18 @@ function toOrigin(where, raw) {
|
|
|
1686
1732
|
return url.origin;
|
|
1687
1733
|
}
|
|
1688
1734
|
function toFiles(where, raw) {
|
|
1689
|
-
if (raw === void 0 || raw === null) return
|
|
1690
|
-
if (
|
|
1691
|
-
|
|
1735
|
+
if (raw === void 0 || raw === null) return {};
|
|
1736
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where}files must map root-relative files and folders to read or write`);
|
|
1737
|
+
const out = {};
|
|
1738
|
+
for (const [entry, level] of Object.entries(raw)) {
|
|
1692
1739
|
const invalid = (why) => /* @__PURE__ */ new Error(`${where}files entry ${JSON.stringify(entry)} ${why}`);
|
|
1693
|
-
if (
|
|
1740
|
+
if (normalizeFilePath(entry) !== entry) throw invalid("must be a root-relative path without .., . or empty segments");
|
|
1694
1741
|
if (deniedFile(entry, DENIED_FILES)) throw invalid("is YAMLite's own and cannot be granted");
|
|
1695
|
-
|
|
1696
|
-
|
|
1742
|
+
if (level !== "read" && level !== "write") throw invalid("must be read or write");
|
|
1743
|
+
if (level === "write" && !entry.endsWith("/") && !writableType(entry)) throw invalid("cannot be write: a page may write only PNG, JPEG, GIF, WebP or PDF files");
|
|
1744
|
+
out[entry] = level;
|
|
1745
|
+
}
|
|
1746
|
+
return out;
|
|
1697
1747
|
}
|
|
1698
1748
|
function toPageAi(where, raw) {
|
|
1699
1749
|
if (raw === void 0 || raw === null || raw === false) return null;
|
|
@@ -3624,13 +3674,15 @@ var Reply = class {
|
|
|
3624
3674
|
this.body = body;
|
|
3625
3675
|
}
|
|
3626
3676
|
};
|
|
3677
|
+
const MAX_BODY = 1048576;
|
|
3627
3678
|
var Router = class {
|
|
3628
3679
|
routes = [];
|
|
3629
|
-
add(method, pattern, handler) {
|
|
3680
|
+
add(method, pattern, handler, opts = {}) {
|
|
3630
3681
|
this.routes.push({
|
|
3631
3682
|
method,
|
|
3632
3683
|
parts: pattern.split("/").filter(Boolean),
|
|
3633
|
-
handler
|
|
3684
|
+
handler,
|
|
3685
|
+
maxBody: opts.maxBody ?? MAX_BODY
|
|
3634
3686
|
});
|
|
3635
3687
|
return this;
|
|
3636
3688
|
}
|
|
@@ -3656,7 +3708,8 @@ var Router = class {
|
|
|
3656
3708
|
}
|
|
3657
3709
|
return {
|
|
3658
3710
|
handler: route.handler,
|
|
3659
|
-
params
|
|
3711
|
+
params,
|
|
3712
|
+
maxBody: route.maxBody
|
|
3660
3713
|
};
|
|
3661
3714
|
}
|
|
3662
3715
|
return wrongMethod ? "method" : null;
|
|
@@ -3670,7 +3723,7 @@ var Router = class {
|
|
|
3670
3723
|
let body;
|
|
3671
3724
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
3672
3725
|
if (!/^application\/json\s*(;|$)/i.test(request.headers.get("content-type") ?? "")) throw new HttpError(415, "content-type must be application/json");
|
|
3673
|
-
body = await readBody(request);
|
|
3726
|
+
body = await readBody(request, match.maxBody);
|
|
3674
3727
|
}
|
|
3675
3728
|
const out = await match.handler({
|
|
3676
3729
|
params: match.params,
|
|
@@ -3707,10 +3760,9 @@ function sendJson(res, status, body) {
|
|
|
3707
3760
|
});
|
|
3708
3761
|
res.end(JSON.stringify(body, jsonReplacer$1));
|
|
3709
3762
|
}
|
|
3710
|
-
|
|
3711
|
-
async function readBody(request) {
|
|
3763
|
+
async function readBody(request, max) {
|
|
3712
3764
|
const tooLarge = () => new HttpError(413, "request body is too large");
|
|
3713
|
-
if (Number(request.headers.get("content-length")) >
|
|
3765
|
+
if (Number(request.headers.get("content-length")) > max) throw tooLarge();
|
|
3714
3766
|
const chunks = [];
|
|
3715
3767
|
let size = 0;
|
|
3716
3768
|
if (request.body) {
|
|
@@ -3719,7 +3771,7 @@ async function readBody(request) {
|
|
|
3719
3771
|
const { value, done } = await reader.read();
|
|
3720
3772
|
if (done) break;
|
|
3721
3773
|
size += value.length;
|
|
3722
|
-
if (size >
|
|
3774
|
+
if (size > max) {
|
|
3723
3775
|
await reader.cancel().catch(() => {});
|
|
3724
3776
|
throw tooLarge();
|
|
3725
3777
|
}
|
|
@@ -4397,6 +4449,14 @@ var EventHub = class {
|
|
|
4397
4449
|
};
|
|
4398
4450
|
for (const res of this.clients.keys()) this.send(res, event.type, event);
|
|
4399
4451
|
}
|
|
4452
|
+
filesChanged(paths) {
|
|
4453
|
+
const event = {
|
|
4454
|
+
type: "files",
|
|
4455
|
+
at: now(),
|
|
4456
|
+
paths
|
|
4457
|
+
};
|
|
4458
|
+
for (const res of this.clients.keys()) this.send(res, event.type, event);
|
|
4459
|
+
}
|
|
4400
4460
|
stream(signal) {
|
|
4401
4461
|
let ctrl;
|
|
4402
4462
|
const drop = () => {
|
|
@@ -5015,17 +5075,13 @@ const rowRoutes = (router, ctx) => {
|
|
|
5015
5075
|
const key = params.key;
|
|
5016
5076
|
const b = objectBody(body, "body");
|
|
5017
5077
|
const values = objectBody(b.values, "values");
|
|
5018
|
-
const base =
|
|
5078
|
+
const base = baseOf$1(b.base);
|
|
5019
5079
|
noKeyIn(spec, values, "rename the record instead");
|
|
5020
5080
|
const { store } = ctx;
|
|
5021
5081
|
writeTx(store, () => {
|
|
5022
5082
|
const current = readRecord(store, spec, key);
|
|
5023
5083
|
if (!current) throw new HttpError(404, `no record "${key}" in ${spec.name}`);
|
|
5024
|
-
|
|
5025
|
-
if (stale.length > 0) throw new HttpError(409, `changed since it was loaded: ${stale.join(", ")}`, {
|
|
5026
|
-
current,
|
|
5027
|
-
stale
|
|
5028
|
-
});
|
|
5084
|
+
if (base) assertFresh(current, base, Object.keys(values));
|
|
5029
5085
|
updateRecord(store, spec, key, values);
|
|
5030
5086
|
});
|
|
5031
5087
|
return { ok: true };
|
|
@@ -5046,13 +5102,31 @@ const rowRoutes = (router, ctx) => {
|
|
|
5046
5102
|
});
|
|
5047
5103
|
return { key: to };
|
|
5048
5104
|
});
|
|
5049
|
-
router.add("DELETE", "/api/tables/:table/rows/:key", ({ params }) => {
|
|
5105
|
+
router.add("DELETE", "/api/tables/:table/rows/:key", ({ params, body }) => {
|
|
5050
5106
|
ctx.proposals.assertIdle();
|
|
5051
5107
|
const spec = tableSpec(ctx, params.table);
|
|
5052
|
-
|
|
5108
|
+
const key = params.key;
|
|
5109
|
+
const base = baseOf$1(body?.base);
|
|
5110
|
+
const { store } = ctx;
|
|
5111
|
+
writeTx(store, () => {
|
|
5112
|
+
if (base) {
|
|
5113
|
+
const current = readRecord(store, spec, key);
|
|
5114
|
+
if (!current) throw new HttpError(404, `no record "${key}" in ${spec.name}`);
|
|
5115
|
+
assertFresh(current, base, Object.keys(base));
|
|
5116
|
+
}
|
|
5117
|
+
deleteRecord(store, spec, key);
|
|
5118
|
+
});
|
|
5053
5119
|
return { ok: true };
|
|
5054
5120
|
});
|
|
5055
5121
|
};
|
|
5122
|
+
const baseOf$1 = (value) => value === void 0 ? null : objectBody(value, "base");
|
|
5123
|
+
function assertFresh(current, base, fields) {
|
|
5124
|
+
const stale = fields.filter((f) => canonical(own$1(current, f) ?? null) !== canonical(own$1(base, f) ?? null));
|
|
5125
|
+
if (stale.length > 0) throw new HttpError(409, `changed since it was loaded: ${stale.join(", ")}`, {
|
|
5126
|
+
current,
|
|
5127
|
+
stale
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5056
5130
|
//#endregion
|
|
5057
5131
|
//#region src/serve/routes/history.ts
|
|
5058
5132
|
const CURSOR = /^\d+$/;
|
|
@@ -5676,7 +5750,7 @@ async function gitStatus(repo) {
|
|
|
5676
5750
|
}
|
|
5677
5751
|
async function gitDiff(repo, paths = []) {
|
|
5678
5752
|
const scope = paths.map((p) => {
|
|
5679
|
-
const top = insideRoot(repo, p);
|
|
5753
|
+
const top = insideRoot$1(repo, p);
|
|
5680
5754
|
if (top === null) throw new Error(`${p} must be inside the data folder`);
|
|
5681
5755
|
return top;
|
|
5682
5756
|
});
|
|
@@ -5737,7 +5811,7 @@ async function ghReady(env = process.env) {
|
|
|
5737
5811
|
return false;
|
|
5738
5812
|
}
|
|
5739
5813
|
}
|
|
5740
|
-
function insideRoot(repo, path) {
|
|
5814
|
+
function insideRoot$1(repo, path) {
|
|
5741
5815
|
const abs = resolve(repo.root, path);
|
|
5742
5816
|
const rel = relative(repo.root, abs);
|
|
5743
5817
|
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return null;
|
|
@@ -5855,7 +5929,7 @@ async function branchName(repo, v, i) {
|
|
|
5855
5929
|
}
|
|
5856
5930
|
async function commitPath(repo, v, i) {
|
|
5857
5931
|
const p = text$1(v, "path", i);
|
|
5858
|
-
const top = insideRoot(repo, p);
|
|
5932
|
+
const top = insideRoot$1(repo, p);
|
|
5859
5933
|
if (top === null) throw new Error(`step ${i + 1}: ${p} must be inside the data folder`);
|
|
5860
5934
|
if (existsSync(resolve(repo.root, p))) return p;
|
|
5861
5935
|
try {
|
|
@@ -6003,7 +6077,7 @@ async function runOne(repo, s, gh) {
|
|
|
6003
6077
|
await g(["switch", s.branch]);
|
|
6004
6078
|
break;
|
|
6005
6079
|
case "commit": {
|
|
6006
|
-
const tops = s.paths.map((p) => insideRoot(repo, p));
|
|
6080
|
+
const tops = s.paths.map((p) => insideRoot$1(repo, p));
|
|
6007
6081
|
await g([
|
|
6008
6082
|
"add",
|
|
6009
6083
|
"-A",
|
|
@@ -6241,7 +6315,7 @@ async function localDriver(root, gh) {
|
|
|
6241
6315
|
changes: []
|
|
6242
6316
|
};
|
|
6243
6317
|
const s = await gitStatus(f.repo);
|
|
6244
|
-
const changes = s.changes.filter((c) => insideRoot(f.repo, c.path) !== null).map((c) => ({
|
|
6318
|
+
const changes = s.changes.filter((c) => insideRoot$1(f.repo, c.path) !== null).map((c) => ({
|
|
6245
6319
|
path: c.path,
|
|
6246
6320
|
status: changeStatus(c.status)
|
|
6247
6321
|
}));
|
|
@@ -6254,7 +6328,7 @@ async function localDriver(root, gh) {
|
|
|
6254
6328
|
defaultBranch: defaultName,
|
|
6255
6329
|
async baseContent(rootPath) {
|
|
6256
6330
|
const { repo } = await need();
|
|
6257
|
-
const top = insideRoot(repo, rootPath);
|
|
6331
|
+
const top = insideRoot$1(repo, rootPath);
|
|
6258
6332
|
if (top === null) return null;
|
|
6259
6333
|
try {
|
|
6260
6334
|
return await runGit(repo.top, ["show", `HEAD:${top}`]);
|
|
@@ -6342,7 +6416,7 @@ async function defaultRef(repo, name) {
|
|
|
6342
6416
|
throw new GitError(`the default branch ${name} is not in this clone`);
|
|
6343
6417
|
}
|
|
6344
6418
|
async function blobAt(repo, rev, rootPath) {
|
|
6345
|
-
const top = insideRoot(repo, rootPath);
|
|
6419
|
+
const top = insideRoot$1(repo, rootPath);
|
|
6346
6420
|
if (top === null) throw new GitError(`${rootPath} is outside the repository`);
|
|
6347
6421
|
let entry;
|
|
6348
6422
|
try {
|
|
@@ -6621,7 +6695,90 @@ function grantedFile(root, files, denied, path) {
|
|
|
6621
6695
|
size
|
|
6622
6696
|
};
|
|
6623
6697
|
}
|
|
6624
|
-
const
|
|
6698
|
+
const MAX_WRITE_BODY = Math.ceil(MAX_PAGE_FILE_BYTES / 3) * 4 + 4096;
|
|
6699
|
+
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
6700
|
+
const insideRoot = (rel) => rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
6701
|
+
function writeTarget(root, page, denied, path) {
|
|
6702
|
+
const realRoot = realpathSync.native(root);
|
|
6703
|
+
const parts = path.split("/");
|
|
6704
|
+
let at = realRoot;
|
|
6705
|
+
let i = 0;
|
|
6706
|
+
for (; i < parts.length - 1; i++) {
|
|
6707
|
+
const next = join(at, parts[i]);
|
|
6708
|
+
try {
|
|
6709
|
+
at = realpathSync.native(next);
|
|
6710
|
+
} catch {
|
|
6711
|
+
if (lstatOrNull(next)) throw new HttpError(409, `${parts.slice(0, i + 1).join("/")} is not a folder`);
|
|
6712
|
+
break;
|
|
6713
|
+
}
|
|
6714
|
+
if (!statSync(at).isDirectory()) throw new HttpError(409, `${parts.slice(0, i + 1).join("/")} is not a folder`);
|
|
6715
|
+
}
|
|
6716
|
+
const target = join(at, ...parts.slice(i));
|
|
6717
|
+
const rel = relative(realRoot, target);
|
|
6718
|
+
const shown = rel.split(sep).join("/");
|
|
6719
|
+
if (!insideRoot(rel) || !fileWritable(page.files, shown) || deniedFile(shown, denied)) throw new HttpError(403, `${path} leads outside the files this page may write`);
|
|
6720
|
+
let isLink = false;
|
|
6721
|
+
try {
|
|
6722
|
+
const st = lstatSync(target);
|
|
6723
|
+
isLink = st.isSymbolicLink();
|
|
6724
|
+
if (!isLink && !st.isFile()) throw new HttpError(409, `${path} is not a file`);
|
|
6725
|
+
} catch (e) {
|
|
6726
|
+
if (e instanceof HttpError) throw e;
|
|
6727
|
+
}
|
|
6728
|
+
if (isLink) throw new HttpError(403, `${path} is a symlink; a page may not write through it`);
|
|
6729
|
+
return target;
|
|
6730
|
+
}
|
|
6731
|
+
const lstatOrNull = (path) => lstatSync(path, { throwIfNoEntry: false }) ?? null;
|
|
6732
|
+
function readIfFile(path) {
|
|
6733
|
+
const st = lstatOrNull(path);
|
|
6734
|
+
if (!st?.isFile()) return null;
|
|
6735
|
+
if (st.size > 10485760) return "too-large";
|
|
6736
|
+
return readFileSync(path);
|
|
6737
|
+
}
|
|
6738
|
+
function baseOf(body) {
|
|
6739
|
+
const base = body?.base;
|
|
6740
|
+
if (base === void 0 || base === null) return base;
|
|
6741
|
+
if (typeof base !== "string" || !/^[0-9a-f]{64}$/.test(base)) throw new HttpError(400, "base must be a SHA-256 hex digest or null");
|
|
6742
|
+
return base;
|
|
6743
|
+
}
|
|
6744
|
+
async function hostedFile(ctx, target, path, signal) {
|
|
6745
|
+
if (existsSync(target)) return null;
|
|
6746
|
+
const hosted = await ctx.pageFiles?.(path, signal);
|
|
6747
|
+
return hosted === void 0 || hosted === "missing" ? null : hosted;
|
|
6748
|
+
}
|
|
6749
|
+
function checkBase(target, path, base, hosted) {
|
|
6750
|
+
const disk = readIfFile(target);
|
|
6751
|
+
const current = disk ?? hosted;
|
|
6752
|
+
const from = disk !== null ? "disk" : hosted !== null ? "host" : null;
|
|
6753
|
+
if (base === void 0) return from;
|
|
6754
|
+
if (current === "too-large") throw new HttpError(409, `${path} changed since the page read it`);
|
|
6755
|
+
const now = current === null ? null : hashContent(current);
|
|
6756
|
+
if (now !== base) throw new HttpError(409, `${path} changed since the page read it`, { current: now });
|
|
6757
|
+
return from;
|
|
6758
|
+
}
|
|
6759
|
+
function writableRequest(ctx, params, query) {
|
|
6760
|
+
const page = pageSpec(ctx, params.page);
|
|
6761
|
+
const asked = query.get("path");
|
|
6762
|
+
if (!asked) throw new HttpError(400, "path is required");
|
|
6763
|
+
const path = normalizeFilePath(asked);
|
|
6764
|
+
if (path === null || path.endsWith("/") || !fileWritable(page.files, path)) throw new HttpError(403, `${asked} is not in the files this page may write`);
|
|
6765
|
+
if (deniedFile(path, ctx.deniedFiles)) throw new HttpError(403, `${asked} is YAMLite's own and no page may write it`);
|
|
6766
|
+
return {
|
|
6767
|
+
page,
|
|
6768
|
+
path
|
|
6769
|
+
};
|
|
6770
|
+
}
|
|
6771
|
+
function replaceFile(target, bytes) {
|
|
6772
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
6773
|
+
const tmp = join(dirname(target), `.yamlite-${crypto.randomUUID()}.tmp`);
|
|
6774
|
+
try {
|
|
6775
|
+
writeFileSync(tmp, bytes);
|
|
6776
|
+
renameSync(tmp, target);
|
|
6777
|
+
} catch (e) {
|
|
6778
|
+
rmSync(tmp, { force: true });
|
|
6779
|
+
throw e;
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6625
6782
|
const pageRoutes = (router, ctx) => {
|
|
6626
6783
|
router.add("GET", "/api/pages/:page/html", ({ params }) => {
|
|
6627
6784
|
const page = pageSpec(ctx, params.page);
|
|
@@ -6634,15 +6791,47 @@ const pageRoutes = (router, ctx) => {
|
|
|
6634
6791
|
const path = normalizeFilePath(asked);
|
|
6635
6792
|
if (path === null || !fileGranted(page.files, path)) throw new HttpError(403, `${asked} is not in this page's files`);
|
|
6636
6793
|
if (deniedFile(path, ctx.deniedFiles)) throw new HttpError(403, `${asked} is YAMLite's own and no page may read it`);
|
|
6637
|
-
let bytes
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6794
|
+
let bytes;
|
|
6795
|
+
try {
|
|
6796
|
+
bytes = readFileSync(grantedFile(ctx.root, page.files, ctx.deniedFiles, path).real);
|
|
6797
|
+
} catch (e) {
|
|
6798
|
+
if (!(e instanceof HttpError) || e.status !== 404) throw e;
|
|
6799
|
+
const hosted = await ctx.pageFiles?.(path, signal);
|
|
6800
|
+
if (hosted === void 0 || hosted === "missing") throw missing(path);
|
|
6801
|
+
if (hosted === "too-large" || hosted.length > 10485760) throw tooLarge(path);
|
|
6802
|
+
bytes = hosted;
|
|
6803
|
+
}
|
|
6641
6804
|
return new Response(bytes, { headers: {
|
|
6642
6805
|
...FILE_HEADERS,
|
|
6643
6806
|
"content-type": contentTypeOf(path)
|
|
6644
6807
|
} });
|
|
6645
6808
|
});
|
|
6809
|
+
router.add("PUT", "/api/pages/:page/file", async ({ params, query, body, signal }) => {
|
|
6810
|
+
const { page, path } = writableRequest(ctx, params, query);
|
|
6811
|
+
if (!writableType(path)) throw new HttpError(415, `${path}: a page may write only PNG, JPEG, GIF, WebP or PDF files`);
|
|
6812
|
+
const data = body?.data;
|
|
6813
|
+
if (typeof data !== "string" || !BASE64.test(data)) throw new HttpError(400, "data must be base64");
|
|
6814
|
+
const bytes = Buffer.from(data, "base64");
|
|
6815
|
+
if (bytes.length > 10485760) throw tooLarge(path);
|
|
6816
|
+
if (!bytesMatchType(path, bytes)) throw new HttpError(415, `${path}: the content is not ${contentTypeOf(path)}`);
|
|
6817
|
+
const base = baseOf(body);
|
|
6818
|
+
const hosted = await hostedFile(ctx, writeTarget(ctx.root, page, ctx.deniedFiles, path), path, signal);
|
|
6819
|
+
const target = writeTarget(ctx.root, page, ctx.deniedFiles, path);
|
|
6820
|
+
checkBase(target, path, base, hosted);
|
|
6821
|
+
replaceFile(target, bytes);
|
|
6822
|
+
return { sha256: hashContent(bytes) };
|
|
6823
|
+
}, { maxBody: MAX_WRITE_BODY });
|
|
6824
|
+
router.add("DELETE", "/api/pages/:page/file", async ({ params, query, body, signal }) => {
|
|
6825
|
+
const { page, path } = writableRequest(ctx, params, query);
|
|
6826
|
+
const base = baseOf(body);
|
|
6827
|
+
const hosted = await hostedFile(ctx, writeTarget(ctx.root, page, ctx.deniedFiles, path), path, signal);
|
|
6828
|
+
const target = writeTarget(ctx.root, page, ctx.deniedFiles, path);
|
|
6829
|
+
const from = checkBase(target, path, base, hosted);
|
|
6830
|
+
if (from === null) throw missing(path);
|
|
6831
|
+
if (from === "host") throw new HttpError(409, `${path} is kept by the host and cannot be deleted here`);
|
|
6832
|
+
unlinkSync(target);
|
|
6833
|
+
return { ok: true };
|
|
6834
|
+
});
|
|
6646
6835
|
};
|
|
6647
6836
|
//#endregion
|
|
6648
6837
|
//#region src/serve/routes/pageai.ts
|
|
@@ -7725,7 +7914,7 @@ function grantedCandidates(root, entries) {
|
|
|
7725
7914
|
return found;
|
|
7726
7915
|
}
|
|
7727
7916
|
function copyPageFiles(root, dir, grants, denied, skipped) {
|
|
7728
|
-
for (const path of new Set(grantedCandidates(root, grants))) {
|
|
7917
|
+
for (const path of new Set(grantedCandidates(root, Object.keys(grants)))) {
|
|
7729
7918
|
if (deniedFile(path, denied)) continue;
|
|
7730
7919
|
let real;
|
|
7731
7920
|
try {
|
|
@@ -7808,7 +7997,7 @@ async function writeSnapshotData(opts) {
|
|
|
7808
7997
|
}
|
|
7809
7998
|
}
|
|
7810
7999
|
const kept = new Set(pages.map(([name]) => name));
|
|
7811
|
-
copyPageFiles(root, opts.dir, y.pages.filter((p) => kept.has(p.name)).
|
|
8000
|
+
copyPageFiles(root, opts.dir, Object.assign({}, ...y.pages.filter((p) => kept.has(p.name)).map((p) => p.files)), ctx.deniedFiles, skippedFiles);
|
|
7812
8001
|
meta = {
|
|
7813
8002
|
...served,
|
|
7814
8003
|
root: basename(root),
|
|
@@ -8574,4 +8763,4 @@ async function open(options) {
|
|
|
8574
8763
|
};
|
|
8575
8764
|
}
|
|
8576
8765
|
//#endregion
|
|
8577
|
-
export { deniedFiles as A, check as B, describeChange as C, isRead as D, PageSql as E, Router as F, resolveConfig as G, configPath as H, createHandler as I,
|
|
8766
|
+
export { Store as $, deniedFiles as A, check as B, describeChange as C, isRead as D, PageSql as E, Router as F, resolveConfig as G, configPath as H, createHandler as I, deniedFile as J, DENIED_FILES as K, HttpError as L, readRecord as M, assertSynced as N, statementCount as O, toWire as P, declaredViews as Q, isLoopback as R, which as S, worthShowing as T, parseConfig as U, CONFIG_NAMES as V, readsFile as W, fileGranted as X, entryCovers as Y, normalizeFilePath as Z, ghReady as _, query as a, gitLog as b, exportSite as c, validateNewTable as d, COLUMN_TYPES as et, ReviewRefused as f, DIFF_LIMIT as g, templatePath as h, formatTable as i, ProposalStore as j, EventHub as k, ROUTES as l, STEP_KINDS as m, formatCsv as n, generateConfig as o, localDriver as p, MAX_PAGE_FILE_BYTES as q, formatJson as r, init as s, open as t, createTable as u, gitBranches as v, fieldChanges as w, gitStatus as x, gitDiff as y, loopbackHosts as z };
|