uniweb 1.17.0 → 1.17.2
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/package.json +7 -7
- package/partials/agents.md +1 -1
- package/src/backend/site-sync.js +37 -6
- package/src/commands/doctor.js +26 -1
- package/src/commands/pull.js +12 -3
- package/src/framework-index.json +6 -6
- package/src/utils/uwx-read.js +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
+
"@uniweb/content-writer": "~1.3.0",
|
|
44
45
|
"@uniweb/schemas": "~1.6.5",
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/content-writer": "~1.3.0"
|
|
46
|
+
"@uniweb/core": "~1.4.1",
|
|
47
|
+
"@uniweb/semantic-parser": "~1.12.2"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.8.
|
|
51
|
-
"@uniweb/
|
|
52
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "~1.8.2",
|
|
51
|
+
"@uniweb/content-reader": "~1.5.4",
|
|
52
|
+
"@uniweb/semantic-parser": "~1.12.2"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -822,7 +822,7 @@ agents:
|
|
|
822
822
|
| not declared | no check at all — nothing happens unless you ask for it |
|
|
823
823
|
| your own site's origin | always fine, without listing it — declaring a partner must not take down your own chat box |
|
|
824
824
|
| a request with **no** `Origin` | **allowed** — see below, this is deliberate |
|
|
825
|
-
| matching |
|
|
825
|
+
| matching | by origin: `https://app.example.com/` is `https://app.example.com`, and a path is ignored; no wildcards. An entry that is not an `http(s)` URL matches nothing |
|
|
826
826
|
|
|
827
827
|
⛔ **It is not a spend limit, and it cannot be.** `Origin` is set by the browser and enforced by the browser: anything that is not a browser sends whatever it likes, or nothing at all. So this stops **casual misuse and third-party embedding** — someone dropping your endpoint into their own page — and it does **not** stop anyone determined to run up your bill. Do not size your budget on it.
|
|
828
828
|
|
package/src/backend/site-sync.js
CHANGED
|
@@ -92,6 +92,17 @@ function entityDocFromUwx(buf) {
|
|
|
92
92
|
return null
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// What a `.uwx`'s entries inflate to, summed — the quantity a backend's package limit counts.
|
|
96
|
+
// null for anything that is not a readable `.uwx`.
|
|
97
|
+
function inflatedSizeOfUwx(buf) {
|
|
98
|
+
if (!buf || buf.length < 2 || buf[0] !== 0x50 || buf[1] !== 0x4b) return null
|
|
99
|
+
try {
|
|
100
|
+
return [...readZip(buf).values()].reduce((n, data) => n + data.length, 0)
|
|
101
|
+
} catch {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
// Every entity `$`-document of a `.uwx` in the order it was submitted — the manifest's, which is
|
|
96
107
|
// what a push's `finalized[].index` counts in. null where a listed file is missing or unreadable.
|
|
97
108
|
function submittedDocsFromUwx(buf) {
|
|
@@ -2169,12 +2180,13 @@ export async function pushSyncPackages({
|
|
|
2169
2180
|
// `boundUuid` is set only for the lanes that address an EXISTING site by uuid
|
|
2170
2181
|
// (content UPDATE, folder push) — never for the CREATE, which has no uuid to be
|
|
2171
2182
|
// wrong about. It is what lets a 404 be read as "the site this clone is bound to
|
|
2172
|
-
// is gone" rather than "the route is missing".
|
|
2183
|
+
// is gone" rather than "the route is missing". `sent` is the package the lane sends,
|
|
2184
|
+
// which a refusal for size measures.
|
|
2173
2185
|
const postLane = async (
|
|
2174
2186
|
label,
|
|
2175
2187
|
doRequest,
|
|
2176
2188
|
explainStale = async () => [],
|
|
2177
|
-
{ boundUuid = null, reportTemplate = false } = {}
|
|
2189
|
+
{ boundUuid = null, reportTemplate = false, sent = null } = {}
|
|
2178
2190
|
) => {
|
|
2179
2191
|
info(`Pushing ${label} to ${dim(client.origin)} …`)
|
|
2180
2192
|
let res
|
|
@@ -2191,7 +2203,7 @@ export async function pushSyncPackages({
|
|
|
2191
2203
|
// Two unrelated conflicts share HTTP 409, so branch on the machine-readable
|
|
2192
2204
|
// `reason` — never on `detail`, which is prose the backend may reword.
|
|
2193
2205
|
let problem = null
|
|
2194
|
-
if ((res.status === 409 || res.status === 400 || res.status === 403 || res.status === 422) && body) {
|
|
2206
|
+
if ((res.status === 409 || res.status === 400 || res.status === 403 || res.status === 413 || res.status === 422) && body) {
|
|
2195
2207
|
try {
|
|
2196
2208
|
problem = JSON.parse(body)
|
|
2197
2209
|
} catch {
|
|
@@ -2376,6 +2388,25 @@ export async function pushSyncPackages({
|
|
|
2376
2388
|
` deleted there → uniweb forget --server ${client.origin}, then push again: it creates a NEW site`
|
|
2377
2389
|
)
|
|
2378
2390
|
note('Deleting this folder removes only your local copy, either way.')
|
|
2391
|
+
} else if (res.status === 413) {
|
|
2392
|
+
// ⭐ TWO LIMITS, ON TWO QUANTITIES, BOTH THE BACKEND'S (backend, 2026-10-23): the
|
|
2393
|
+
// request as sent, and the package as its entries inflate. A package refusal names its
|
|
2394
|
+
// limit (`package_too_large`, `limit` in bytes), so quote it beside what this package
|
|
2395
|
+
// inflates to. A request refusal names none, so say what was sent and quote no limit —
|
|
2396
|
+
// a copy of theirs here would be a second constant to drift. The package is already
|
|
2397
|
+
// Deflated (`createZip`), so there is no smaller form of it to retry with.
|
|
2398
|
+
if (problem?.reason === 'package_too_large') {
|
|
2399
|
+
const inflated = humanBytes(inflatedSizeOfUwx(sent))
|
|
2400
|
+
const limit = humanBytes(problem.limit)
|
|
2401
|
+
note(
|
|
2402
|
+
`${inflated ? `This push's content is ${inflated} uncompressed` : "This push's content is too large"} — more than the ${limit ? `${limit} ` : ''}the backend at ${client.origin} takes in one push.`
|
|
2403
|
+
)
|
|
2404
|
+
} else {
|
|
2405
|
+
const size = humanBytes(sent?.length)
|
|
2406
|
+
note(
|
|
2407
|
+
`${size ? `This push sent ${size}` : 'This push was too large'} — more than the backend at ${client.origin} accepts in one request.`
|
|
2408
|
+
)
|
|
2409
|
+
}
|
|
2379
2410
|
} else if (problem?.reason === 'template_records_not_copyable' && Array.isArray(problem.records)) {
|
|
2380
2411
|
// ⭐ A push asking to designate the site a template is refused whole when a record in its
|
|
2381
2412
|
// folder cannot be copied into the sites made from it — each named, with a `detail` written
|
|
@@ -2615,7 +2646,7 @@ export async function pushSyncPackages({
|
|
|
2615
2646
|
localBuffer: siteContent.buffer,
|
|
2616
2647
|
uuid: siteContentUuid
|
|
2617
2648
|
}),
|
|
2618
|
-
{ boundUuid: siteContentUuid, reportTemplate: true }
|
|
2649
|
+
{ boundUuid: siteContentUuid, reportTemplate: true, sent: siteContent.buffer }
|
|
2619
2650
|
)
|
|
2620
2651
|
if (!finalized) {
|
|
2621
2652
|
mergeHarvested()
|
|
@@ -2631,7 +2662,7 @@ export async function pushSyncPackages({
|
|
|
2631
2662
|
'site-content',
|
|
2632
2663
|
() => client.createSiteContent(siteContent.buffer, { source }),
|
|
2633
2664
|
undefined,
|
|
2634
|
-
{ reportTemplate: true }
|
|
2665
|
+
{ reportTemplate: true, sent: siteContent.buffer }
|
|
2635
2666
|
)
|
|
2636
2667
|
if (payload === null) return { exitCode: 1, finalizedTotal, wrote }
|
|
2637
2668
|
const minted = extractMintedSiteUuid(payload)
|
|
@@ -2679,7 +2710,7 @@ export async function pushSyncPackages({
|
|
|
2679
2710
|
'records',
|
|
2680
2711
|
() => client.pushFolder(boundSiteUuid, records.buffer, { source }),
|
|
2681
2712
|
undefined,
|
|
2682
|
-
{ boundUuid: boundSiteUuid }
|
|
2713
|
+
{ boundUuid: boundSiteUuid, sent: records.buffer }
|
|
2683
2714
|
)
|
|
2684
2715
|
if (!finalized) {
|
|
2685
2716
|
mergeHarvested({ point: false })
|
package/src/commands/doctor.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
extensionNameOf,
|
|
20
20
|
findSiteExtension
|
|
21
21
|
} from '@uniweb/build'
|
|
22
|
-
import { loadDeployYml, AGENTS_KEYS } from '@uniweb/build/site'
|
|
22
|
+
import { loadDeployYml, AGENTS_KEYS, originOf } from '@uniweb/build/site'
|
|
23
23
|
import { listAdapters } from '@uniweb/build/hosts'
|
|
24
24
|
import { RUNTIME_KEYS } from '@uniweb/build/uwx'
|
|
25
25
|
import { getCliVersion } from '../versions.js'
|
|
@@ -337,6 +337,11 @@ export function findFormContent(sitePath, siteYml) {
|
|
|
337
337
|
*
|
|
338
338
|
* `agents: false` turns the capability off in one word and is not an object —
|
|
339
339
|
* flagging its absence of keys would be nonsense.
|
|
340
|
+
*
|
|
341
|
+
* ⭐ **An `expectedOrigins` entry that names no origin matches nothing**, and that is silent
|
|
342
|
+
* too. `app.example.com`, with no scheme, reads as no origin, so a list of only such entries
|
|
343
|
+
* declines every third-party caller. The entry is read by the rule the host checks by
|
|
344
|
+
* (`originOf`, `@uniweb/projections`).
|
|
340
345
|
*/
|
|
341
346
|
export function checkAgentsBlock({ siteName, siteYml, issues }) {
|
|
342
347
|
const agents = siteYml?.agents
|
|
@@ -353,6 +358,8 @@ export function checkAgentsBlock({ siteName, siteYml, issues }) {
|
|
|
353
358
|
return
|
|
354
359
|
}
|
|
355
360
|
|
|
361
|
+
checkExpectedOrigins({ siteName, agents, issues })
|
|
362
|
+
|
|
356
363
|
const known = new Set(AGENTS_KEYS)
|
|
357
364
|
const unknown = Object.keys(agents).filter((k) => !known.has(k))
|
|
358
365
|
if (unknown.length === 0) return
|
|
@@ -381,6 +388,24 @@ export function checkAgentsBlock({ siteName, siteYml, issues }) {
|
|
|
381
388
|
)
|
|
382
389
|
}
|
|
383
390
|
|
|
391
|
+
/** `agents.expectedOrigins` entries that name no origin, each named. */
|
|
392
|
+
function checkExpectedOrigins({ siteName, agents, issues }) {
|
|
393
|
+
const declared = agents.expectedOrigins
|
|
394
|
+
if (declared === undefined || declared === null) return
|
|
395
|
+
const unreadable = (Array.isArray(declared) ? declared : [declared]).filter((entry) => originOf(entry) === null)
|
|
396
|
+
if (unreadable.length === 0) return
|
|
397
|
+
const id = 'agents-origin-unreadable'
|
|
398
|
+
const shown = unreadable.map((entry) => `'${String(entry)}'`).join(', ')
|
|
399
|
+
issues.push({
|
|
400
|
+
id,
|
|
401
|
+
type: 'warning',
|
|
402
|
+
site: siteName,
|
|
403
|
+
message: `site.yml: \`agents.expectedOrigins\` ${unreadable.length === 1 ? 'entry' : 'entries'} ${shown} name${unreadable.length === 1 ? 's' : ''} no origin, so ${unreadable.length === 1 ? 'it matches' : 'they match'} nothing`
|
|
404
|
+
})
|
|
405
|
+
warn(`[${id}] ${siteName}: \`agents.expectedOrigins\` ${unreadable.length === 1 ? 'entry' : 'entries'} ${shown} name${unreadable.length === 1 ? 's' : ''} no origin.`)
|
|
406
|
+
log(` ${colors.dim}Write each as an http(s) URL with its scheme, e.g. https://app.example.com. An entry that names no origin matches nothing.${colors.reset}`)
|
|
407
|
+
}
|
|
408
|
+
|
|
384
409
|
/**
|
|
385
410
|
* The closest known key within a small edit distance, or null.
|
|
386
411
|
*
|
package/src/commands/pull.js
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
readBackendState,
|
|
79
79
|
siteContentDocumentToProject,
|
|
80
80
|
dataBlockModels,
|
|
81
|
+
siteDefaultSectionType,
|
|
81
82
|
recordsToProject,
|
|
82
83
|
siteSelfScope,
|
|
83
84
|
readZip,
|
|
@@ -338,8 +339,8 @@ export function describeUnplacedRecords({ docs, taken, skipped }) {
|
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
// Read a pull lane's bytes into entity `$`-documents. The backend serves a `.uwx`
|
|
341
|
-
// (
|
|
342
|
-
// documents. Falls back to a JSON body (a raw doc, a `{document}`/`{entity}` envelope,
|
|
342
|
+
// (a ZIP of `manifest.json` + `entities/<uuid>.json`, each entry Stored or Deflate —
|
|
343
|
+
// `readZip` reads both); the entity files ARE the documents. Falls back to a JSON body (a raw doc, a `{document}`/`{entity}` envelope,
|
|
343
344
|
// or a list) so the lane survives a future envelope change. Returns an array (possibly
|
|
344
345
|
// empty).
|
|
345
346
|
export function readPullDocuments(buf) {
|
|
@@ -829,6 +830,7 @@ export async function pull(args = [], deps = {}) {
|
|
|
829
830
|
// resolves.
|
|
830
831
|
let keepAuthoredFoundation = false
|
|
831
832
|
let dataModel = null
|
|
833
|
+
let sectionType
|
|
832
834
|
// The Models the pulled site's queries name, as the push qualified them — the re-bank
|
|
833
835
|
// resolves with them too.
|
|
834
836
|
let pulledQueryModels = []
|
|
@@ -854,6 +856,12 @@ export async function pull(args = [], deps = {}) {
|
|
|
854
856
|
// A translated data block is read back by what its section's type declares it is, as the
|
|
855
857
|
// push translated it — after the registered foundation above, whose section types say it.
|
|
856
858
|
dataModel = await dataBlockModels(siteDir)
|
|
859
|
+
// The type a section that names none renders with — what the push filled in, and what a
|
|
860
|
+
// section's file leaves out again. A clone's foundation is the one the document pins.
|
|
861
|
+
sectionType = await siteDefaultSectionType(
|
|
862
|
+
siteDir,
|
|
863
|
+
keepAuthoredFoundation ? null : { foundation: siteDoc?.info?.foundation }
|
|
864
|
+
)
|
|
857
865
|
}
|
|
858
866
|
// ⭐ THE SCOPE THE PULLED RECORDS AND QUERIES WERE QUALIFIED WITH — the pushed site's
|
|
859
867
|
// FOUNDATION's: the scope of the pinned ref it carries (`info.foundation`), else the
|
|
@@ -998,7 +1006,8 @@ export async function pull(args = [], deps = {}) {
|
|
|
998
1006
|
keepAuthoredFoundation,
|
|
999
1007
|
// So a query's `@acme/member` comes back as the author's `@/member`.
|
|
1000
1008
|
scope: pullScope,
|
|
1001
|
-
dataModel
|
|
1009
|
+
dataModel,
|
|
1010
|
+
sectionType
|
|
1002
1011
|
})
|
|
1003
1012
|
wrote.push(...report.pages, ...report.sections, ...report.layout)
|
|
1004
1013
|
removed.push(
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-24T15:06:07.442Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
|
-
"version": "1.8.
|
|
9
|
+
"version": "1.8.2"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.4"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"version": "1.3.0"
|
|
16
16
|
},
|
|
17
17
|
"@uniweb/core": {
|
|
18
|
-
"version": "1.4.
|
|
18
|
+
"version": "1.4.1"
|
|
19
19
|
},
|
|
20
20
|
"@uniweb/frame-bridge": {
|
|
21
21
|
"version": "1.0.2"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"version": "1.0.5"
|
|
25
25
|
},
|
|
26
26
|
"@uniweb/kit": {
|
|
27
|
-
"version": "1.6.
|
|
27
|
+
"version": "1.6.1"
|
|
28
28
|
},
|
|
29
29
|
"@uniweb/loom": {
|
|
30
30
|
"version": "1.1.2"
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"version": "1.1.4"
|
|
34
34
|
},
|
|
35
35
|
"@uniweb/projections": {
|
|
36
|
-
"version": "1.1.
|
|
36
|
+
"version": "1.1.7"
|
|
37
37
|
},
|
|
38
38
|
"@uniweb/runtime": {
|
|
39
|
-
"version": "1.1.
|
|
39
|
+
"version": "1.1.16"
|
|
40
40
|
},
|
|
41
41
|
"@uniweb/scene": {
|
|
42
42
|
"version": "1.0.1"
|
package/src/utils/uwx-read.js
CHANGED
|
@@ -21,12 +21,14 @@
|
|
|
21
21
|
*
|
|
22
22
|
* ## Two things measured on a real payload, both of which would break a naive reader
|
|
23
23
|
*
|
|
24
|
-
* 1. ⛔ **The archive is NOT "stored".** `pull.js`
|
|
25
|
-
* Stored ZIP"
|
|
26
|
-
* files — measured on a live pull: `manifest.json` method 0 (STORED),
|
|
27
|
-
* entity JSON method 8 (DEFLATED). A stored-only reader silently yields the
|
|
24
|
+
* 1. ⛔ **The archive is NOT "stored".** `pull.js` described the format as *"our
|
|
25
|
+
* Stored ZIP"* until 2026-10-23, and that is true of `manifest.json` and false of
|
|
26
|
+
* the entity files — measured on a live pull: `manifest.json` method 0 (STORED),
|
|
27
|
+
* the entity JSON method 8 (DEFLATED). A stored-only reader silently yields the
|
|
28
28
|
* manifest and drops the document, i.e. it returns success and no entities.
|
|
29
|
-
* **Both methods are handled below and the tests cover a mixed archive.**
|
|
29
|
+
* **Both methods are handled below and the tests cover a mixed archive.** Our own
|
|
30
|
+
* writer (`@uniweb/build`'s `createZip`) has Deflated too since that day, so no
|
|
31
|
+
* `.uwx` in either direction is Stored-only.
|
|
30
32
|
* 2. The entity payload is the *only* thing wanted; `manifest.json` is skipped, as
|
|
31
33
|
* every other reader in the tree does.
|
|
32
34
|
*
|