@valbuild/server 0.139.2 → 0.140.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/CHANGELOG.md +72 -0
- package/dist/declarations/src/ValOps.d.ts +48 -3
- package/dist/declarations/src/ValOpsHttp.d.ts +2 -0
- package/dist/declarations/src/patch/jsonValuesPatch.d.ts +25 -0
- package/dist/declarations/src/valServerConfig.d.ts +9 -0
- package/dist/valbuild-server.cjs.dev.js +483 -18
- package/dist/valbuild-server.cjs.prod.js +483 -18
- package/dist/valbuild-server.esm.js +484 -19
- package/package.json +3 -3
|
@@ -1488,17 +1488,22 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1488
1488
|
serializedSchema,
|
|
1489
1489
|
entryKey,
|
|
1490
1490
|
baseContent,
|
|
1491
|
-
patches
|
|
1491
|
+
patches,
|
|
1492
|
+
baseContentOf,
|
|
1493
|
+
stopBefore
|
|
1492
1494
|
} = args;
|
|
1493
1495
|
let content = baseContent;
|
|
1494
1496
|
let deleted = false;
|
|
1495
1497
|
const appliedPatchIds = [];
|
|
1496
|
-
for (const {
|
|
1498
|
+
replay: for (const [patchIndex, {
|
|
1497
1499
|
patchId,
|
|
1498
1500
|
patch: patch$1
|
|
1499
|
-
} of patches) {
|
|
1501
|
+
}] of patches.entries()) {
|
|
1500
1502
|
let touched = false;
|
|
1501
|
-
for (const rawOp of patch$1) {
|
|
1503
|
+
for (const [opIndex, rawOp] of patch$1.entries()) {
|
|
1504
|
+
if (stopBefore && (patchIndex > stopBefore.patch || patchIndex === stopBefore.patch && opIndex >= stopBefore.op)) {
|
|
1505
|
+
break replay;
|
|
1506
|
+
}
|
|
1502
1507
|
/**
|
|
1503
1508
|
* A write of the WHOLE record becomes what it says about THIS entry.
|
|
1504
1509
|
*
|
|
@@ -1523,6 +1528,13 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1523
1528
|
const cls = serializedSchema ? classifyJsonValuesOp(serializedSchema, op.path) : {
|
|
1524
1529
|
kind: "normal"
|
|
1525
1530
|
};
|
|
1531
|
+
if (op.op === "move" && wholeEntryKey(serializedSchema, op.from) === entryKey && wholeEntryKey(serializedSchema, op.path) !== entryKey) {
|
|
1532
|
+
// Moved AWAY: under its old key, the entry is gone.
|
|
1533
|
+
touched = true;
|
|
1534
|
+
content = undefined;
|
|
1535
|
+
deleted = true;
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1526
1538
|
if (cls.kind !== "entry" || cls.recordPath.length > 0 || cls.entryKey !== entryKey) {
|
|
1527
1539
|
continue;
|
|
1528
1540
|
}
|
|
@@ -1568,7 +1580,30 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1568
1580
|
continue;
|
|
1569
1581
|
} else {
|
|
1570
1582
|
// move/copy INTO this key: the content comes from the source entry,
|
|
1571
|
-
//
|
|
1583
|
+
// as it stood at this op (it is a different `*.val.json`).
|
|
1584
|
+
const fromKey = wholeEntryKey(serializedSchema, op.from);
|
|
1585
|
+
const fromBase = fromKey === undefined ? undefined : baseContentOf === null || baseContentOf === void 0 ? void 0 : baseContentOf(fromKey);
|
|
1586
|
+
if (fromKey !== undefined && fromBase !== undefined) {
|
|
1587
|
+
const source = applyJsonValuesEntryPatches({
|
|
1588
|
+
serializedSchema,
|
|
1589
|
+
entryKey: fromKey,
|
|
1590
|
+
baseContent: fromBase.content,
|
|
1591
|
+
patches,
|
|
1592
|
+
baseContentOf,
|
|
1593
|
+
stopBefore: {
|
|
1594
|
+
patch: patchIndex,
|
|
1595
|
+
op: opIndex
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
if (source.kind === "error") {
|
|
1599
|
+
return source;
|
|
1600
|
+
}
|
|
1601
|
+
if (source.kind === "content") {
|
|
1602
|
+
content = patch.deepClone(source.content);
|
|
1603
|
+
deleted = false;
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1572
1607
|
return {
|
|
1573
1608
|
kind: "error",
|
|
1574
1609
|
message: `Cannot resolve '${op.op}' of jsonValues entry '${entryKey}' from its own content`,
|
|
@@ -1620,6 +1655,18 @@ function applyJsonValuesEntryPatches(args) {
|
|
|
1620
1655
|
};
|
|
1621
1656
|
}
|
|
1622
1657
|
|
|
1658
|
+
/**
|
|
1659
|
+
* The entry a path names when it names a WHOLE entry of the root
|
|
1660
|
+
* `.jsonValues()` record, and `undefined` for anything else.
|
|
1661
|
+
*/
|
|
1662
|
+
function wholeEntryKey(serializedSchema, path) {
|
|
1663
|
+
if (!serializedSchema) {
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
const cls = classifyJsonValuesOp(serializedSchema, path);
|
|
1667
|
+
return cls.kind === "entry" && cls.recordPath.length === 0 && cls.subPath.length === 0 ? cls.entryKey : undefined;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1623
1670
|
/**
|
|
1624
1671
|
* Resolves an EXISTING entry's `*.val.json` path (relative to rootDir) from the
|
|
1625
1672
|
* `import(...)` path recorded in the `.val.ts` thunk (from
|
|
@@ -2845,7 +2892,7 @@ function v4Of(high, low) {
|
|
|
2845
2892
|
* each other by the API route's zod schema, which is the only place they meet.
|
|
2846
2893
|
*/
|
|
2847
2894
|
|
|
2848
|
-
const DEFAULT_TIMEOUT_MS$
|
|
2895
|
+
const DEFAULT_TIMEOUT_MS$2 = 5000;
|
|
2849
2896
|
const DEFAULT_MAX_REDIRECTS = 5;
|
|
2850
2897
|
|
|
2851
2898
|
/**
|
|
@@ -2943,7 +2990,7 @@ function needsGetFallback(code) {
|
|
|
2943
2990
|
return code === 405 || code === 501;
|
|
2944
2991
|
}
|
|
2945
2992
|
async function probeUrl(rawUrl, options = {}) {
|
|
2946
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$
|
|
2993
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$2;
|
|
2947
2994
|
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
2948
2995
|
const now = options.now ?? (() => Date.now());
|
|
2949
2996
|
const started = now();
|
|
@@ -3591,7 +3638,7 @@ class ValOps {
|
|
|
3591
3638
|
message: `Could not fetch patches: ${JSON.stringify(patchOps.errors)}`
|
|
3592
3639
|
};
|
|
3593
3640
|
}
|
|
3594
|
-
modulePatches = scopedModulePatches(patchOps.patches, moduleFilePath, opts === null || opts === void 0 ? void 0 : opts.patchIds).map(p => ({
|
|
3641
|
+
modulePatches = scopedModulePatches(patchOps.patches, moduleFilePath, opts === null || opts === void 0 ? void 0 : opts.patchIds, patchOps.commits).map(p => ({
|
|
3595
3642
|
patchId: p.patchId,
|
|
3596
3643
|
patch: p.patch
|
|
3597
3644
|
}));
|
|
@@ -3605,7 +3652,25 @@ class ValOps {
|
|
|
3605
3652
|
const entries = [];
|
|
3606
3653
|
const missing = [];
|
|
3607
3654
|
const errors = [];
|
|
3608
|
-
|
|
3655
|
+
/*
|
|
3656
|
+
* The entries a whole-entry `move` or `copy` reads from. A renamed or
|
|
3657
|
+
* duplicated entry's content is theirs, so they are loaded beside the
|
|
3658
|
+
* ones asked for, and not reported.
|
|
3659
|
+
*/
|
|
3660
|
+
const sourceKeys = new Set();
|
|
3661
|
+
for (const {
|
|
3662
|
+
patch
|
|
3663
|
+
} of modulePatches) {
|
|
3664
|
+
for (const op of patch) {
|
|
3665
|
+
if (op.op === "move" || op.op === "copy") {
|
|
3666
|
+
const fromKey = wholeEntryKey(serializedSchema, op.from);
|
|
3667
|
+
if (fromKey !== undefined && !requestedKeys.includes(fromKey)) {
|
|
3668
|
+
sourceKeys.add(fromKey);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
const loaded = await Promise.all([...requestedKeys, ...sourceKeys].map(async entryKey => {
|
|
3609
3674
|
const marker = record[entryKey];
|
|
3610
3675
|
if (marker !== undefined && !core.Internal.isJson(marker)) {
|
|
3611
3676
|
// Not a jsonValues entry — return the inlined value as-is (defensive).
|
|
@@ -3667,6 +3732,15 @@ class ValOps {
|
|
|
3667
3732
|
};
|
|
3668
3733
|
}
|
|
3669
3734
|
}));
|
|
3735
|
+
const resolved = loaded.slice(0, requestedKeys.length);
|
|
3736
|
+
const bases = new Map();
|
|
3737
|
+
for (const entry of loaded) {
|
|
3738
|
+
if (!("message" in entry) && !("inline" in entry)) {
|
|
3739
|
+
bases.set(entry.entryKey, {
|
|
3740
|
+
content: entry.baseContent
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3670
3744
|
for (const result of resolved) {
|
|
3671
3745
|
const {
|
|
3672
3746
|
entryKey
|
|
@@ -3696,7 +3770,8 @@ class ValOps {
|
|
|
3696
3770
|
serializedSchema,
|
|
3697
3771
|
entryKey,
|
|
3698
3772
|
baseContent,
|
|
3699
|
-
patches: modulePatches
|
|
3773
|
+
patches: modulePatches,
|
|
3774
|
+
baseContentOf: key => bases.get(key)
|
|
3700
3775
|
});
|
|
3701
3776
|
if (res.kind === "error") {
|
|
3702
3777
|
errors.push({
|
|
@@ -5429,8 +5504,10 @@ function formatPatchSourceError(error) {
|
|
|
5429
5504
|
* 1. this module's, since the chain is branch-wide;
|
|
5430
5505
|
* 2. this caller's, when they asked to be scoped. `undefined` is "everything",
|
|
5431
5506
|
* which is what every unscoped caller gets and must keep getting;
|
|
5432
|
-
* 3.
|
|
5433
|
-
* and
|
|
5507
|
+
* 3. a patch published after this build is kept whatever the scope: it is
|
|
5508
|
+
* nobody's to hold back, and this build does not have it. One already in
|
|
5509
|
+
* the build is dropped. `draftOverlay` decides which is which, from
|
|
5510
|
+
* `commits`.
|
|
5434
5511
|
*
|
|
5435
5512
|
* Filtered here rather than by asking `fetchPatches` for a list, and that is
|
|
5436
5513
|
* load-bearing: both implementations read an empty `patchIds` as "no filter"
|
|
@@ -5440,9 +5517,52 @@ function formatPatchSourceError(error) {
|
|
|
5440
5517
|
* instead of base. It costs no round trip either: the whole chain is what the
|
|
5441
5518
|
* unscoped path fetches anyway.
|
|
5442
5519
|
*/
|
|
5443
|
-
function scopedModulePatches(patches, moduleFilePath, patchIds
|
|
5520
|
+
function scopedModulePatches(patches, moduleFilePath, patchIds, /** The commits content says came after this build: see `draftOverlay`. */
|
|
5521
|
+
commits) {
|
|
5444
5522
|
const scope = patchIds && new Set(patchIds);
|
|
5445
|
-
return patches.filter(patch => patch.path === moduleFilePath &&
|
|
5523
|
+
return draftOverlay(patches.filter(patch => patch.path === moduleFilePath && (patch.appliedAt !== null || scope === undefined || scope.has(patch.patchId))), commits);
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
/**
|
|
5527
|
+
* The patches a DRAFT applies on top of this build's own source: the pending
|
|
5528
|
+
* ones, and the ones published AFTER this build, in chain order -- with
|
|
5529
|
+
* `appliedAt` cleared on the latter, because this build has none of them.
|
|
5530
|
+
*
|
|
5531
|
+
* Content places every caller at its own build (`getApplicablePatchesAndCommits`
|
|
5532
|
+
* in valbuild/home: "the earliest the caller has not seen") and returns the
|
|
5533
|
+
* commits after it as `commits`. A patch applied at one of those was
|
|
5534
|
+
* published after this build and is not in it, so a draft has to apply it --
|
|
5535
|
+
* skipping it showed the build's old value from the publish until the next
|
|
5536
|
+
* build went live. `/sources/~` already applies them (`getSources` walks every
|
|
5537
|
+
* patch it is given); the `.jsonValues()` entry path skipped them.
|
|
5538
|
+
*
|
|
5539
|
+
* A patch applied at a commit NOT in that list is already in this build --
|
|
5540
|
+
* content returns one only because it was asked for by id -- and is dropped:
|
|
5541
|
+
* applying it again would apply it twice. Without `commits` (a store that
|
|
5542
|
+
* does not report them) every applied patch is dropped, as before. Only
|
|
5543
|
+
* `ValOpsHttp` ever reports `appliedAt`; `fs` and memory stores report `null`.
|
|
5544
|
+
*
|
|
5545
|
+
* Chain order, not published-first, because that is the order the Studio
|
|
5546
|
+
* applies them in, and a draft page starts from the server's answer and is
|
|
5547
|
+
* then kept up to date by the Studio's: the two must agree.
|
|
5548
|
+
*
|
|
5549
|
+
* For the reads that render a draft only. What to COMMIT still keys on
|
|
5550
|
+
* `appliedAt`: a published patch must never be published again.
|
|
5551
|
+
*/
|
|
5552
|
+
function draftOverlay(patches, commits) {
|
|
5553
|
+
const afterThisBuild = new Set((commits ?? []).map(commit => commit.commitSha));
|
|
5554
|
+
const overlay = [];
|
|
5555
|
+
for (const patch of patches) {
|
|
5556
|
+
if (patch.appliedAt === null) {
|
|
5557
|
+
overlay.push(patch);
|
|
5558
|
+
} else if (afterThisBuild.has(patch.appliedAt.commitSha)) {
|
|
5559
|
+
overlay.push({
|
|
5560
|
+
...patch,
|
|
5561
|
+
appliedAt: null
|
|
5562
|
+
});
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
return overlay;
|
|
5446
5566
|
}
|
|
5447
5567
|
function getFieldsForType(type) {
|
|
5448
5568
|
if (type === "file") {
|
|
@@ -6579,7 +6699,7 @@ function describeLogProblem(problem) {
|
|
|
6579
6699
|
*/
|
|
6580
6700
|
|
|
6581
6701
|
const PATCH_LOCK_FILE_NAME = "patches.lock";
|
|
6582
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
6702
|
+
const DEFAULT_TIMEOUT_MS$1 = 10_000;
|
|
6583
6703
|
const DEFAULT_POLL_MS = 25;
|
|
6584
6704
|
|
|
6585
6705
|
/**
|
|
@@ -6698,7 +6818,7 @@ function tryCreate(lockFilePath, info) {
|
|
|
6698
6818
|
}
|
|
6699
6819
|
async function acquirePatchLock(lockFilePath, options) {
|
|
6700
6820
|
const now = options.now ?? Date.now;
|
|
6701
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
6821
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
6702
6822
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
6703
6823
|
const deadline = now() + timeoutMs;
|
|
6704
6824
|
try {
|
|
@@ -8369,6 +8489,8 @@ const GetApplicablePatches = z.z.object({
|
|
|
8369
8489
|
* Optional for the same reason.
|
|
8370
8490
|
*/
|
|
8371
8491
|
headVersion: z.z.number().optional(),
|
|
8492
|
+
/** Of `patches`, the ones a running publish holds. See `OrderedPatches.publishingPatches`. */
|
|
8493
|
+
publishingPatches: z.z.array(internal.PatchId).optional(),
|
|
8372
8494
|
commits: z.z.array(z.z.object({
|
|
8373
8495
|
commitSha: z.z.string(),
|
|
8374
8496
|
/*
|
|
@@ -9253,6 +9375,13 @@ class ValOpsHttp extends ValOps {
|
|
|
9253
9375
|
deployments: allPatchData.deployments || [],
|
|
9254
9376
|
patches,
|
|
9255
9377
|
appliedPatches,
|
|
9378
|
+
/*
|
|
9379
|
+
* Absent, not empty, from a content service that does not say: an empty
|
|
9380
|
+
* list would claim no publish is running when it cannot know.
|
|
9381
|
+
*/
|
|
9382
|
+
...(allPatchData.publishingPatches !== undefined ? {
|
|
9383
|
+
publishingPatches: allPatchData.publishingPatches
|
|
9384
|
+
} : {}),
|
|
9256
9385
|
/*
|
|
9257
9386
|
* The CHAIN head, not the publish head below. Spread: absent is a content
|
|
9258
9387
|
* service that does not report it, which is not the same as `null`.
|
|
@@ -9620,6 +9749,9 @@ class ValOpsHttp extends ValOps {
|
|
|
9620
9749
|
} : {}),
|
|
9621
9750
|
...(data.headVersion !== undefined ? {
|
|
9622
9751
|
headVersion: data.headVersion
|
|
9752
|
+
} : {}),
|
|
9753
|
+
...(data.publishingPatches !== undefined ? {
|
|
9754
|
+
publishingPatches: data.publishingPatches
|
|
9623
9755
|
} : {})
|
|
9624
9756
|
};
|
|
9625
9757
|
}
|
|
@@ -11969,6 +12101,135 @@ contentUrl) {
|
|
|
11969
12101
|
}
|
|
11970
12102
|
}
|
|
11971
12103
|
|
|
12104
|
+
/**
|
|
12105
|
+
* Forwarding for `/admin/proxy/*`: Val Build's Studio API, `/v1/studio/*` on
|
|
12106
|
+
* the content server, called on the editor's behalf with the token from their
|
|
12107
|
+
* session.
|
|
12108
|
+
*
|
|
12109
|
+
* The web components the Studio mounts from content.val.build cannot call Val
|
|
12110
|
+
* Build themselves. The editor's credential is in this app's httpOnly
|
|
12111
|
+
* `val_session` cookie, and a cookie of admin.val.build's own would be
|
|
12112
|
+
* third-party on this page, which Safari and Firefox block. So they call this
|
|
12113
|
+
* app, and this is where the token is attached.
|
|
12114
|
+
*
|
|
12115
|
+
* It is a pipe for the path, the query and a JSON body, so a new component or
|
|
12116
|
+
* endpoint needs no Val release. It is NOT a pipe for where the request goes:
|
|
12117
|
+
* the URL is built from `contentUrl` (the server's `valContentUrl`) and must
|
|
12118
|
+
* still be under `/v1/studio/` after the URL parser has resolved `..`,
|
|
12119
|
+
* `%2e%2e` and backslashes. The editor's token can reach the endpoints written
|
|
12120
|
+
* for the Studio and nothing else on Val Build.
|
|
12121
|
+
*/
|
|
12122
|
+
|
|
12123
|
+
const STUDIO_API_PREFIX = "/v1/studio/";
|
|
12124
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
12125
|
+
async function forwardToStudioApi({
|
|
12126
|
+
method,
|
|
12127
|
+
path,
|
|
12128
|
+
rawQuery,
|
|
12129
|
+
body,
|
|
12130
|
+
credential,
|
|
12131
|
+
contentUrl,
|
|
12132
|
+
fetchImpl = fetch,
|
|
12133
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
12134
|
+
}) {
|
|
12135
|
+
const url = studioApiUrl(contentUrl, path, rawQuery);
|
|
12136
|
+
if (url === null) {
|
|
12137
|
+
return {
|
|
12138
|
+
status: 404,
|
|
12139
|
+
json: {
|
|
12140
|
+
code: "not-found",
|
|
12141
|
+
message: "No such Val Build endpoint."
|
|
12142
|
+
}
|
|
12143
|
+
};
|
|
12144
|
+
}
|
|
12145
|
+
// Every method the route accepts may carry a JSON body: a DELETE that
|
|
12146
|
+
// names what it removes in its body is as valid as a PUT.
|
|
12147
|
+
const sendsBody = method !== "GET" && body !== undefined;
|
|
12148
|
+
let response;
|
|
12149
|
+
try {
|
|
12150
|
+
response = await fetchImpl(url, {
|
|
12151
|
+
method,
|
|
12152
|
+
headers: {
|
|
12153
|
+
// `x-val-pat` is the header the rest of Val Build reads a PAT from.
|
|
12154
|
+
...("pat" in credential ? {
|
|
12155
|
+
"x-val-pat": credential.pat
|
|
12156
|
+
} : {
|
|
12157
|
+
authorization: `Bearer ${credential.bearer}`
|
|
12158
|
+
}),
|
|
12159
|
+
accept: "application/json",
|
|
12160
|
+
...(sendsBody ? {
|
|
12161
|
+
"content-type": "application/json"
|
|
12162
|
+
} : {})
|
|
12163
|
+
},
|
|
12164
|
+
body: sendsBody ? JSON.stringify(body) : undefined,
|
|
12165
|
+
// Never follow a redirect with the token attached.
|
|
12166
|
+
redirect: "manual",
|
|
12167
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
12168
|
+
});
|
|
12169
|
+
} catch {
|
|
12170
|
+
return {
|
|
12171
|
+
status: 500,
|
|
12172
|
+
json: {
|
|
12173
|
+
code: "unreachable",
|
|
12174
|
+
message: "Could not reach Val Build."
|
|
12175
|
+
}
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
let json;
|
|
12179
|
+
try {
|
|
12180
|
+
json = await response.json();
|
|
12181
|
+
} catch {
|
|
12182
|
+
json = {
|
|
12183
|
+
code: "bad-gateway",
|
|
12184
|
+
message: `Val Build answered ${response.status} without JSON.`
|
|
12185
|
+
};
|
|
12186
|
+
}
|
|
12187
|
+
return {
|
|
12188
|
+
status: mapStatus(response.status),
|
|
12189
|
+
json
|
|
12190
|
+
};
|
|
12191
|
+
}
|
|
12192
|
+
|
|
12193
|
+
/**
|
|
12194
|
+
* `{contentUrl}/v1/studio{path}{rawQuery}`, or null when the result is not
|
|
12195
|
+
* under that prefix on that origin — the check that keeps the proxy from being
|
|
12196
|
+
* pointed anywhere else with the editor's token.
|
|
12197
|
+
*/
|
|
12198
|
+
function studioApiUrl(contentUrl, path, rawQuery) {
|
|
12199
|
+
if (!path.startsWith("/") || rawQuery !== "" && !rawQuery.startsWith("?")) {
|
|
12200
|
+
return null;
|
|
12201
|
+
}
|
|
12202
|
+
let base;
|
|
12203
|
+
let url;
|
|
12204
|
+
try {
|
|
12205
|
+
base = new URL(contentUrl);
|
|
12206
|
+
url = new URL(`${STUDIO_API_PREFIX}${path.slice(1)}${rawQuery}`, base);
|
|
12207
|
+
} catch {
|
|
12208
|
+
return null;
|
|
12209
|
+
}
|
|
12210
|
+
if (url.origin !== base.origin || !url.pathname.startsWith(STUDIO_API_PREFIX)) {
|
|
12211
|
+
return null;
|
|
12212
|
+
}
|
|
12213
|
+
return url.toString();
|
|
12214
|
+
}
|
|
12215
|
+
|
|
12216
|
+
/**
|
|
12217
|
+
* Onto the statuses this server speaks. The JSON is passed through untouched,
|
|
12218
|
+
* so a `code` from Val Build still says what happened.
|
|
12219
|
+
*/
|
|
12220
|
+
function mapStatus(status) {
|
|
12221
|
+
if (status >= 200 && status < 300) {
|
|
12222
|
+
return 200;
|
|
12223
|
+
}
|
|
12224
|
+
if (status === 400 || status === 401 || status === 403 || status === 404) {
|
|
12225
|
+
return status;
|
|
12226
|
+
}
|
|
12227
|
+
if (status >= 400 && status < 500) {
|
|
12228
|
+
return 400;
|
|
12229
|
+
}
|
|
12230
|
+
return 500;
|
|
12231
|
+
}
|
|
12232
|
+
|
|
11972
12233
|
function getPersonalAccessTokenPath(root) {
|
|
11973
12234
|
return path__namespace["default"].join(path__namespace["default"].resolve(root), ".val", "pat.json");
|
|
11974
12235
|
}
|
|
@@ -12512,6 +12773,27 @@ function warnIfInsecureUrls(urls) {
|
|
|
12512
12773
|
* genuinely confusing afternoon.
|
|
12513
12774
|
*/
|
|
12514
12775
|
|
|
12776
|
+
/**
|
|
12777
|
+
* The personal access token `val login` wrote for this project, or null when
|
|
12778
|
+
* there is none (or it does not parse). Only `fs` mode has a working directory
|
|
12779
|
+
* to read it from, so every other mode answers null.
|
|
12780
|
+
*
|
|
12781
|
+
* Unlike `resolveRemoteFileAuth` this never answers with the api key: the
|
|
12782
|
+
* caller wants to act as the DEVELOPER, and an api key is the project.
|
|
12783
|
+
*/
|
|
12784
|
+
async function readValLoginToken(options) {
|
|
12785
|
+
if (options.mode !== "fs") {
|
|
12786
|
+
return null;
|
|
12787
|
+
}
|
|
12788
|
+
const fs = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
|
|
12789
|
+
try {
|
|
12790
|
+
const file = await fs.promises.readFile(getPersonalAccessTokenPath(options.cwd), "utf-8");
|
|
12791
|
+
const parsed = parsePersonalAccessTokenFile(file);
|
|
12792
|
+
return parsed.success ? parsed.data.pat : null;
|
|
12793
|
+
} catch {
|
|
12794
|
+
return null;
|
|
12795
|
+
}
|
|
12796
|
+
}
|
|
12515
12797
|
async function resolveRemoteFileAuth(options) {
|
|
12516
12798
|
if (options.apiKey) {
|
|
12517
12799
|
return {
|
|
@@ -12671,6 +12953,129 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
12671
12953
|
return null;
|
|
12672
12954
|
});
|
|
12673
12955
|
};
|
|
12956
|
+
|
|
12957
|
+
/**
|
|
12958
|
+
* `/admin/proxy/*`, for Val Build's web components in the Studio: forwarded
|
|
12959
|
+
* to the Studio API on the content server. See `adminProxy.ts` for what is
|
|
12960
|
+
* forwarded and why.
|
|
12961
|
+
*
|
|
12962
|
+
* Three refusals before anything leaves this server, and the `code`s are
|
|
12963
|
+
* the ones the components act on:
|
|
12964
|
+
* - no `x-val-studio` header: 403, since a cross-site request cannot set it;
|
|
12965
|
+
* - no Val Build behind this Studio (no `valBuildUrl`, or fs mode without
|
|
12966
|
+
* a `val login`): 404 `not-connected`, which the component shows as such
|
|
12967
|
+
* rather than as an outage — though `/admin/status` keeps the Studio from
|
|
12968
|
+
* mounting it at all in that case;
|
|
12969
|
+
* - a deployed Studio without a valid session: 401 `unauthenticated`, which
|
|
12970
|
+
* the component answers by asking the Studio to sign in.
|
|
12971
|
+
*/
|
|
12972
|
+
const adminProxy = async (method, req) => {
|
|
12973
|
+
if (req.headers[internal.VAL_STUDIO_HEADER] !== "1") {
|
|
12974
|
+
return {
|
|
12975
|
+
status: 403,
|
|
12976
|
+
json: {
|
|
12977
|
+
code: "forbidden",
|
|
12978
|
+
message: `Requests to the Val Build proxy need the ${internal.VAL_STUDIO_HEADER} header.`
|
|
12979
|
+
}
|
|
12980
|
+
};
|
|
12981
|
+
}
|
|
12982
|
+
const credential = await getValBuildCredential(req.cookies);
|
|
12983
|
+
if (credential.status !== "ok" || !options.valBuildUrl) {
|
|
12984
|
+
if (!serverOps.requiresAuth || !options.valBuildUrl) {
|
|
12985
|
+
return {
|
|
12986
|
+
status: 404,
|
|
12987
|
+
json: {
|
|
12988
|
+
code: "not-connected",
|
|
12989
|
+
message: "This Studio is not connected to Val Build."
|
|
12990
|
+
}
|
|
12991
|
+
};
|
|
12992
|
+
}
|
|
12993
|
+
return {
|
|
12994
|
+
status: 401,
|
|
12995
|
+
json: {
|
|
12996
|
+
code: "unauthenticated",
|
|
12997
|
+
message: credential.status === "invalid" ? credential.message : "Sign in to Val Build."
|
|
12998
|
+
}
|
|
12999
|
+
};
|
|
13000
|
+
}
|
|
13001
|
+
// To the content server, which serves the Studio API: Val Build's sign-in
|
|
13002
|
+
// (`valBuildUrl`) is what makes the credential, the content server is
|
|
13003
|
+
// what the components' data comes from.
|
|
13004
|
+
return forwardToStudioApi({
|
|
13005
|
+
method,
|
|
13006
|
+
path: req.path,
|
|
13007
|
+
rawQuery: req.rawQuery,
|
|
13008
|
+
body: req.body,
|
|
13009
|
+
credential: credential.credential,
|
|
13010
|
+
contentUrl: options.valContentUrl
|
|
13011
|
+
});
|
|
13012
|
+
};
|
|
13013
|
+
|
|
13014
|
+
/**
|
|
13015
|
+
* What this Studio can act on Val Build with, for the editor in front of it:
|
|
13016
|
+
*
|
|
13017
|
+
* - the session's token, in a deployed Studio (and in a local one where
|
|
13018
|
+
* someone signed in through the Studio anyway);
|
|
13019
|
+
* - otherwise, in fs mode, the developer's own `val login` token from
|
|
13020
|
+
* `.val/pat.json` — the same file `val validate --fix` reads.
|
|
13021
|
+
*
|
|
13022
|
+
* An api key is never one of them: it is the project, not a person, and
|
|
13023
|
+
* these requests are answered with a person's projects.
|
|
13024
|
+
*/
|
|
13025
|
+
const getValBuildCredential = async cookies => {
|
|
13026
|
+
const session = getSessionToken(cookies);
|
|
13027
|
+
if (session.status === "ok") {
|
|
13028
|
+
return {
|
|
13029
|
+
status: "ok",
|
|
13030
|
+
credential: {
|
|
13031
|
+
bearer: session.token
|
|
13032
|
+
}
|
|
13033
|
+
};
|
|
13034
|
+
}
|
|
13035
|
+
if (options.mode === "fs") {
|
|
13036
|
+
const pat = await readValLoginToken(options);
|
|
13037
|
+
if (pat !== null) {
|
|
13038
|
+
return {
|
|
13039
|
+
status: "ok",
|
|
13040
|
+
credential: {
|
|
13041
|
+
pat
|
|
13042
|
+
}
|
|
13043
|
+
};
|
|
13044
|
+
}
|
|
13045
|
+
return {
|
|
13046
|
+
status: "none"
|
|
13047
|
+
};
|
|
13048
|
+
}
|
|
13049
|
+
return session;
|
|
13050
|
+
};
|
|
13051
|
+
|
|
13052
|
+
/** The editor's Val Build token, from inside their session cookie. */
|
|
13053
|
+
const getSessionToken = cookies => {
|
|
13054
|
+
const cookie = cookies[internal.VAL_SESSION_COOKIE];
|
|
13055
|
+
if (typeof cookie !== "string" || !options.valSecret) {
|
|
13056
|
+
return {
|
|
13057
|
+
status: "none"
|
|
13058
|
+
};
|
|
13059
|
+
}
|
|
13060
|
+
const verified = verifyJwt(cookie, options.valSecret);
|
|
13061
|
+
if (!verified.success) {
|
|
13062
|
+
return {
|
|
13063
|
+
status: "invalid",
|
|
13064
|
+
message: sessionErrorMessage(verified.reason)
|
|
13065
|
+
};
|
|
13066
|
+
}
|
|
13067
|
+
const payload = IntegratedServerJwtPayload.safeParse(verified.data);
|
|
13068
|
+
if (!payload.success) {
|
|
13069
|
+
return {
|
|
13070
|
+
status: "invalid",
|
|
13071
|
+
message: "Session invalid. You will need to login again."
|
|
13072
|
+
};
|
|
13073
|
+
}
|
|
13074
|
+
return {
|
|
13075
|
+
status: "ok",
|
|
13076
|
+
token: payload.data.token
|
|
13077
|
+
};
|
|
13078
|
+
};
|
|
12674
13079
|
const getAuth = cookies => {
|
|
12675
13080
|
const cookie = cookies[internal.VAL_SESSION_COOKIE];
|
|
12676
13081
|
/*
|
|
@@ -14350,6 +14755,20 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14350
14755
|
}
|
|
14351
14756
|
};
|
|
14352
14757
|
}
|
|
14758
|
+
/*
|
|
14759
|
+
* What this build's draft applies: the pending patches and the ones
|
|
14760
|
+
* published AFTER this build, which it does not have -- see
|
|
14761
|
+
* `draftOverlay`. `getSources` already applied both, but
|
|
14762
|
+
* `analyzePatches` skips every patch with `appliedAt`, so a module
|
|
14763
|
+
* changed only by a post-build publish was reported with no
|
|
14764
|
+
* `patches`, and a reader that sends only changed modules (TanStack's
|
|
14765
|
+
* `fetchValDraft`) left it out: the page was rendered with this
|
|
14766
|
+
* build's older value. Chain order, which is what the Studio applies.
|
|
14767
|
+
*/
|
|
14768
|
+
patchOps = {
|
|
14769
|
+
...patchOps,
|
|
14770
|
+
patches: draftOverlay(patchOps.patches, patchOps.commits)
|
|
14771
|
+
};
|
|
14353
14772
|
const patchAnalysis = serverOps.analyzePatches(patchOps.patches);
|
|
14354
14773
|
const schemasRes = await serverOps.getSchemas();
|
|
14355
14774
|
let sourcesRes = await serverOps.getSources();
|
|
@@ -14598,6 +15017,31 @@ const ValServer = (valModules, options, callbacks) => {
|
|
|
14598
15017
|
});
|
|
14599
15018
|
}
|
|
14600
15019
|
},
|
|
15020
|
+
"/admin/status": {
|
|
15021
|
+
GET: async req => {
|
|
15022
|
+
const credential = await getValBuildCredential(req.cookies);
|
|
15023
|
+
return {
|
|
15024
|
+
status: 200,
|
|
15025
|
+
json: {
|
|
15026
|
+
connected: Boolean(options.valBuildUrl) && credential.status === "ok",
|
|
15027
|
+
/*
|
|
15028
|
+
* How an editor signs in to Val Build here, which only the server
|
|
15029
|
+
* knows. NOT the stat `mode`: that says whether patches are local,
|
|
15030
|
+
* and a deployed memory-mode host reports `fs` while having no
|
|
15031
|
+
* working directory to run `val login` in. Only a developer's own
|
|
15032
|
+
* checkout (fs mode) signs in with `val login`.
|
|
15033
|
+
*/
|
|
15034
|
+
signIn: options.mode === "fs" ? "val-login" : "studio"
|
|
15035
|
+
}
|
|
15036
|
+
};
|
|
15037
|
+
}
|
|
15038
|
+
},
|
|
15039
|
+
"/admin/proxy": {
|
|
15040
|
+
GET: req => adminProxy("GET", req),
|
|
15041
|
+
POST: req => adminProxy("POST", req),
|
|
15042
|
+
PUT: req => adminProxy("PUT", req),
|
|
15043
|
+
DELETE: req => adminProxy("DELETE", req)
|
|
15044
|
+
},
|
|
14601
15045
|
"/profiles": {
|
|
14602
15046
|
GET: async req => {
|
|
14603
15047
|
const cookies = req.cookies;
|
|
@@ -17078,6 +17522,13 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
|
17078
17522
|
if (!cookiesRes.success) {
|
|
17079
17523
|
return zodErrorResult(cookiesRes.error, "invalid cookies");
|
|
17080
17524
|
}
|
|
17525
|
+
const headersRes = reqDefinition.headers ? getHeaders(req, reqDefinition.headers) : {
|
|
17526
|
+
success: true,
|
|
17527
|
+
data: {}
|
|
17528
|
+
};
|
|
17529
|
+
if (!headersRes.success) {
|
|
17530
|
+
return zodErrorResult(headersRes.error, "invalid headers");
|
|
17531
|
+
}
|
|
17081
17532
|
const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
|
|
17082
17533
|
let query = {};
|
|
17083
17534
|
if (reqDefinition.query) {
|
|
@@ -17126,6 +17577,8 @@ function createValApiRouter(route, valServerPromise, convert) {
|
|
|
17126
17577
|
res = await endpointImpl({
|
|
17127
17578
|
body: bodyRes.data,
|
|
17128
17579
|
cookies: cookiesRes.data,
|
|
17580
|
+
headers: headersRes.data,
|
|
17581
|
+
rawQuery: reqDefinition.rawQuery ? url.search : undefined,
|
|
17129
17582
|
query,
|
|
17130
17583
|
path
|
|
17131
17584
|
});
|
|
@@ -17222,10 +17675,22 @@ function zodErrorResult(error, message) {
|
|
|
17222
17675
|
}
|
|
17223
17676
|
|
|
17224
17677
|
// TODO: is this naive implementation is too naive?
|
|
17678
|
+
/** The declared headers, by name; `Headers.get` is case-insensitive. */
|
|
17679
|
+
function getHeaders(req, headersDef) {
|
|
17680
|
+
const input = {};
|
|
17681
|
+
for (const name of Object.keys(headersDef)) {
|
|
17682
|
+
var _req$headers;
|
|
17683
|
+
const value = (_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers.get(name);
|
|
17684
|
+
if (value !== null && value !== undefined) {
|
|
17685
|
+
input[name] = value;
|
|
17686
|
+
}
|
|
17687
|
+
}
|
|
17688
|
+
return z.z.object(headersDef).safeParse(input);
|
|
17689
|
+
}
|
|
17225
17690
|
function getCookies(req, cookiesDef) {
|
|
17226
|
-
var _req$
|
|
17691
|
+
var _req$headers2;
|
|
17227
17692
|
const input = {};
|
|
17228
|
-
const cookieParts = (_req$
|
|
17693
|
+
const cookieParts = (_req$headers2 = req.headers) === null || _req$headers2 === void 0 || (_req$headers2 = _req$headers2.get("Cookie")) === null || _req$headers2 === void 0 ? void 0 : _req$headers2.split("; ");
|
|
17229
17694
|
for (const name of Object.keys(cookiesDef)) {
|
|
17230
17695
|
const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
|
|
17231
17696
|
const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;
|