@valbuild/server 0.139.1 → 0.140.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.
@@ -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
- // which the caller must resolve (it is a different `*.val.json`).
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$1 = 5000;
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$1;
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
- const resolved = await Promise.all(requestedKeys.map(async entryKey => {
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. not already applied — a fact about this path rather than about scoping,
5433
- * and true with or without a scope.
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 && !patch.appliedAt && (scope === undefined || scope.has(patch.patchId)));
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 {
@@ -11969,6 +12089,135 @@ contentUrl) {
11969
12089
  }
11970
12090
  }
11971
12091
 
12092
+ /**
12093
+ * Forwarding for `/admin/proxy/*`: Val Build's Studio API, `/v1/studio/*` on
12094
+ * the content server, called on the editor's behalf with the token from their
12095
+ * session.
12096
+ *
12097
+ * The web components the Studio mounts from content.val.build cannot call Val
12098
+ * Build themselves. The editor's credential is in this app's httpOnly
12099
+ * `val_session` cookie, and a cookie of admin.val.build's own would be
12100
+ * third-party on this page, which Safari and Firefox block. So they call this
12101
+ * app, and this is where the token is attached.
12102
+ *
12103
+ * It is a pipe for the path, the query and a JSON body, so a new component or
12104
+ * endpoint needs no Val release. It is NOT a pipe for where the request goes:
12105
+ * the URL is built from `contentUrl` (the server's `valContentUrl`) and must
12106
+ * still be under `/v1/studio/` after the URL parser has resolved `..`,
12107
+ * `%2e%2e` and backslashes. The editor's token can reach the endpoints written
12108
+ * for the Studio and nothing else on Val Build.
12109
+ */
12110
+
12111
+ const STUDIO_API_PREFIX = "/v1/studio/";
12112
+ const DEFAULT_TIMEOUT_MS = 10_000;
12113
+ async function forwardToStudioApi({
12114
+ method,
12115
+ path,
12116
+ rawQuery,
12117
+ body,
12118
+ credential,
12119
+ contentUrl,
12120
+ fetchImpl = fetch,
12121
+ timeoutMs = DEFAULT_TIMEOUT_MS
12122
+ }) {
12123
+ const url = studioApiUrl(contentUrl, path, rawQuery);
12124
+ if (url === null) {
12125
+ return {
12126
+ status: 404,
12127
+ json: {
12128
+ code: "not-found",
12129
+ message: "No such Val Build endpoint."
12130
+ }
12131
+ };
12132
+ }
12133
+ // Every method the route accepts may carry a JSON body: a DELETE that
12134
+ // names what it removes in its body is as valid as a PUT.
12135
+ const sendsBody = method !== "GET" && body !== undefined;
12136
+ let response;
12137
+ try {
12138
+ response = await fetchImpl(url, {
12139
+ method,
12140
+ headers: {
12141
+ // `x-val-pat` is the header the rest of Val Build reads a PAT from.
12142
+ ...("pat" in credential ? {
12143
+ "x-val-pat": credential.pat
12144
+ } : {
12145
+ authorization: `Bearer ${credential.bearer}`
12146
+ }),
12147
+ accept: "application/json",
12148
+ ...(sendsBody ? {
12149
+ "content-type": "application/json"
12150
+ } : {})
12151
+ },
12152
+ body: sendsBody ? JSON.stringify(body) : undefined,
12153
+ // Never follow a redirect with the token attached.
12154
+ redirect: "manual",
12155
+ signal: AbortSignal.timeout(timeoutMs)
12156
+ });
12157
+ } catch {
12158
+ return {
12159
+ status: 500,
12160
+ json: {
12161
+ code: "unreachable",
12162
+ message: "Could not reach Val Build."
12163
+ }
12164
+ };
12165
+ }
12166
+ let json;
12167
+ try {
12168
+ json = await response.json();
12169
+ } catch {
12170
+ json = {
12171
+ code: "bad-gateway",
12172
+ message: `Val Build answered ${response.status} without JSON.`
12173
+ };
12174
+ }
12175
+ return {
12176
+ status: mapStatus(response.status),
12177
+ json
12178
+ };
12179
+ }
12180
+
12181
+ /**
12182
+ * `{contentUrl}/v1/studio{path}{rawQuery}`, or null when the result is not
12183
+ * under that prefix on that origin — the check that keeps the proxy from being
12184
+ * pointed anywhere else with the editor's token.
12185
+ */
12186
+ function studioApiUrl(contentUrl, path, rawQuery) {
12187
+ if (!path.startsWith("/") || rawQuery !== "" && !rawQuery.startsWith("?")) {
12188
+ return null;
12189
+ }
12190
+ let base;
12191
+ let url;
12192
+ try {
12193
+ base = new URL(contentUrl);
12194
+ url = new URL(`${STUDIO_API_PREFIX}${path.slice(1)}${rawQuery}`, base);
12195
+ } catch {
12196
+ return null;
12197
+ }
12198
+ if (url.origin !== base.origin || !url.pathname.startsWith(STUDIO_API_PREFIX)) {
12199
+ return null;
12200
+ }
12201
+ return url.toString();
12202
+ }
12203
+
12204
+ /**
12205
+ * Onto the statuses this server speaks. The JSON is passed through untouched,
12206
+ * so a `code` from Val Build still says what happened.
12207
+ */
12208
+ function mapStatus(status) {
12209
+ if (status >= 200 && status < 300) {
12210
+ return 200;
12211
+ }
12212
+ if (status === 400 || status === 401 || status === 403 || status === 404) {
12213
+ return status;
12214
+ }
12215
+ if (status >= 400 && status < 500) {
12216
+ return 400;
12217
+ }
12218
+ return 500;
12219
+ }
12220
+
11972
12221
  function getPersonalAccessTokenPath(root) {
11973
12222
  return path__namespace["default"].join(path__namespace["default"].resolve(root), ".val", "pat.json");
11974
12223
  }
@@ -12512,6 +12761,27 @@ function warnIfInsecureUrls(urls) {
12512
12761
  * genuinely confusing afternoon.
12513
12762
  */
12514
12763
 
12764
+ /**
12765
+ * The personal access token `val login` wrote for this project, or null when
12766
+ * there is none (or it does not parse). Only `fs` mode has a working directory
12767
+ * to read it from, so every other mode answers null.
12768
+ *
12769
+ * Unlike `resolveRemoteFileAuth` this never answers with the api key: the
12770
+ * caller wants to act as the DEVELOPER, and an api key is the project.
12771
+ */
12772
+ async function readValLoginToken(options) {
12773
+ if (options.mode !== "fs") {
12774
+ return null;
12775
+ }
12776
+ const fs = await Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); });
12777
+ try {
12778
+ const file = await fs.promises.readFile(getPersonalAccessTokenPath(options.cwd), "utf-8");
12779
+ const parsed = parsePersonalAccessTokenFile(file);
12780
+ return parsed.success ? parsed.data.pat : null;
12781
+ } catch {
12782
+ return null;
12783
+ }
12784
+ }
12515
12785
  async function resolveRemoteFileAuth(options) {
12516
12786
  if (options.apiKey) {
12517
12787
  return {
@@ -12671,6 +12941,129 @@ const ValServer = (valModules, options, callbacks) => {
12671
12941
  return null;
12672
12942
  });
12673
12943
  };
12944
+
12945
+ /**
12946
+ * `/admin/proxy/*`, for Val Build's web components in the Studio: forwarded
12947
+ * to the Studio API on the content server. See `adminProxy.ts` for what is
12948
+ * forwarded and why.
12949
+ *
12950
+ * Three refusals before anything leaves this server, and the `code`s are
12951
+ * the ones the components act on:
12952
+ * - no `x-val-studio` header: 403, since a cross-site request cannot set it;
12953
+ * - no Val Build behind this Studio (no `valBuildUrl`, or fs mode without
12954
+ * a `val login`): 404 `not-connected`, which the component shows as such
12955
+ * rather than as an outage — though `/admin/status` keeps the Studio from
12956
+ * mounting it at all in that case;
12957
+ * - a deployed Studio without a valid session: 401 `unauthenticated`, which
12958
+ * the component answers by asking the Studio to sign in.
12959
+ */
12960
+ const adminProxy = async (method, req) => {
12961
+ if (req.headers[internal.VAL_STUDIO_HEADER] !== "1") {
12962
+ return {
12963
+ status: 403,
12964
+ json: {
12965
+ code: "forbidden",
12966
+ message: `Requests to the Val Build proxy need the ${internal.VAL_STUDIO_HEADER} header.`
12967
+ }
12968
+ };
12969
+ }
12970
+ const credential = await getValBuildCredential(req.cookies);
12971
+ if (credential.status !== "ok" || !options.valBuildUrl) {
12972
+ if (!serverOps.requiresAuth || !options.valBuildUrl) {
12973
+ return {
12974
+ status: 404,
12975
+ json: {
12976
+ code: "not-connected",
12977
+ message: "This Studio is not connected to Val Build."
12978
+ }
12979
+ };
12980
+ }
12981
+ return {
12982
+ status: 401,
12983
+ json: {
12984
+ code: "unauthenticated",
12985
+ message: credential.status === "invalid" ? credential.message : "Sign in to Val Build."
12986
+ }
12987
+ };
12988
+ }
12989
+ // To the content server, which serves the Studio API: Val Build's sign-in
12990
+ // (`valBuildUrl`) is what makes the credential, the content server is
12991
+ // what the components' data comes from.
12992
+ return forwardToStudioApi({
12993
+ method,
12994
+ path: req.path,
12995
+ rawQuery: req.rawQuery,
12996
+ body: req.body,
12997
+ credential: credential.credential,
12998
+ contentUrl: options.valContentUrl
12999
+ });
13000
+ };
13001
+
13002
+ /**
13003
+ * What this Studio can act on Val Build with, for the editor in front of it:
13004
+ *
13005
+ * - the session's token, in a deployed Studio (and in a local one where
13006
+ * someone signed in through the Studio anyway);
13007
+ * - otherwise, in fs mode, the developer's own `val login` token from
13008
+ * `.val/pat.json` — the same file `val validate --fix` reads.
13009
+ *
13010
+ * An api key is never one of them: it is the project, not a person, and
13011
+ * these requests are answered with a person's projects.
13012
+ */
13013
+ const getValBuildCredential = async cookies => {
13014
+ const session = getSessionToken(cookies);
13015
+ if (session.status === "ok") {
13016
+ return {
13017
+ status: "ok",
13018
+ credential: {
13019
+ bearer: session.token
13020
+ }
13021
+ };
13022
+ }
13023
+ if (options.mode === "fs") {
13024
+ const pat = await readValLoginToken(options);
13025
+ if (pat !== null) {
13026
+ return {
13027
+ status: "ok",
13028
+ credential: {
13029
+ pat
13030
+ }
13031
+ };
13032
+ }
13033
+ return {
13034
+ status: "none"
13035
+ };
13036
+ }
13037
+ return session;
13038
+ };
13039
+
13040
+ /** The editor's Val Build token, from inside their session cookie. */
13041
+ const getSessionToken = cookies => {
13042
+ const cookie = cookies[internal.VAL_SESSION_COOKIE];
13043
+ if (typeof cookie !== "string" || !options.valSecret) {
13044
+ return {
13045
+ status: "none"
13046
+ };
13047
+ }
13048
+ const verified = verifyJwt(cookie, options.valSecret);
13049
+ if (!verified.success) {
13050
+ return {
13051
+ status: "invalid",
13052
+ message: sessionErrorMessage(verified.reason)
13053
+ };
13054
+ }
13055
+ const payload = IntegratedServerJwtPayload.safeParse(verified.data);
13056
+ if (!payload.success) {
13057
+ return {
13058
+ status: "invalid",
13059
+ message: "Session invalid. You will need to login again."
13060
+ };
13061
+ }
13062
+ return {
13063
+ status: "ok",
13064
+ token: payload.data.token
13065
+ };
13066
+ };
12674
13067
  const getAuth = cookies => {
12675
13068
  const cookie = cookies[internal.VAL_SESSION_COOKIE];
12676
13069
  /*
@@ -14350,6 +14743,20 @@ const ValServer = (valModules, options, callbacks) => {
14350
14743
  }
14351
14744
  };
14352
14745
  }
14746
+ /*
14747
+ * What this build's draft applies: the pending patches and the ones
14748
+ * published AFTER this build, which it does not have -- see
14749
+ * `draftOverlay`. `getSources` already applied both, but
14750
+ * `analyzePatches` skips every patch with `appliedAt`, so a module
14751
+ * changed only by a post-build publish was reported with no
14752
+ * `patches`, and a reader that sends only changed modules (TanStack's
14753
+ * `fetchValDraft`) left it out: the page was rendered with this
14754
+ * build's older value. Chain order, which is what the Studio applies.
14755
+ */
14756
+ patchOps = {
14757
+ ...patchOps,
14758
+ patches: draftOverlay(patchOps.patches, patchOps.commits)
14759
+ };
14353
14760
  const patchAnalysis = serverOps.analyzePatches(patchOps.patches);
14354
14761
  const schemasRes = await serverOps.getSchemas();
14355
14762
  let sourcesRes = await serverOps.getSources();
@@ -14598,6 +15005,31 @@ const ValServer = (valModules, options, callbacks) => {
14598
15005
  });
14599
15006
  }
14600
15007
  },
15008
+ "/admin/status": {
15009
+ GET: async req => {
15010
+ const credential = await getValBuildCredential(req.cookies);
15011
+ return {
15012
+ status: 200,
15013
+ json: {
15014
+ connected: Boolean(options.valBuildUrl) && credential.status === "ok",
15015
+ /*
15016
+ * How an editor signs in to Val Build here, which only the server
15017
+ * knows. NOT the stat `mode`: that says whether patches are local,
15018
+ * and a deployed memory-mode host reports `fs` while having no
15019
+ * working directory to run `val login` in. Only a developer's own
15020
+ * checkout (fs mode) signs in with `val login`.
15021
+ */
15022
+ signIn: options.mode === "fs" ? "val-login" : "studio"
15023
+ }
15024
+ };
15025
+ }
15026
+ },
15027
+ "/admin/proxy": {
15028
+ GET: req => adminProxy("GET", req),
15029
+ POST: req => adminProxy("POST", req),
15030
+ PUT: req => adminProxy("PUT", req),
15031
+ DELETE: req => adminProxy("DELETE", req)
15032
+ },
14601
15033
  "/profiles": {
14602
15034
  GET: async req => {
14603
15035
  const cookies = req.cookies;
@@ -17078,6 +17510,13 @@ function createValApiRouter(route, valServerPromise, convert) {
17078
17510
  if (!cookiesRes.success) {
17079
17511
  return zodErrorResult(cookiesRes.error, "invalid cookies");
17080
17512
  }
17513
+ const headersRes = reqDefinition.headers ? getHeaders(req, reqDefinition.headers) : {
17514
+ success: true,
17515
+ data: {}
17516
+ };
17517
+ if (!headersRes.success) {
17518
+ return zodErrorResult(headersRes.error, "invalid headers");
17519
+ }
17081
17520
  const actualQueryParams = groupQueryParams(Array.from(url.searchParams.entries()));
17082
17521
  let query = {};
17083
17522
  if (reqDefinition.query) {
@@ -17126,6 +17565,8 @@ function createValApiRouter(route, valServerPromise, convert) {
17126
17565
  res = await endpointImpl({
17127
17566
  body: bodyRes.data,
17128
17567
  cookies: cookiesRes.data,
17568
+ headers: headersRes.data,
17569
+ rawQuery: reqDefinition.rawQuery ? url.search : undefined,
17129
17570
  query,
17130
17571
  path
17131
17572
  });
@@ -17222,10 +17663,22 @@ function zodErrorResult(error, message) {
17222
17663
  }
17223
17664
 
17224
17665
  // TODO: is this naive implementation is too naive?
17666
+ /** The declared headers, by name; `Headers.get` is case-insensitive. */
17667
+ function getHeaders(req, headersDef) {
17668
+ const input = {};
17669
+ for (const name of Object.keys(headersDef)) {
17670
+ var _req$headers;
17671
+ const value = (_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers.get(name);
17672
+ if (value !== null && value !== undefined) {
17673
+ input[name] = value;
17674
+ }
17675
+ }
17676
+ return z.z.object(headersDef).safeParse(input);
17677
+ }
17225
17678
  function getCookies(req, cookiesDef) {
17226
- var _req$headers;
17679
+ var _req$headers2;
17227
17680
  const input = {};
17228
- const cookieParts = (_req$headers = req.headers) === null || _req$headers === void 0 || (_req$headers = _req$headers.get("Cookie")) === null || _req$headers === void 0 ? void 0 : _req$headers.split("; ");
17681
+ 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
17682
  for (const name of Object.keys(cookiesDef)) {
17230
17683
  const cookie = cookieParts === null || cookieParts === void 0 ? void 0 : cookieParts.find(cookie => cookie.startsWith(`${name}=`));
17231
17684
  const value = cookie ? decodeURIComponent(cookie === null || cookie === void 0 ? void 0 : cookie.split("=")[1]) : undefined;