@augurworks/augur 0.15.2 → 0.15.4
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/INSTALL.md +3 -2
- package/README.md +4 -2
- package/agents/README.md +7 -6
- package/agents/drafts.md +14 -6
- package/agents/prototype-contract.md +1 -1
- package/agents/publishing.md +19 -16
- package/build.js +1 -97
- package/package.json +1 -1
- package/scripts/cli.mjs +11 -5
- package/scripts/clone.mjs +0 -20
- package/scripts/connect.mjs +57 -4
- package/scripts/hook.mjs +2 -1
- package/scripts/init.mjs +2 -2
- package/scripts/land.mjs +4 -1
- package/scripts/lib/adapters.mjs +23 -3
- package/scripts/lib/draft.mjs +76 -8
- package/scripts/no-tenant-globals.mjs +16 -0
- package/scripts/open.mjs +10 -7
- package/scripts/publish.mjs +19 -0
- package/scripts/read.mjs +13 -6
- package/scripts/save.mjs +3 -1
- package/scripts/status.mjs +5 -23
- package/scripts/sync.mjs +3 -1
- package/src/_worker.js +161 -314
- package/src/galleries.mjs +400 -0
- package/src/state-inventory.mjs +0 -4
- package/src/unit-object.mjs +22 -4
- package/agents/working-marks.md +0 -86
- package/scripts/lib/marks.mjs +0 -107
- package/scripts/mark.mjs +0 -112
- package/scripts/ship.mjs +0 -460
package/src/_worker.js
CHANGED
|
@@ -104,6 +104,10 @@ import { PURGED_AUTHOR, purgeThreads, idCollisions } from "./purge.mjs";
|
|
|
104
104
|
import { authoredUnits, unitOfPath, unitPaths } from "./publish-units.mjs";
|
|
105
105
|
import { composePublish, forkLanded } from "./publish-compose.mjs";
|
|
106
106
|
import { normUnit, splitDraftPath, unitTable, draftAddress, DRAFT_ID_RE } from "./unit-core.mjs";
|
|
107
|
+
import {
|
|
108
|
+
siteModel, derivedPathKind, renderRootIndex, renderOpportunityIndex, renderPlaygroundIndex,
|
|
109
|
+
renderTierIndex, renderComponentsIndex, searchIndex, catalogFrom,
|
|
110
|
+
} from "./galleries.mjs";
|
|
107
111
|
// `F-fork-verb`. Fork as a deliberate verb — one unit aliased to a new path, zero bytes
|
|
108
112
|
// moved — plus the rule that keeps a fork's lineage and owner alive across every later
|
|
109
113
|
// publish. Both are pure, and both are the CLI's too if it ever needs them.
|
|
@@ -4333,7 +4337,7 @@ function resolveBundlePath(manifests, pathname) {
|
|
|
4333
4337
|
// against. It is the first argument for the same reason `loadTenantContext` takes it
|
|
4334
4338
|
// first: everything below reads one workspace's store, and a function that had to guess
|
|
4335
4339
|
// would resolve a path against whatever the isolate happened to hold.
|
|
4336
|
-
async function assetFetch(tenantId, env, request) {
|
|
4340
|
+
async function assetFetch(tenantId, env, request, opts = {}) {
|
|
4337
4341
|
if (!bundleMode(env)) return env.ASSETS.fetch(request);
|
|
4338
4342
|
const url = new URL(request.url);
|
|
4339
4343
|
// ── a draft address: the draft's table, not the manifest ─────────────────────────
|
|
@@ -4352,6 +4356,22 @@ async function assetFetch(tenantId, env, request) {
|
|
|
4352
4356
|
return blobResponse(env, request, f);
|
|
4353
4357
|
}
|
|
4354
4358
|
const manifests = await loadManifests(tenantId, env);
|
|
4359
|
+
// ── a design-system draft, across the site (`?ds=<draft>`, §5) ──────────────────
|
|
4360
|
+
// A path under the workspace's declared skill folder resolves from THAT draft's table
|
|
4361
|
+
// when the request carries one, so a shared change can be looked at on every prototype
|
|
4362
|
+
// before it lands. Only the skill's own paths: a prototype's files never come from it,
|
|
4363
|
+
// and a draft that does not hold the file falls through to main.
|
|
4364
|
+
if (opts.dsDraft && DRAFT_ID_RE.test(opts.dsDraft) && decoded) {
|
|
4365
|
+
const skill = Object.values(manifests)
|
|
4366
|
+
.flatMap((m) => ((m && m.routing) || {}).publicSkillPrefixes || [])
|
|
4367
|
+
.map(normUnit).find((p) => p && decoded.startsWith(p));
|
|
4368
|
+
const stub = skill ? unitStub(env, tenantId, skill) : null;
|
|
4369
|
+
if (stub) {
|
|
4370
|
+
const dr = await unitCall(stub, `/draft/${opts.dsDraft}`, null, "GET");
|
|
4371
|
+
const f = dr.status === 200 && dr.body.table ? dr.body.table[decoded] : null;
|
|
4372
|
+
if (f) return blobResponse(env, request, f);
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4355
4375
|
const r = resolveBundlePath(manifests, url.pathname);
|
|
4356
4376
|
if (r.redirect) return Response.redirect(new URL(r.redirect + url.search, url).toString(), 308);
|
|
4357
4377
|
if (r.miss) return new Response("Not Found", { status: 404 });
|
|
@@ -4783,6 +4803,9 @@ function publishRefusalBody(refusal) {
|
|
|
4783
4803
|
"viewer-role": "This account can look around but not publish.",
|
|
4784
4804
|
"not-an-admin": "This token was minted for an admin and this account is no longer one. Run `augur login` again for a token scoped to what it may still publish.",
|
|
4785
4805
|
}[refusal];
|
|
4806
|
+
// `unknown-token` and `wrong-space` stay a bare `forbidden` ON PURPOSE: a guessed token
|
|
4807
|
+
// must not learn whether it exists somewhere. The sentence a person needs — pair again —
|
|
4808
|
+
// is said by the CLI, which knows it just used a saved token (scripts/lib/draft.mjs).
|
|
4786
4809
|
return message ? { error: "forbidden", message } : { error: "forbidden" };
|
|
4787
4810
|
}
|
|
4788
4811
|
|
|
@@ -5029,21 +5052,23 @@ const UNIT_CT_RE = /^[\w.+-]+\/[\w.+-]+(; ?charset=[\w-]+)?$/i;
|
|
|
5029
5052
|
// to anonymous visitors, from a draft on a folder nobody edits.
|
|
5030
5053
|
const isNewUnitPath = (unit) => unit.split("/").filter(Boolean).length === 2;
|
|
5031
5054
|
// Top-level folders a NEW unit may not be created under. A landing replaces a unit's
|
|
5032
|
-
// folder wholesale, so
|
|
5033
|
-
//
|
|
5034
|
-
//
|
|
5035
|
-
//
|
|
5036
|
-
//
|
|
5037
|
-
//
|
|
5038
|
-
//
|
|
5055
|
+
// folder wholesale, so the folders whose contents are GENERATED — the tokens page, the
|
|
5056
|
+
// primitives gallery, the changelog, the search index, fonts, engine chrome (`_`, `__`) —
|
|
5057
|
+
// are never a unit. The four library tiers are NOT here: `/components/button/` is one
|
|
5058
|
+
// demo folder, a unit like any prototype, and the tier's own index is a sibling file the
|
|
5059
|
+
// derived renderer owns. `skills` stays reserved with ONE exception, decided where the
|
|
5060
|
+
// workspace's declared design system is known (`unitApi`): the skill folder the space
|
|
5061
|
+
// names is the design-system unit. Stateless and the same on every workspace. A unit the
|
|
5062
|
+
// manifest ALREADY declares is never held to this: the manifest is the authority.
|
|
5039
5063
|
const RESERVED_UNIT_FOLDERS = Object.freeze([
|
|
5040
|
-
"
|
|
5041
|
-
"admin", "changelog", "search",
|
|
5064
|
+
"skills", "tokens", "primitives", "fonts", "tracks", "admin", "changelog", "search",
|
|
5042
5065
|
]);
|
|
5043
5066
|
const isReservedUnitFolder = (unit) => {
|
|
5044
5067
|
const first = unit.split("/").filter(Boolean)[0] || "";
|
|
5045
5068
|
return first.startsWith("_") || RESERVED_UNIT_FOLDERS.includes(first);
|
|
5046
5069
|
};
|
|
5070
|
+
/** The one unit allowed under `skills/`: the design system the workspace declares. */
|
|
5071
|
+
const isDeclaredSkillUnit = (unit, tctx) => ((tctx && tctx.PUBLIC_SKILL_PREFIXES) || []).some((p) => normUnit(p) === unit);
|
|
5047
5072
|
const shortText = (s, n) => String(s == null ? "" : s).slice(0, n);
|
|
5048
5073
|
// `tctx.SPACES` is a routing field, filled from the live manifests by `loadTenantContext`
|
|
5049
5074
|
// on the request's normal path — which this route also goes through, since `handleRequest`
|
|
@@ -5077,7 +5102,12 @@ function defaultSpaceIdFromManifests(manifests) {
|
|
|
5077
5102
|
* rule the commit handler already keeps. `unitSources` records the landing so the old
|
|
5078
5103
|
* composed publish treats the unit as somebody's work rather than as a fast-forward.
|
|
5079
5104
|
*/
|
|
5080
|
-
|
|
5105
|
+
// Every landing in a space writes the one manifest by compare-and-set. Three attempts
|
|
5106
|
+
// with no pause between them lost 2 of 8 landings made in the same second (measured live,
|
|
5107
|
+
// 6 Sep 2026); six attempts with a little jitter between them is what a burst of agents
|
|
5108
|
+
// landing at once needs. The CLI lands again on `manifest-contended` as well.
|
|
5109
|
+
const UNIT_LANDING_ATTEMPTS = 6;
|
|
5110
|
+
const landingBackoff = (attempt) => new Promise((r) => setTimeout(r, 30 + Math.random() * 90 * (attempt + 1)));
|
|
5081
5111
|
async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, now) {
|
|
5082
5112
|
const bundles = bundlesFor(env, tctx.tenantId);
|
|
5083
5113
|
const key = `spaces/${spaceId}/manifest.json`;
|
|
@@ -5140,7 +5170,11 @@ async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, n
|
|
|
5140
5170
|
// A unit the manifest already declares keeps the entry it has, spelling and all: the
|
|
5141
5171
|
// set union that replaced it added a second, normalized entry beside one written
|
|
5142
5172
|
// without its trailing slash, and two entries for one unit is two things to prune.
|
|
5143
|
-
|
|
5173
|
+
// The design-system unit is the one landing that adds NO public prefix: its public
|
|
5174
|
+
// surface is the rendered-asset rule `publicSkillPrefixes` already spells, and a whole
|
|
5175
|
+
// skill folder opened to the gate would expose the documents that rule keeps gated.
|
|
5176
|
+
const skillUnit = ((cur.routing || {}).publicSkillPrefixes || []).some((p) => normUnit(p) === unit);
|
|
5177
|
+
routing.publicPrefixes = authoredUnits(cur).has(unit) || skillUnit
|
|
5144
5178
|
? [...(routing.publicPrefixes || [])]
|
|
5145
5179
|
: [...(routing.publicPrefixes || []), unit];
|
|
5146
5180
|
routing.unitSources = { ...(routing.unitSources || {}), [unit]: { sha: null, dirty: false, landed: true, by: who.personId, at: now } };
|
|
@@ -5158,7 +5192,7 @@ async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, n
|
|
|
5158
5192
|
// A store that answers `null` refused the precondition — R2's way of saying the object
|
|
5159
5193
|
// moved under us. Anything else is a write.
|
|
5160
5194
|
const wrote = await bundles.put(key, JSON.stringify(out), etag ? { onlyIf: { etagMatches: etag } } : undefined);
|
|
5161
|
-
if (etag && wrote === null) { bustManifests(tctx.tenantId); continue; }
|
|
5195
|
+
if (etag && wrote === null) { bustManifests(tctx.tenantId); await landingBackoff(attempt); continue; }
|
|
5162
5196
|
// THE BYTES ARE LIVE FROM HERE: the manifest is the pointer visitors follow. The version
|
|
5163
5197
|
// document is the rollback record, and a store failure writing it must not turn a landing
|
|
5164
5198
|
// that already happened into a reported failure with no lease release — it is logged as
|
|
@@ -5249,6 +5283,12 @@ async function unitCaller(tctx, request, env, spaceId) {
|
|
|
5249
5283
|
return { who: { personId: personId(me.email), label: me.email }, session: session || "browser" };
|
|
5250
5284
|
}
|
|
5251
5285
|
|
|
5286
|
+
/** A `draft-closed` answer names who landed it by id; the roster puts the face on it. */
|
|
5287
|
+
function closedFace(tctx, body) {
|
|
5288
|
+
if (!body || body.error !== "draft-closed" || !body.by) return body;
|
|
5289
|
+
return { ...body, ...personFace(tctx.USERS, body.by) };
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5252
5292
|
async function unitApi(tctx, request, url, env) {
|
|
5253
5293
|
const verb = url.pathname.slice(UNIT_API_PREFIX.length);
|
|
5254
5294
|
if (!/^[a-z-]+$/.test(verb)) return jsonResponse({ error: "bad-path" }, 400);
|
|
@@ -5297,7 +5337,7 @@ async function unitApi(tctx, request, url, env) {
|
|
|
5297
5337
|
// reached, so a folder that is not one is never given a draft to be adopted from later.
|
|
5298
5338
|
// A unit the space ALREADY publishes is a unit whatever its shape (the manifest is the
|
|
5299
5339
|
// authority on what exists); anything else has to be the shape a new one is created in.
|
|
5300
|
-
if (!authoredUnits(live).has(unit)) {
|
|
5340
|
+
if (!authoredUnits(live).has(unit) && !isDeclaredSkillUnit(unit, tctx)) {
|
|
5301
5341
|
if (!isNewUnitPath(unit)) return jsonResponse({ error: "bad-unit", reason: "not-a-prototype-folder" }, 400);
|
|
5302
5342
|
if (isReservedUnitFolder(unit)) return jsonResponse({ error: "bad-unit", reason: "reserved-folder" }, 400);
|
|
5303
5343
|
}
|
|
@@ -5373,13 +5413,13 @@ async function unitApi(tctx, request, url, env) {
|
|
|
5373
5413
|
}
|
|
5374
5414
|
if (missing.length) return jsonResponse({ error: "missing-blobs", missing: [...new Set(missing)] }, 409);
|
|
5375
5415
|
const r = await unitCall(stub, "/save", { draftId: body.draftId, draftRevision: body.draftRevision, changes, baseRevision: body.baseRevision, at: now });
|
|
5376
|
-
return jsonResponse(r.body, r.status);
|
|
5416
|
+
return jsonResponse(closedFace(tctx, r.body), r.status);
|
|
5377
5417
|
}
|
|
5378
5418
|
if (verb === "land" || verb === "restore") {
|
|
5379
5419
|
const r = await unitCall(stub, `/${verb}`, verb === "land"
|
|
5380
5420
|
? { draftId: body.draftId, baseRevision: body.baseRevision, at: now }
|
|
5381
5421
|
: { revision: body.revision, at: now });
|
|
5382
|
-
if (r.status !== 200) return jsonResponse(r.body, r.status);
|
|
5422
|
+
if (r.status !== 200) return jsonResponse(closedFace(tctx, r.body), r.status);
|
|
5383
5423
|
const written = await writeUnitLanding(tctx, env, spaceId, unit, r.body.table, r.body.changed, who, now);
|
|
5384
5424
|
if (written.error) {
|
|
5385
5425
|
await unitCall(stub, "/abandon-land", { lease: r.body.lease });
|
|
@@ -5418,7 +5458,7 @@ async function unitApi(tctx, request, url, env) {
|
|
|
5418
5458
|
if (verb === "sync" || verb === "discard") {
|
|
5419
5459
|
const r = await unitCall(stub, `/${verb}`, { draftId: body.draftId, at: now });
|
|
5420
5460
|
if (verb === "discard" && r.status === 200) await noteUnitDrafts(tctx, env, stub, unit, now);
|
|
5421
|
-
return jsonResponse(r.body, r.status);
|
|
5461
|
+
return jsonResponse(closedFace(tctx, r.body), r.status);
|
|
5422
5462
|
}
|
|
5423
5463
|
return jsonResponse({ error: "unknown-verb" }, 404);
|
|
5424
5464
|
}
|
|
@@ -5427,46 +5467,6 @@ async function publishApi(tctx, request, url, env) {
|
|
|
5427
5467
|
const [spaceId, op, arg] = url.pathname.slice("/__publish/".length).split("/");
|
|
5428
5468
|
if (!spaceId || !op || !/^[a-z0-9_][a-z0-9-]*$/.test(spaceId)) return jsonResponse({ error: "bad-path" }, 400);
|
|
5429
5469
|
|
|
5430
|
-
// ── working marks (`F-presence-marks`) ─────────────────────────────────────
|
|
5431
|
-
//
|
|
5432
|
-
// AHEAD OF THE BUNDLE-STORE GUARD, deliberately: a mark is not published content and
|
|
5433
|
-
// holds nothing the store knows about, so an instance serving from ASSETS — `augur dev`,
|
|
5434
|
-
// `npm run offline`, a raw engine build — is exactly where two agents most need to stay
|
|
5435
|
-
// out of each other's way, and 501 there would be an accident of where the check sits.
|
|
5436
|
-
//
|
|
5437
|
-
// ANY VALID PUBLISH TOKEN, whatever its scope. A mark names a path, not a space, and a
|
|
5438
|
-
// space-scoped token holder is precisely the person whose work-start is worth announcing.
|
|
5439
|
-
// The capability gate still applies, so a restricted credential (the control plane's
|
|
5440
|
-
// purge token) reaches this no more than it reaches anything else.
|
|
5441
|
-
if (spaceId === "_marks") {
|
|
5442
|
-
const a = await publishAuthDetailed(tctx, request, env, spaceId, true);
|
|
5443
|
-
if (!a.entry) return jsonResponse(publishRefusalBody(a.refusal), 403);
|
|
5444
|
-
if (capabilityRefusal(a.entry, spaceId, op)) {
|
|
5445
|
-
return jsonResponse({ error: "forbidden", reason: "capability-not-granted" }, 403);
|
|
5446
|
-
}
|
|
5447
|
-
// WHO, from the credential and never from the body. `augur login` labels a token with
|
|
5448
|
-
// the holder's address; a token an admin minted by hand carries whatever they typed,
|
|
5449
|
-
// which hashes to a stable id that resolves to no roster face — honest, and better
|
|
5450
|
-
// than letting the caller name itself.
|
|
5451
|
-
const who = { personId: personId(a.entry.label || "") };
|
|
5452
|
-
if (op === "list" && request.method === "GET") {
|
|
5453
|
-
return jsonResponse({ ...(await readMarks(tctx, env)), ttlMs: MARK_TTL_MS, maxTtlMs: MARK_TTL_MAX_MS });
|
|
5454
|
-
}
|
|
5455
|
-
let body = null;
|
|
5456
|
-
if (request.method === "POST") {
|
|
5457
|
-
try { body = await request.json(); } catch (e) { return jsonResponse({ error: "bad-json" }, 400); }
|
|
5458
|
-
}
|
|
5459
|
-
if (op === "set" && request.method === "POST") {
|
|
5460
|
-
const out = await writeMark(tctx, env, who, { path: body && body.path, ttl: body && body.ttl });
|
|
5461
|
-
return out.error ? jsonResponse(out, out.error === "bad-input" ? 400 : 503) : jsonResponse(out);
|
|
5462
|
-
}
|
|
5463
|
-
if (op === "clear" && request.method === "POST") {
|
|
5464
|
-
const out = await clearMark(tctx, env, who, { path: body && body.path });
|
|
5465
|
-
return out.error ? jsonResponse(out, out.error === "bad-input" ? 400 : 503) : jsonResponse(out);
|
|
5466
|
-
}
|
|
5467
|
-
return jsonResponse({ error: "unknown-op" }, 400);
|
|
5468
|
-
}
|
|
5469
|
-
|
|
5470
5470
|
if (!env.BUNDLES) return jsonResponse({ error: "bundle-store-not-configured" }, 501);
|
|
5471
5471
|
// This workspace's view of the store. `blobs/` is content-addressed and shared, so the
|
|
5472
5472
|
// three `blobs/…` operations below deliberately keep using the binding directly — the
|
|
@@ -7238,6 +7238,94 @@ async function composeChrome(tctx, res, url) {
|
|
|
7238
7238
|
return new Response(html, { status: res.status, statusText: res.statusText, headers });
|
|
7239
7239
|
}
|
|
7240
7240
|
|
|
7241
|
+
// ---- Derived pages (drafts that land, §6.4) -------------------------------------
|
|
7242
|
+
// Where drafts are served, the gallery, each opportunity's index, the playground, the
|
|
7243
|
+
// library tiers and the finder's index are RENDERED HERE from the live store, so a landing
|
|
7244
|
+
// is on them at once and no client build ships them. The stored copies a publish once
|
|
7245
|
+
// baked are ignored on this path — they cannot know about a landing. Where drafts are not
|
|
7246
|
+
// served, nothing here runs and the stored pages serve exactly as before.
|
|
7247
|
+
//
|
|
7248
|
+
// Two store reads feed a render besides the manifest — the status baseline
|
|
7249
|
+
// (`/prototype-status.json`) and the design-system catalog (`/registry.json`) — cached per
|
|
7250
|
+
// manifest version; the status OVERLAY is read fresh every time, one get, as the status
|
|
7251
|
+
// route itself reads it. Per-request work after that is string rendering.
|
|
7252
|
+
const DERIVED = tenantCache("derived");
|
|
7253
|
+
const DERIVED_PATH_RE = /^\/(?:[^/]+\/?(?:index\.html)?|index\.html|__search\.json)?$/;
|
|
7254
|
+
async function derivedInputs(tctx, env, spaceId, manifest) {
|
|
7255
|
+
const slot = DERIVED.entry(tctx.tenantId, () => ({ key: null, baseline: {}, catalog: {} }));
|
|
7256
|
+
const key = `${spaceId}:${manifest.version}`;
|
|
7257
|
+
if (slot.key === key) return slot;
|
|
7258
|
+
const readJson = async (p) => {
|
|
7259
|
+
const f = (manifest.files || {})[p];
|
|
7260
|
+
if (!f || !env.BUNDLES) return null;
|
|
7261
|
+
try {
|
|
7262
|
+
const obj = await env.BUNDLES.get("blobs/" + f.h);
|
|
7263
|
+
return obj ? JSON.parse(await obj.text()) : null;
|
|
7264
|
+
} catch (e) { return null; }
|
|
7265
|
+
};
|
|
7266
|
+
const base = await readJson("/prototype-status.json");
|
|
7267
|
+
const baseline = {};
|
|
7268
|
+
for (const [k, v] of Object.entries(base && typeof base === "object" ? base : {})) {
|
|
7269
|
+
if (!k.startsWith("_") && typeof v === "string") baseline[k] = v;
|
|
7270
|
+
}
|
|
7271
|
+
slot.baseline = baseline;
|
|
7272
|
+
slot.catalog = catalogFrom(await readJson("/registry.json"));
|
|
7273
|
+
slot.key = key;
|
|
7274
|
+
return slot;
|
|
7275
|
+
}
|
|
7276
|
+
/** A derived page for this path, or null when the path is not one (or drafts are not served here). */
|
|
7277
|
+
async function derivedPage(tctx, env, url) {
|
|
7278
|
+
if (!draftsServedHere(env) || !DERIVED_PATH_RE.test(url.pathname)) return null;
|
|
7279
|
+
const manifests = await loadManifests(tctx.tenantId, env);
|
|
7280
|
+
const spaceId = defaultSpaceIdFromCtx(tctx) || defaultSpaceIdFromManifests(manifests);
|
|
7281
|
+
const manifest = spaceId ? manifests[spaceId] : null;
|
|
7282
|
+
if (!manifest || !manifest.files) return null;
|
|
7283
|
+
const inputs = await derivedInputs(tctx, env, spaceId, manifest);
|
|
7284
|
+
const store = overlayFor(env, tctx);
|
|
7285
|
+
let statuses = {};
|
|
7286
|
+
try { statuses = store ? (await store.read("statuses")) || {} : {}; } catch (e) { statuses = {}; }
|
|
7287
|
+
// A recorded author resolves to a face through the roster; an id nobody answers to
|
|
7288
|
+
// (an ex-member, the `live` adoption) is nobody's face rather than a blank chip.
|
|
7289
|
+
const people = (id) => { const f = personFace(tctx.USERS, id); return f.name ? { id, ...f } : null; };
|
|
7290
|
+
const now = Date.now();
|
|
7291
|
+
const model = siteModel({ manifest, statuses, baseline: inputs.baseline, people, now });
|
|
7292
|
+
const kind = derivedPathKind(url.pathname, model);
|
|
7293
|
+
if (!kind) return null;
|
|
7294
|
+
const sp = (tctx.SPACES || []).find((s) => s.id === spaceId) || {};
|
|
7295
|
+
const ctx = { spaces: tctx.SPACES || [], activeSpace: spaceId, chrome: tctx.CHROME_POINTER || null, projectsLabel: sp.projectsLabel || "", now };
|
|
7296
|
+
if (kind.kind === "search") return jsonResponse(searchIndex(model, ctx), 200, { "Cache-Control": "no-cache" });
|
|
7297
|
+
if (kind.kind === "opportunity" && !kind.slash) return Response.redirect(new URL(`/${encodeURIComponent(kind.name)}/${url.search}`, url).toString(), 308);
|
|
7298
|
+
const html = kind.kind === "root" ? renderRootIndex(model, ctx)
|
|
7299
|
+
: kind.kind === "opportunity" ? renderOpportunityIndex(model, kind.name, ctx)
|
|
7300
|
+
: kind.kind === "playground" ? renderPlaygroundIndex(model, ctx)
|
|
7301
|
+
: kind.kind === "tier" ? renderTierIndex(model, kind.tier, ctx)
|
|
7302
|
+
: kind.kind === "components" ? renderComponentsIndex(model, inputs.catalog, ctx)
|
|
7303
|
+
: null;
|
|
7304
|
+
if (html == null) return null;
|
|
7305
|
+
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": ASSET_REVALIDATE } });
|
|
7306
|
+
}
|
|
7307
|
+
|
|
7308
|
+
// ---- The design-system overlay cookie (`?ds=`) --------------------------------
|
|
7309
|
+
// `?ds=<draft>` on any page names a design-system draft to look through; the cookie keeps
|
|
7310
|
+
// it while the person navigates; `?ds=` with nothing drops it. A malformed id is nobody's
|
|
7311
|
+
// draft and touches no cookie. Members only — the branch that reads this is past the gate.
|
|
7312
|
+
const DS_COOKIE = "augur_ds";
|
|
7313
|
+
function dsOverlay(request, url) {
|
|
7314
|
+
if (url.searchParams.has("ds")) {
|
|
7315
|
+
const v = url.searchParams.get("ds") || "";
|
|
7316
|
+
if (v === "") return { draft: null, setCookie: `${DS_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax; Secure; HttpOnly` };
|
|
7317
|
+
if (!DRAFT_ID_RE.test(v)) return { draft: null, setCookie: null };
|
|
7318
|
+
return { draft: v, setCookie: `${DS_COOKIE}=${v}; Path=/; SameSite=Lax; Secure; HttpOnly` };
|
|
7319
|
+
}
|
|
7320
|
+
const c = cookieValue(request.headers.get("Cookie") || "", DS_COOKIE);
|
|
7321
|
+
return { draft: c && DRAFT_ID_RE.test(c) ? c : null, setCookie: null };
|
|
7322
|
+
}
|
|
7323
|
+
function withSetCookie(res, cookie) {
|
|
7324
|
+
const out = new Response(res.body, res);
|
|
7325
|
+
out.headers.append("Set-Cookie", cookie);
|
|
7326
|
+
return out;
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7241
7329
|
// ---- The draft bar (drafts that land, §5) -------------------------------------
|
|
7242
7330
|
// A prototype is raw HTML from a space and carries no chrome; a draft has no build step
|
|
7243
7331
|
// that could bake one in. So a member looking at a unit's page — main or a draft address —
|
|
@@ -7835,16 +7923,6 @@ const OVERLAY_KV_KEYS = Object.freeze({
|
|
|
7835
7923
|
boards: Object.freeze({ doc: "board", layout: "keyed", workspaceScoped: true }),
|
|
7836
7924
|
// Canvas image METADATA — the bytes are in R2. See assetApi.
|
|
7837
7925
|
assets: Object.freeze({ doc: "basset-meta", layout: "keyed" }),
|
|
7838
|
-
// WORKING MARKS — "something is editing here right now". One row per path, and the row
|
|
7839
|
-
// is only meaningful until its own TTL runs out. See the marks section below.
|
|
7840
|
-
//
|
|
7841
|
-
// `map` RATHER THAN `keyed`, and the trade is worth naming because it looks backwards.
|
|
7842
|
-
// Keyed would give one KV document per path, so two marks written in the same window
|
|
7843
|
-
// could not lose each other. It would also turn every READ into a kv.list plus a get per
|
|
7844
|
-
// row — and marks are read by a gallery page as well as by the CLI, which puts a listing
|
|
7845
|
-
// in front of ordinary page loads on a store whose daily get budget has been exhausted
|
|
7846
|
-
// before. One document is one get. What it costs is stated on `writeMark`.
|
|
7847
|
-
marks: Object.freeze({ doc: "marks", layout: "map" }),
|
|
7848
7926
|
// WHICH UNITS HAVE OPEN DRAFTS — a hint the gallery reads so it can ask the right unit
|
|
7849
7927
|
// objects and no others (`draftsIndexApi`). One row per unit, `{n, at}`, written from
|
|
7850
7928
|
// the object's own presence answer after open, land and discard. NEVER THE TRUTH: the
|
|
@@ -9055,11 +9133,12 @@ function doorText(f) {
|
|
|
9055
9133
|
+ ` ${f.connect}\n\n`
|
|
9056
9134
|
+ `It prints a link and a code. The owner of this workspace opens the link in a\n`
|
|
9057
9135
|
+ `browser they are already signed in to and enters the code. The token lands on\n`
|
|
9058
|
-
+ `that machine, and
|
|
9136
|
+
+ `that machine, and every verb below uses it from then on. Talking to that person\n`
|
|
9137
|
+
+ `through messages rather than a terminal? Add --no-wait: it prints the line and\n`
|
|
9138
|
+
+ `exits; run the same command again after they approve and the token is collected.\n`
|
|
9139
|
+
+ `With no\n`
|
|
9059
9140
|
+ `source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches one (it reads\n`
|
|
9060
9141
|
+ `the origin from the pairing).\n\n`
|
|
9061
|
-
+ `Not on npm yet? The engine clone sits next to every workspace that publishes:\n\n`
|
|
9062
|
-
+ ` node <engine>/scripts/cli.mjs connect --origin ${f.origin}\n\n`
|
|
9063
9142
|
: `Device pairing is switched off on this workspace. Ask an admin for an invite;\n`
|
|
9064
9143
|
+ `once you have signed in, \`augur login\` (email and password, meant for CI)\n`
|
|
9065
9144
|
+ `trades that for a publish token.\n\n`;
|
|
@@ -10221,235 +10300,6 @@ async function nameApi(tctx, request, url, env) {
|
|
|
10221
10300
|
return jsonResponse({ error: "method-not-allowed" }, 405);
|
|
10222
10301
|
}
|
|
10223
10302
|
|
|
10224
|
-
// ---- Working marks (KV-backed, single key) ----------------------------------
|
|
10225
|
-
//
|
|
10226
|
-
// `F-presence-marks`. Nothing anywhere said what was already being worked on. Two
|
|
10227
|
-
// collaborators' tools — usually two agents, on two machines, told to improve "the
|
|
10228
|
-
// checkout flow" — would each open the same folder, each edit it, and find out at publish
|
|
10229
|
-
// time, where the answer is a fork and a conflict file nobody asked for.
|
|
10230
|
-
//
|
|
10231
|
-
// ⚠️ THIS IS DELIBERATELY NOT A LOCK, and every line below is written so it cannot become
|
|
10232
|
-
// one. A mark REFUSES NOTHING. It is not consulted by the gate, by the publish handler, by
|
|
10233
|
-
// the commit CAS or by anything else that could say no. It is a note left where the next
|
|
10234
|
-
// reader will look, and the whole protocol is: write one before you start, read them
|
|
10235
|
-
// before you start. Enforcement when coordination fails is the composed publish's job
|
|
10236
|
-
// (`src/publish-compose.mjs`), which is the only place in this engine allowed to refuse a
|
|
10237
|
-
// write over a collision — and it does it on evidence, after the fact, never on a claim.
|
|
10238
|
-
//
|
|
10239
|
-
// THE PROTOCOL IS AGENT-FIRST. The badge a person sees on a gallery card is the byproduct,
|
|
10240
|
-
// not the point: as an agent's edit shrinks toward seconds, a mark is FELT almost never and
|
|
10241
|
-
// READ always. So the write side is the CLI (`augur mark`, over a publish token, see the
|
|
10242
|
-
// `_marks` branch in publishApi) and the browser side is read-only — a person editing in a
|
|
10243
|
-
// tab is not running a work-start step and inventing one for them would be a lie about who
|
|
10244
|
-
// wrote what.
|
|
10245
|
-
//
|
|
10246
|
-
// ⚠️ A MARK EXPIRES BY ITSELF AND IS NEVER TRUSTED TO BE CLEARED. The thing that leaves a
|
|
10247
|
-
// mark is a process that can be killed — Ctrl-C, an OOM, a laptop lid — and a claim that
|
|
10248
|
-
// outlives the claimant is worse than no claim at all, because the next reader believes it.
|
|
10249
|
-
// So EXPIRY IS A READ-TIME FILTER (`liveMarks`), not a cleanup job: the moment `startedAt +
|
|
10250
|
-
// ttl` is in the past the mark is gone from every answer, whether or not anything ever runs
|
|
10251
|
-
// again. `sweepExpired` below only reclaims the BYTES, opportunistically, and correctness
|
|
10252
|
-
// never depends on it having run.
|
|
10253
|
-
|
|
10254
|
-
/** How long a mark is good for when the caller does not say. */
|
|
10255
|
-
const MARK_TTL_MS = 10 * 60_000;
|
|
10256
|
-
/**
|
|
10257
|
-
* The longest a caller may ask for. An agent that wants four hours is describing a lock,
|
|
10258
|
-
* and the answer to a lock is a shorter mark re-written as the work continues.
|
|
10259
|
-
*/
|
|
10260
|
-
const MARK_TTL_MAX_MS = 60 * 60_000;
|
|
10261
|
-
/** The shortest, so a `--ttl 0` cannot write a mark that is already dead. */
|
|
10262
|
-
const MARK_TTL_MIN_MS = 5_000;
|
|
10263
|
-
/**
|
|
10264
|
-
* How many lapsed rows one write may reclaim. Bounded low because on the KV backing each
|
|
10265
|
-
* row delete is a whole-document read and put, so the sweep can cost more than the litter
|
|
10266
|
-
* it collects; an unbounded one would turn a work-start step into a hundred writes on a
|
|
10267
|
-
* workspace nobody has marked in a month. Nothing depends on it running at all.
|
|
10268
|
-
*/
|
|
10269
|
-
const MARK_SWEEP_MAX = 4;
|
|
10270
|
-
/** Belt and braces: a workspace cannot be filled with marks by a loop. */
|
|
10271
|
-
const MARK_MAX_ROWS = 200;
|
|
10272
|
-
|
|
10273
|
-
/**
|
|
10274
|
-
* One spelling of a path, so two tools that mean the same folder agree.
|
|
10275
|
-
*
|
|
10276
|
-
* Leading and trailing slash, always: a mark names a UNIT — the prototype folder a URL
|
|
10277
|
-
* names and a person edits — and `unitOfPath` in src/publish-units.mjs decides containment
|
|
10278
|
-
* by prefix, which only works when a folder ends in a slash. `/a/b` and `/a/bc/` would
|
|
10279
|
-
* otherwise overlap.
|
|
10280
|
-
*/
|
|
10281
|
-
function normalizeMarkPath(p) {
|
|
10282
|
-
const s = clamp(p, 300);
|
|
10283
|
-
if (!s) return "";
|
|
10284
|
-
const trimmed = s.trim().replace(/^\.\//, "").replace(/\/{2,}/g, "/");
|
|
10285
|
-
if (!trimmed || trimmed === "/") return "/";
|
|
10286
|
-
return `/${trimmed.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
|
|
10287
|
-
}
|
|
10288
|
-
|
|
10289
|
-
/**
|
|
10290
|
-
* Do these two paths describe overlapping work? Containment in either direction — a mark
|
|
10291
|
-
* on `/checkout/` covers `/checkout/step-two/`, and a mark on `/checkout/step-two/` is
|
|
10292
|
-
* worth showing to somebody about to take `/checkout/`.
|
|
10293
|
-
*/
|
|
10294
|
-
function markPathsOverlap(a, b) {
|
|
10295
|
-
const x = normalizeMarkPath(a), y = normalizeMarkPath(b);
|
|
10296
|
-
if (!x || !y) return false;
|
|
10297
|
-
return x === y || x.startsWith(y) || y.startsWith(x);
|
|
10298
|
-
}
|
|
10299
|
-
|
|
10300
|
-
/** The instant a mark stops meaning anything. Pure, and the only definition of expiry. */
|
|
10301
|
-
function markExpiresAt(m) {
|
|
10302
|
-
const started = Date.parse((m && m.startedAt) || "");
|
|
10303
|
-
if (!Number.isFinite(started)) return 0;
|
|
10304
|
-
const ttl = Number.isFinite(+(m && m.ttl)) ? +m.ttl : MARK_TTL_MS;
|
|
10305
|
-
return started + Math.min(Math.max(ttl, MARK_TTL_MIN_MS), MARK_TTL_MAX_MS);
|
|
10306
|
-
}
|
|
10307
|
-
|
|
10308
|
-
/**
|
|
10309
|
-
* The live marks in a stored map, newest first. THE expiry rule — every reader goes
|
|
10310
|
-
* through here, so a lapsed mark cannot be reported by one surface and hidden by another.
|
|
10311
|
-
*/
|
|
10312
|
-
function liveMarks(map, now = Date.now()) {
|
|
10313
|
-
return Object.entries(map || {})
|
|
10314
|
-
.map(([path, m]) => (m && typeof m === "object" ? { ...m, path: m.path || path } : null))
|
|
10315
|
-
.filter((m) => m && markExpiresAt(m) > now)
|
|
10316
|
-
.sort((a, b) => Date.parse(b.startedAt || 0) - Date.parse(a.startedAt || 0));
|
|
10317
|
-
}
|
|
10318
|
-
|
|
10319
|
-
/**
|
|
10320
|
-
* What a mark looks like on the wire: the stored row plus two things a reader would
|
|
10321
|
-
* otherwise have to compute, and one it could not — the display name behind the id.
|
|
10322
|
-
*
|
|
10323
|
-
* The NAME IS RESOLVED, NEVER STORED. `personId` is the same one-way hash a comment
|
|
10324
|
-
* carries, so a mark holds no address; the roster turns it back into a face at read time,
|
|
10325
|
-
* which also means a rename shows through and an ex-member resolves to nobody.
|
|
10326
|
-
*/
|
|
10327
|
-
function decorateMark(m, users, now = Date.now()) {
|
|
10328
|
-
const u = (users || []).find((x) => x && personId(x.email) === m.personId);
|
|
10329
|
-
return {
|
|
10330
|
-
path: m.path,
|
|
10331
|
-
personId: m.personId || null,
|
|
10332
|
-
startedAt: m.startedAt,
|
|
10333
|
-
ttl: markExpiresAt(m) - Date.parse(m.startedAt),
|
|
10334
|
-
by: u ? u.name || nameFromEmail(u.email) : null,
|
|
10335
|
-
initials: u ? u.initials || initialsFor(u.name || u.email) : null,
|
|
10336
|
-
color: u ? u.color || null : null,
|
|
10337
|
-
expiresIn: Math.max(0, markExpiresAt(m) - now),
|
|
10338
|
-
};
|
|
10339
|
-
}
|
|
10340
|
-
|
|
10341
|
-
/** Read the live marks for a workspace, decorated. `null` store answers with nothing. */
|
|
10342
|
-
async function readMarks(tctx, env) {
|
|
10343
|
-
const store = overlayFor(env, tctx);
|
|
10344
|
-
if (!store) return { marks: [], warning: "no-kv-binding" };
|
|
10345
|
-
const map = await store.read("marks");
|
|
10346
|
-
const now = Date.now();
|
|
10347
|
-
return { marks: liveMarks(map, now).map((m) => decorateMark(m, tctx.USERS, now)), now };
|
|
10348
|
-
}
|
|
10349
|
-
|
|
10350
|
-
/**
|
|
10351
|
-
* Reclaim the bytes of rows that lapsed. NOT the expiry mechanism — `liveMarks` already
|
|
10352
|
-
* stopped reporting these, and this runs only so a one-way author id does not sit in the
|
|
10353
|
-
* store for months after it stopped meaning anything. Per-key deletes, never a whole-family
|
|
10354
|
-
* `replace`: a replace computed from a read taken moments ago would drop a mark another
|
|
10355
|
-
* agent wrote in between, and on the workspace object it would delete rows it never read.
|
|
10356
|
-
*
|
|
10357
|
-
* It swallows its own failures on purpose. Reclaiming bytes may never be the reason a
|
|
10358
|
-
* work-start step reports a failure, because the mark it was announcing is already written.
|
|
10359
|
-
*/
|
|
10360
|
-
async function sweepExpired(store, map, now, keep) {
|
|
10361
|
-
let swept = 0;
|
|
10362
|
-
for (const [path, m] of Object.entries(map || {})) {
|
|
10363
|
-
if (swept >= MARK_SWEEP_MAX) break;
|
|
10364
|
-
if (path === keep) continue;
|
|
10365
|
-
if (markExpiresAt(m) > now) continue;
|
|
10366
|
-
try { await store.set("marks", "", path, null); swept++; } catch (e) { break; }
|
|
10367
|
-
}
|
|
10368
|
-
return swept;
|
|
10369
|
-
}
|
|
10370
|
-
|
|
10371
|
-
/**
|
|
10372
|
-
* Write one mark. `who` is resolved by the caller from a credential — a session or a
|
|
10373
|
-
* publish token — and NEVER from the request body, exactly like a comment's authorship.
|
|
10374
|
-
*
|
|
10375
|
-
* ⚠️ ON THE KV BACKING TWO MARKS WRITTEN IN THE SAME WINDOW CAN LOSE EACH OTHER, and that
|
|
10376
|
-
* is a known cost rather than an oversight. A `map` family is one document: `set` reads it,
|
|
10377
|
-
* changes one key and puts it back, so a mark written between the read and the put is
|
|
10378
|
-
* overwritten — and KV reads converge globally rather than instantly, which makes the
|
|
10379
|
-
* window as wide as the convergence, not as wide as the round trip. The overlay's own
|
|
10380
|
-
* header says the same thing about statuses, names and pins; the workspace object closes
|
|
10381
|
-
* it for all of them at once by making each key a row.
|
|
10382
|
-
*
|
|
10383
|
-
* WHY IT IS SURVIVABLE HERE AND WOULD NOT BE IN A LOCK. A lost mark costs the next reader
|
|
10384
|
-
* a hint. It cannot cost anybody work, because nothing anywhere asks a mark for permission:
|
|
10385
|
-
* the loser of the race is still editing, still publishing, and still protected by the
|
|
10386
|
-
* composed publish, which settles a real collision on evidence. A lock that lost a write
|
|
10387
|
-
* would hand two writers the same exclusive claim, which is why this is not one.
|
|
10388
|
-
*/
|
|
10389
|
-
async function writeMark(tctx, env, who, { path, ttl }) {
|
|
10390
|
-
const store = overlayFor(env, tctx);
|
|
10391
|
-
if (!store) return { error: "no-kv-binding" };
|
|
10392
|
-
const p = normalizeMarkPath(path);
|
|
10393
|
-
if (!p) return { error: "bad-input" };
|
|
10394
|
-
const ms = Number.isFinite(+ttl) && +ttl > 0
|
|
10395
|
-
? Math.min(Math.max(+ttl, MARK_TTL_MIN_MS), MARK_TTL_MAX_MS)
|
|
10396
|
-
: MARK_TTL_MS;
|
|
10397
|
-
const now = Date.now();
|
|
10398
|
-
const before = await store.read("marks");
|
|
10399
|
-
// The only refusal on this route, and it is a runaway-loop guard rather than a policy:
|
|
10400
|
-
// this many things being worked on at once in one workspace is a script, not a team.
|
|
10401
|
-
const live = liveMarks(before, now);
|
|
10402
|
-
if (live.length >= MARK_MAX_ROWS && !live.some((m) => m.path === p)) {
|
|
10403
|
-
return { error: "too-many-marks" };
|
|
10404
|
-
}
|
|
10405
|
-
// ⚠️ THE ROW CARRIES NO ADDRESS — not in the value and not in the `owner` column. A mark
|
|
10406
|
-
// is read by more things than a comment thread is (a gallery page stamps a badge from
|
|
10407
|
-
// it), and `personId` is exactly enough to put a face on it.
|
|
10408
|
-
const mark = { path: p, personId: who.personId, startedAt: new Date(now).toISOString(), ttl: ms };
|
|
10409
|
-
await store.set("marks", "", p, mark, null);
|
|
10410
|
-
const swept = await sweepExpired(store, before, now, p);
|
|
10411
|
-
// The answer is COMPUTED from what was just written, never re-read. A second read costs
|
|
10412
|
-
// a round trip to say what this function already knows, and on KV it can come back
|
|
10413
|
-
// STALER than the write it was meant to confirm — a work-start step that printed "your
|
|
10414
|
-
// mark is not there" right after writing it would teach people to distrust the tool.
|
|
10415
|
-
const marks = [mark, ...live.filter((m) => m.path !== p)];
|
|
10416
|
-
return {
|
|
10417
|
-
mark: decorateMark(mark, tctx.USERS, now),
|
|
10418
|
-
marks: marks.map((m) => decorateMark(m, tctx.USERS, now)),
|
|
10419
|
-
swept,
|
|
10420
|
-
};
|
|
10421
|
-
}
|
|
10422
|
-
|
|
10423
|
-
/**
|
|
10424
|
-
* Release a mark early. A COURTESY, never the guarantee — the TTL is the guarantee, and a
|
|
10425
|
-
* tool that is killed never reaches this. Only the mark's own author may clear it: taking
|
|
10426
|
-
* somebody else's mark down would turn the note into something worth fighting over.
|
|
10427
|
-
*/
|
|
10428
|
-
async function clearMark(tctx, env, who, { path }) {
|
|
10429
|
-
const store = overlayFor(env, tctx);
|
|
10430
|
-
if (!store) return { error: "no-kv-binding" };
|
|
10431
|
-
const p = normalizeMarkPath(path);
|
|
10432
|
-
if (!p) return { error: "bad-input" };
|
|
10433
|
-
const map = await store.read("marks");
|
|
10434
|
-
const cur = (map || {})[p];
|
|
10435
|
-
const now = Date.now();
|
|
10436
|
-
if (!cur || markExpiresAt(cur) <= now) return { cleared: false, reason: "no-mark" };
|
|
10437
|
-
if (cur.personId !== who.personId) return { cleared: false, reason: "not-yours" };
|
|
10438
|
-
await store.set("marks", "", p, null);
|
|
10439
|
-
const marks = liveMarks(map, now).filter((m) => m.path !== p);
|
|
10440
|
-
return { cleared: true, marks: marks.map((m) => decorateMark(m, tctx.USERS, now)) };
|
|
10441
|
-
}
|
|
10442
|
-
|
|
10443
|
-
/**
|
|
10444
|
-
* The browser's read. GET only, on purpose — see the header: the badge is the byproduct of
|
|
10445
|
-
* an agent protocol, and a tab is not a work-start step.
|
|
10446
|
-
*/
|
|
10447
|
-
async function marksApi(tctx, request, url, env) {
|
|
10448
|
-
if (request.method !== "GET") return jsonResponse({ error: "method-not-allowed" }, 405);
|
|
10449
|
-
const out = await readMarks(tctx, env);
|
|
10450
|
-
return jsonResponse({ ...out, ttlMs: MARK_TTL_MS });
|
|
10451
|
-
}
|
|
10452
|
-
|
|
10453
10303
|
// ---- Prototype deletion (repo-write via dispatch webhook) -------------------
|
|
10454
10304
|
// "Delete forever" on a prototype card. The worker holds NO repo credentials —
|
|
10455
10305
|
// it forwards the request to a per-instance webhook (a GitHub repository_dispatch
|
|
@@ -12299,13 +12149,6 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
12299
12149
|
if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
|
|
12300
12150
|
return pinsApi(tctx, request, url, env, me);
|
|
12301
12151
|
}
|
|
12302
|
-
// Working marks — READ ONLY here. Who is working where is workspace-internal, so it
|
|
12303
|
-
// asks for a session like the gallery around it; the WRITE side is a publish token and
|
|
12304
|
-
// lives under /__publish/_marks, because a work-start step is something a tool runs.
|
|
12305
|
-
if (url.pathname === "/__marks") {
|
|
12306
|
-
if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
|
|
12307
|
-
return marksApi(tctx, request, url, env);
|
|
12308
|
-
}
|
|
12309
12152
|
if (url.pathname === "/__name") {
|
|
12310
12153
|
if (!authed) return jsonResponse({ error: "unauthorized" }, 401);
|
|
12311
12154
|
const denied = viewerWriteRefusal(request, url, me, "name", tctx.SPACES);
|
|
@@ -12433,13 +12276,20 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
12433
12276
|
// Past the gate (or nothing gates the site) → serve. A 404 gets one more chance
|
|
12434
12277
|
// as a created canvas (a KV-registered board with no repo file — see canvasesApi).
|
|
12435
12278
|
if (authed) {
|
|
12436
|
-
|
|
12279
|
+
// A design-system draft to look through (`?ds=`), remembered in a cookie.
|
|
12280
|
+
const ds = dsOverlay(request, url);
|
|
12281
|
+
const stamp = (res) => (ds.setCookie ? withSetCookie(res, ds.setCookie) : res);
|
|
12282
|
+
// Where drafts are served, the gallery and its indexes are derived from the live
|
|
12283
|
+
// store rather than read from it — a landing is on them at once.
|
|
12284
|
+
const derived = await derivedPage(tctx, env, url);
|
|
12285
|
+
if (derived) return stamp(derived.status === 308 ? derived : await serveContent(tctx, derived, url, me, env));
|
|
12286
|
+
const asset = await assetFetch(tctx.tenantId, env, request, { dsDraft: ds.draft });
|
|
12437
12287
|
if (asset.status === 404) {
|
|
12438
12288
|
const virt = await virtualCanvas(tctx, request, env, url);
|
|
12439
|
-
if (virt) return virt;
|
|
12440
|
-
return notFoundResponse(tctx);
|
|
12289
|
+
if (virt) return stamp(virt);
|
|
12290
|
+
return stamp(notFoundResponse(tctx));
|
|
12441
12291
|
}
|
|
12442
|
-
return serveContent(tctx, asset, url, me, env);
|
|
12292
|
+
return stamp(await serveContent(tctx, asset, url, me, env));
|
|
12443
12293
|
}
|
|
12444
12294
|
|
|
12445
12295
|
// Created canvas boards are public like published prototypes — same obscure
|
|
@@ -12504,9 +12354,6 @@ export const __testables = Object.freeze({
|
|
|
12504
12354
|
nextPublishVersion, overlayFor, overlayKvKey, statusApi, nameApi, pinsApi,
|
|
12505
12355
|
currencyApi, currencyRows, freshness, whenWords, parseSince, unitKey, unitProvenance,
|
|
12506
12356
|
STALE_AFTER_DAYS, STATUS_LABELS, VALID_STATUS,
|
|
12507
|
-
marksApi, readMarks, writeMark, clearMark, liveMarks, markExpiresAt, decorateMark,
|
|
12508
|
-
normalizeMarkPath, markPathsOverlap, sweepExpired,
|
|
12509
|
-
MARK_TTL_MS, MARK_TTL_MAX_MS, MARK_TTL_MIN_MS, MARK_SWEEP_MAX, MARK_MAX_ROWS,
|
|
12510
12357
|
exportState, importState,
|
|
12511
12358
|
quotaBump, quotaMinute, quotaDay, workspaceStatus, touchWorkspaceActivity,
|
|
12512
12359
|
deleteWorkspace, purgeDue, blobGc, clearFamilies, NEVER_CLEARED,
|
|
@@ -12524,7 +12371,7 @@ export const __testables = Object.freeze({
|
|
|
12524
12371
|
doorFacts, doorText, wantsMachineDoor, gateResponse, DOOR_DOCS, DOOR_WELL_KNOWN,
|
|
12525
12372
|
resumeAfterDormancy,
|
|
12526
12373
|
PITI_VIEW_KEY, PITI_REMARKS_KEY,
|
|
12527
|
-
publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, isEngineChrome, publishRefusalBody, splitDraftPath,
|
|
12374
|
+
publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, derivedPage, dsOverlay, isEngineChrome, publishRefusalBody, splitDraftPath,
|
|
12528
12375
|
adminStorageApi,
|
|
12529
12376
|
adminCustomDomainApi,
|
|
12530
12377
|
isPrefixBacked, backedPublicPrefixes,
|