@augurworks/augur 0.15.6 → 0.15.8
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/agents/drafts.md +2 -1
- package/package.json +1 -1
- package/scripts/connect.mjs +26 -0
- package/scripts/restore.mjs +8 -0
- package/src/_worker.js +167 -18
- package/src/drafts/drafts.js +1 -0
- package/src/tenant-do.js +38 -4
- package/src/unit-object.mjs +11 -5
package/agents/drafts.md
CHANGED
|
@@ -55,7 +55,8 @@ saves on every burst of changes; `augur save` saves once.
|
|
|
55
55
|
## Landing
|
|
56
56
|
|
|
57
57
|
`augur land` replaces the prototype's real URL with your draft, records who landed it and
|
|
58
|
-
when
|
|
58
|
+
when — and whose draft it was, when a member lands somebody else's from the site — and
|
|
59
|
+
closes the draft. It is refused in exactly one case: somebody landed on this
|
|
59
60
|
prototype since you opened yours. Then:
|
|
60
61
|
|
|
61
62
|
```
|
package/package.json
CHANGED
package/scripts/connect.mjs
CHANGED
|
@@ -64,6 +64,32 @@ async function post(pathPart, body) {
|
|
|
64
64
|
return { status: r.status, json };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// A machine that is ALREADY connected to this origin does not pair again by accident. One
|
|
68
|
+
// cold agent collected its token with a blocking `connect`, did not read the answer, ran
|
|
69
|
+
// `connect` again, minted a second code, and had the person approve that too. If the saved
|
|
70
|
+
// token still answers, say so and stop; `--again` pairs afresh on purpose, and a token the
|
|
71
|
+
// workspace no longer honours (revoked, another workspace's) falls through to a new pairing.
|
|
72
|
+
const AGAIN = argv.includes("--again");
|
|
73
|
+
const TOKENS_FILE = path.join(os.homedir(), ".config", "augur", "tokens.json");
|
|
74
|
+
function savedToken() {
|
|
75
|
+
try { const t = JSON.parse(readFileSync(TOKENS_FILE, "utf8"))[host]; return t && t.token ? t : null; } catch (e) { return null; }
|
|
76
|
+
}
|
|
77
|
+
if (!AGAIN && !process.env.AUGUR_TOKEN) {
|
|
78
|
+
const saved = savedToken();
|
|
79
|
+
if (saved && !(saved.expiresAt && Date.parse(saved.expiresAt) <= Date.now())) {
|
|
80
|
+
let live = false;
|
|
81
|
+
try {
|
|
82
|
+
const r = await fetch(`${ORIGIN}/__unit/drafts`, { headers: { Authorization: `Bearer ${saved.token}`, Accept: "application/json" } });
|
|
83
|
+
live = r.status === 200;
|
|
84
|
+
} catch (e) { live = false; }
|
|
85
|
+
if (live) {
|
|
86
|
+
log(`this machine is already connected to ${C.bold}${host}${C.off}${saved.at ? ` (since ${saved.at.slice(0, 16).replace("T", " ")})` : ""}. Nothing to approve.`);
|
|
87
|
+
console.log(` ${C.dim}\`augur open <opportunity>/<prototype>\` works from here. \`augur connect --again\` pairs afresh.${C.off}`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
// A pairing this machine already started and nobody has collected: ask once whether it
|
|
68
94
|
// was approved meanwhile, and if not, keep waiting on THAT code rather than minting a
|
|
69
95
|
// second one for the same person to type.
|
package/scripts/restore.mjs
CHANGED
|
@@ -292,6 +292,14 @@ if (STATE) {
|
|
|
292
292
|
})).json();
|
|
293
293
|
if (!res.ok) die(`the instance refused the state: ${res.reason}${res.failed ? ` (${res.failed.join(", ")})` : ""}`);
|
|
294
294
|
if (res.skipped && res.skipped.length) log(`\x1b[33mskipped (not in the instance's inventory): ${res.skipped.join(", ")}\x1b[0m`);
|
|
295
|
+
// A restore says "at least this": a member the copy does not name stays, and the
|
|
296
|
+
// instance names them so a keep is never mistaken for a replace.
|
|
297
|
+
if (res.members && res.members.kept && res.members.kept.length) {
|
|
298
|
+
log(`\x1b[33mkept ${res.members.kept.length} member(s) this copy does not name (a restore removes nobody): ${res.members.kept.join(", ")} — remove them in the people panel if they should go\x1b[0m`);
|
|
299
|
+
}
|
|
300
|
+
if (res.members && res.members.removed && res.members.removed.length) {
|
|
301
|
+
log(`removed ${res.members.removed.length} member(s) this copy does not name: ${res.members.removed.join(", ")}`);
|
|
302
|
+
}
|
|
295
303
|
stateReport = res;
|
|
296
304
|
}
|
|
297
305
|
}
|
package/src/_worker.js
CHANGED
|
@@ -3754,10 +3754,13 @@ function connectPage(tctx, me) {
|
|
|
3754
3754
|
<p>This account can look around but not publish, so it cannot approve a terminal.</p>
|
|
3755
3755
|
<a class="home" href="/">Back to Augur</a>`
|
|
3756
3756
|
: `<h1>Connect a terminal</h1>
|
|
3757
|
-
<p>Type the code your terminal
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3757
|
+
<p>Type the code that your terminal, or the assistant you are working with right now,
|
|
3758
|
+
is showing you. Approving it lets that terminal publish as
|
|
3759
|
+
<strong>${escapeHtml(me.email)}</strong>: it works as you, and everything it lands
|
|
3760
|
+
carries your name.</p>
|
|
3761
|
+
<p class="warn">Only approve a code from a terminal you started or an assistant you
|
|
3762
|
+
are talking to right now. A code that arrives by mail, from a stranger, or out of
|
|
3763
|
+
the blue is not yours — do not type it.</p>
|
|
3761
3764
|
<form id="pairf">
|
|
3762
3765
|
<input id="pairc" autocomplete="off" autocapitalize="characters" spellcheck="false"
|
|
3763
3766
|
placeholder="ABCD-EFGH" aria-label="Pairing code" />
|
|
@@ -5219,7 +5222,11 @@ function personFace(users, id) {
|
|
|
5219
5222
|
: { name: null, initials: null, color: null };
|
|
5220
5223
|
}
|
|
5221
5224
|
const decorateDrafts = (drafts, users) => (drafts || []).map((d) => ({ ...d, ...personFace(users, d.owner) }));
|
|
5222
|
-
const decorateLandings = (landings, users) => (landings || []).map((l) => ({
|
|
5225
|
+
const decorateLandings = (landings, users) => (landings || []).map((l) => ({
|
|
5226
|
+
...l, ...personFace(users, l.by),
|
|
5227
|
+
// The draft's owner, with a face, when the landing was made from somebody else's draft.
|
|
5228
|
+
draft: l.draftOwner && l.draftOwner !== l.by ? { owner: l.draftOwner, session: l.draftSession || "", ...personFace(users, l.draftOwner) } : null,
|
|
5229
|
+
}));
|
|
5223
5230
|
|
|
5224
5231
|
/** How many units the gallery's index will ask about in one answer. */
|
|
5225
5232
|
const DRAFTS_INDEX_MAX = 50;
|
|
@@ -6916,6 +6923,7 @@ function loginPage(tctx, redirect, error, requestUrl, opts = {}) {
|
|
|
6916
6923
|
<head>
|
|
6917
6924
|
<meta charset="utf-8" />
|
|
6918
6925
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6926
|
+
<meta name="description" content="A sign-in gate. Assistants and scripts: GET /llms.txt says how to get in." />
|
|
6919
6927
|
<meta name="robots" content="noindex, nofollow" />
|
|
6920
6928
|
${previewHead(tctx, requestUrl)}
|
|
6921
6929
|
<link rel="preload" href="/fonts/inter-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin />
|
|
@@ -7022,6 +7030,7 @@ function loginPage(tctx, redirect, error, requestUrl, opts = {}) {
|
|
|
7022
7030
|
</div>
|
|
7023
7031
|
${formBody}
|
|
7024
7032
|
${tctx.LOGIN_HINT && !passwordless ? `<p class="hint">${escapeHtml(tctx.LOGIN_HINT)}</p>` : ""}
|
|
7033
|
+
<p class="hint door">Connecting an assistant or a script? It reads <a href="/llms.txt">/llms.txt</a>.</p>
|
|
7025
7034
|
</main>
|
|
7026
7035
|
</body>
|
|
7027
7036
|
</html>`;
|
|
@@ -7359,8 +7368,86 @@ async function withDraftUi(tctx, res, url, me, env) {
|
|
|
7359
7368
|
return new Response(html, { status: res.status, statusText: res.statusText, headers });
|
|
7360
7369
|
}
|
|
7361
7370
|
/** A content response, dressed: the live-reload poll, the current chrome, the draft bar, the cache policy. */
|
|
7362
|
-
async function serveContent(tctx, asset, url, me, env) {
|
|
7363
|
-
return withAssetCache(await withDraftUi(tctx, await composeChrome(tctx, withLiveReload(tctx, asset, url), url), url, me, env), url);
|
|
7371
|
+
async function serveContent(tctx, asset, url, me, env, request = null) {
|
|
7372
|
+
return withAgentPreface(withDoorLink(withAssetCache(await withDraftUi(tctx, await composeChrome(tctx, withLiveReload(tctx, asset, url), url), url, me, env), url)), request, url, me);
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
/**
|
|
7376
|
+
* Is this fetch a BROWSER's? Every browser sends `Sec-Fetch-Dest` (and `-Mode`) on every
|
|
7377
|
+
* request it makes — a navigation, an iframe, an image, a prefetch — and no scripted
|
|
7378
|
+
* fetcher does: curl, node, Python, the fetch behind a summarising tool. The headers are
|
|
7379
|
+
* forbidden to page script, so a page cannot fake them off; a fetcher can only fake them
|
|
7380
|
+
* ON, which is the harmless direction. A `Mozilla/` user agent counts as a browser too,
|
|
7381
|
+
* for the browser old enough to send no Sec-Fetch at all and for the fetcher that dresses
|
|
7382
|
+
* as one — both get the page a person gets, which is the conservative side of the line.
|
|
7383
|
+
*/
|
|
7384
|
+
function browserFetch(request) {
|
|
7385
|
+
if (!request || !request.headers) return true;
|
|
7386
|
+
if (request.headers.has("Sec-Fetch-Dest") || request.headers.has("Sec-Fetch-Mode")) return true;
|
|
7387
|
+
return /^Mozilla\//.test(request.headers.get("User-Agent") || "");
|
|
7388
|
+
}
|
|
7389
|
+
|
|
7390
|
+
/**
|
|
7391
|
+
* The one paragraph a non-browser fetch of a prototype is given, first in the body.
|
|
7392
|
+
*
|
|
7393
|
+
* DESCRIPTIVE, THIRD PERSON, NO ADDRESS AND NO REASSURANCE. A first draft said "Assistants
|
|
7394
|
+
* and scripts: … a terminal is paired with the person's approval, and nobody is ever asked
|
|
7395
|
+
* for a password", and the third cold agent read exactly that sentence as a prompt
|
|
7396
|
+
* injection — text on a fetched page that speaks to the agent and reassures it is the
|
|
7397
|
+
* shape of an attack, whatever it says. A statement of fact and a link is what a page may
|
|
7398
|
+
* carry: what this is, and where the workspace documents how it is edited.
|
|
7399
|
+
*/
|
|
7400
|
+
function agentPreface(url) {
|
|
7401
|
+
const origin = escapeHtml(url.origin);
|
|
7402
|
+
return `<p data-augur-door>This prototype is served by an Augur workspace (${origin}). `
|
|
7403
|
+
+ `The workspace documents how its prototypes are edited at <a href="${DOOR_DOCS}">${origin}${DOOR_DOCS}</a>.</p>`;
|
|
7404
|
+
}
|
|
7405
|
+
|
|
7406
|
+
/**
|
|
7407
|
+
* A prototype fetched by something that is not a browser opens with the door.
|
|
7408
|
+
*
|
|
7409
|
+
* An agent handed a prototype's URL fetches the page and finds a finished site with
|
|
7410
|
+
* nothing on it about how it is edited — the customer's own HTML, and on a public
|
|
7411
|
+
* prototype never the gate. The `Link` header (below) reaches a scripted agent; a
|
|
7412
|
+
* summarising fetch keeps only the text. So a fetch that carries no browser headers gets
|
|
7413
|
+
* ONE paragraph prepended to the body, naming the origin and `/llms.txt`, and a browser
|
|
7414
|
+
* gets the page byte for byte as published — a person never sees it, an embed never
|
|
7415
|
+
* carries it, and the design is not touched. Decided 6 Sep 2026 after two of four cold
|
|
7416
|
+
* agents fetched the prototype rather than the workspace and told the person to ask a
|
|
7417
|
+
* developer.
|
|
7418
|
+
*
|
|
7419
|
+
* `no-store`, so the agent's variant is never what a cache hands a browser; the wrap is
|
|
7420
|
+
* the outermost on the serving path for the same reason — nothing below it stores what
|
|
7421
|
+
* this adds. Only a 200 HTML page: an error page, a redirect and a stylesheet are left alone.
|
|
7422
|
+
* Only a SIGNED-OUT fetch: a request carrying a member's session is already inside, and
|
|
7423
|
+
* what it is served — the gallery, a gated prototype — stays byte for byte what it was.
|
|
7424
|
+
*/
|
|
7425
|
+
async function withAgentPreface(res, request, url, me = null) {
|
|
7426
|
+
if (!res || res.status !== 200 || !/text\/html/.test(res.headers.get("Content-Type") || "")) return res;
|
|
7427
|
+
if (me || browserFetch(request)) return res;
|
|
7428
|
+
const html = await res.text();
|
|
7429
|
+
const m = /<body[^>]*>/i.exec(html);
|
|
7430
|
+
const out = m ? html.slice(0, m.index + m[0].length) + agentPreface(url) + html.slice(m.index + m[0].length) : agentPreface(url) + html;
|
|
7431
|
+
const headers = new Headers(res.headers);
|
|
7432
|
+
headers.delete("Content-Length");
|
|
7433
|
+
headers.delete("ETag");
|
|
7434
|
+
headers.set("Cache-Control", "no-store");
|
|
7435
|
+
headers.set("Vary", "Sec-Fetch-Dest, User-Agent");
|
|
7436
|
+
return new Response(out, { status: res.status, statusText: res.statusText, headers });
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* A served page carries the same `Link: </llms.txt>; rel="help"` the gate carries. An agent
|
|
7440
|
+
* handed the URL of a PUBLIC prototype never meets the gate — it fetches the page, finds a
|
|
7441
|
+
* finished site with nothing on it about how it is edited, and gives up. The header is the
|
|
7442
|
+
* one pointer that costs the page no bytes and no pixels; a scripted agent reads it, and a
|
|
7443
|
+
* summarising one at least reaches the gate's own words at the workspace root.
|
|
7444
|
+
*/
|
|
7445
|
+
function withDoorLink(res) {
|
|
7446
|
+
if (!res || !/text\/html/.test(res.headers.get("Content-Type") || "")) return res;
|
|
7447
|
+
if (res.headers.get("Link")) return res;
|
|
7448
|
+
const headers = new Headers(res.headers);
|
|
7449
|
+
headers.set("Link", `<${DOOR_DOCS}>; rel="help"`);
|
|
7450
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
7364
7451
|
}
|
|
7365
7452
|
|
|
7366
7453
|
// Test seam: loadConfig fills the chrome pointer, the workspace list and the runtime-
|
|
@@ -8386,6 +8473,43 @@ async function readStateFamily(tctx, env, entry, store, kv) {
|
|
|
8386
8473
|
}
|
|
8387
8474
|
return await store.read(family, "");
|
|
8388
8475
|
}
|
|
8476
|
+
// ── the identity families the workspace OBJECT holds since the cut-over ──────────
|
|
8477
|
+
//
|
|
8478
|
+
// `publish:tokens` is read by the publish path from the object FIRST and KV second, and a
|
|
8479
|
+
// mint writes both — so a row can be in one store and not the other. Reading KV alone
|
|
8480
|
+
// here (which is what this did) produced a `--full` copy that omitted tokens which were
|
|
8481
|
+
// live and answering; a restore from it would have dropped them without a word. The copy
|
|
8482
|
+
// is the union of both stores.
|
|
8483
|
+
//
|
|
8484
|
+
// ⚠️ FOR A HASH BOTH STORES HOLD, KV'S RECORD IS THE ONE COPIED, VERBATIM. The object's
|
|
8485
|
+
// row is a projection into columns — it stamps a `createdAt` on a record the copy never
|
|
8486
|
+
// carried and spells `label` as null where the record had none — and a copy that took
|
|
8487
|
+
// the projection stopped verifying: `augur migrate` compares the target's export with the
|
|
8488
|
+
// source's, and a KV→workspace migration reported `publish:tokens` as not matching on
|
|
8489
|
+
// data that had landed. What the object ADDS is the rows KV lacks; what KV holds is what a
|
|
8490
|
+
// restore wrote and what a verifier reads back. (The admin panel lists the same union the
|
|
8491
|
+
// other way round — the object's row is what governs a publish — and that is a display
|
|
8492
|
+
// question, not a copy question.)
|
|
8493
|
+
//
|
|
8494
|
+
// An object that CANNOT be asked throws, and the export files that under `failed` — a
|
|
8495
|
+
// restore refuses such a copy. Answering from KV alone instead would be a copy that calls
|
|
8496
|
+
// itself full while missing every object-held token, the exact thing this branch closes.
|
|
8497
|
+
if (entry.id === PUBLISH_TOKENS_KEY) {
|
|
8498
|
+
const ident = identityFor(env, tctx, "publishTokens");
|
|
8499
|
+
if (ident) {
|
|
8500
|
+
const listed = await ident.tokenList();
|
|
8501
|
+
const held = (listed && listed.tokens && typeof listed.tokens === "object") ? listed.tokens : {};
|
|
8502
|
+
let map = null;
|
|
8503
|
+
if (kv) {
|
|
8504
|
+
const raw = await kv.get(entry.id);
|
|
8505
|
+
if (raw != null) { try { map = JSON.parse(raw); } catch (e) { map = null; } }
|
|
8506
|
+
}
|
|
8507
|
+
// Absent stays absent: no document in KV and no row in the object is the one answer a
|
|
8508
|
+
// restore must LEAVE alone (it clears a `{}`). A seeded-but-empty object is `{}`.
|
|
8509
|
+
if (map === null && !Object.keys(held).length && !(listed && listed.seeded)) return null;
|
|
8510
|
+
return { ...held, ...(map || {}) };
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8389
8513
|
if (!kv) return null;
|
|
8390
8514
|
if (STATE_KV_PREFIXED.includes(entry.id)) {
|
|
8391
8515
|
const out = {};
|
|
@@ -8652,6 +8776,9 @@ async function importState(tctx, env, doc) {
|
|
|
8652
8776
|
},
|
|
8653
8777
|
);
|
|
8654
8778
|
|
|
8779
|
+
// Whether the copy carried the roster overlay at all: the object counts the people
|
|
8780
|
+
// the copy does not name only when the copy claims to say who belongs.
|
|
8781
|
+
identity.rosterCarried = Object.prototype.hasOwnProperty.call(doc.families, "users:roster");
|
|
8655
8782
|
const res = await stub.fetch("https://workspace/state/import", {
|
|
8656
8783
|
method: "POST",
|
|
8657
8784
|
headers: { "content-type": "application/json" },
|
|
@@ -8660,7 +8787,15 @@ async function importState(tctx, env, doc) {
|
|
|
8660
8787
|
}),
|
|
8661
8788
|
});
|
|
8662
8789
|
if (!res.ok) return { ok: false, reason: "workspace-refused", status: res.status };
|
|
8663
|
-
const { atomic, refused } = await res.json();
|
|
8790
|
+
const { atomic, refused, members } = await res.json();
|
|
8791
|
+
// The people `prune` removed from the object are removed the way the admin panel
|
|
8792
|
+
// removes them: the session, the KV-side token and invite copies go too. Best-effort
|
|
8793
|
+
// and after the object's transaction — the row is the fact, these are its consequences.
|
|
8794
|
+
for (const email of (members && members.removed) || []) {
|
|
8795
|
+
try { await revokeSecret(env, email, tctx); } catch (e) { /* no KV binding: nothing to revoke there */ }
|
|
8796
|
+
await revokePublishTokens(tctx, env, email);
|
|
8797
|
+
await revokeInvitesFor(tctx, env, email);
|
|
8798
|
+
}
|
|
8664
8799
|
// ⚠️ THE IDENTITY FAMILIES GO TO BOTH, AND THAT IS THE POINT OF THE SPLIT.
|
|
8665
8800
|
// The object gets a faithful copy and KV stays exactly what the KV path reads, so a
|
|
8666
8801
|
// restore cannot take an instance down whichever store is currently answering. With the
|
|
@@ -8681,6 +8816,9 @@ async function importState(tctx, env, doc) {
|
|
|
8681
8816
|
// from a complete one, which is the failure this whole path exists to avoid.
|
|
8682
8817
|
unmapped: identitySkipped,
|
|
8683
8818
|
refusedRows: refused || [],
|
|
8819
|
+
// Who the copy did not name and what became of them — `kept` on a plain restore,
|
|
8820
|
+
// `removed` under `prune`. Absent when the copy carried no roster.
|
|
8821
|
+
...(members ? { members } : {}),
|
|
8684
8822
|
};
|
|
8685
8823
|
}
|
|
8686
8824
|
|
|
@@ -9100,6 +9238,8 @@ function wantsJson(request, url) {
|
|
|
9100
9238
|
// probe and the "an unknown path and the root are the same page" contract depend on it.
|
|
9101
9239
|
const DOOR_DOCS = "/llms.txt";
|
|
9102
9240
|
const DOOR_WELL_KNOWN = "/.well-known/augur.json";
|
|
9241
|
+
/** The engine's public source — what the CLI on npm is built from, named so an agent can check. */
|
|
9242
|
+
const ENGINE_SOURCE = "https://github.com/andratwiro/augur";
|
|
9103
9243
|
|
|
9104
9244
|
/** Does this deployment serve drafts — a unit store beside a bundle store. The same two checks `unitApi` makes. */
|
|
9105
9245
|
const draftsServedHere = (env) => !!(env && env.BUNDLES && unitNamespace(env));
|
|
@@ -9114,6 +9254,10 @@ function doorFacts(tctx, url, env) {
|
|
|
9114
9254
|
engine: tctx.INSTANCE_ENGINE_VERSION ? { version: tctx.INSTANCE_ENGINE_VERSION } : {},
|
|
9115
9255
|
pairing: { enabled: pairing, start: "/__publish/_pair/start", approve: "/__connect" },
|
|
9116
9256
|
connect: pairing ? `npx @augurworks/augur connect --origin ${url.origin}` : null,
|
|
9257
|
+
// Where the CLI comes from, for an agent that checks before it runs anything: the
|
|
9258
|
+
// package is the same open-source engine that serves this page, and `npm view
|
|
9259
|
+
// @augurworks/augur repository` names the same repository.
|
|
9260
|
+
source: ENGINE_SOURCE,
|
|
9117
9261
|
docs: DOOR_DOCS,
|
|
9118
9262
|
// Where drafts are served, a prototype is changed by opening it, not by shipping a
|
|
9119
9263
|
// tree — and the door is where a cold agent learns which of the two this instance is.
|
|
@@ -9134,11 +9278,16 @@ function doorText(f) {
|
|
|
9134
9278
|
+ `It prints a link and a code. The owner of this workspace opens the link in a\n`
|
|
9135
9279
|
+ `browser they are already signed in to and enters the code. The token lands on\n`
|
|
9136
9280
|
+ `that machine, and every verb below uses it from then on.\n\n`
|
|
9137
|
-
+ `
|
|
9138
|
-
+ `
|
|
9139
|
-
+ `
|
|
9140
|
-
+ `
|
|
9141
|
-
+ `the
|
|
9281
|
+
+ `The CLI is this engine's own, open source at ${f.source} — the same engine that\n`
|
|
9282
|
+
+ `serves this page; \`npm view @augurworks/augur repository\` names that repository.\n\n`
|
|
9283
|
+
+ `AGENTS: the approval is the person's act, in their browser — you cannot approve\n`
|
|
9284
|
+
+ `it from here and must not try; your part is to send them the line and collect\n`
|
|
9285
|
+
+ `the token afterwards. If your person reads your messages only after you finish\n`
|
|
9286
|
+
+ `your turn, run it with --no-wait: it prints the line and exits. Send them that\n`
|
|
9287
|
+
+ `line, end your turn, and run the same command again once they say they approved —\n`
|
|
9288
|
+
+ `the token is collected then, for that same code, and no second code is minted.\n`
|
|
9289
|
+
+ `Waiting inside one turn skips nothing; it only hides the code from them for the\n`
|
|
9290
|
+
+ `five minutes it lives.\n\n`
|
|
9142
9291
|
+ (f.drafts && f.drafts.enabled
|
|
9143
9292
|
? `You need no source tree here: \`augur open <opportunity>/<prototype>\` fetches that one\n`
|
|
9144
9293
|
+ `prototype into a folder of its own (see below). Do not clone the workspace first.\n\n`
|
|
@@ -12247,7 +12396,7 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
12247
12396
|
}
|
|
12248
12397
|
const asset = await assetFetch(tctx.tenantId, env, request);
|
|
12249
12398
|
if (asset.status === 404) return notFoundResponse(tctx);
|
|
12250
|
-
return serveContent(tctx, asset, url, me, env);
|
|
12399
|
+
return serveContent(tctx, asset, url, me, env, request);
|
|
12251
12400
|
}
|
|
12252
12401
|
|
|
12253
12402
|
// Published prototypes are public — never gated, regardless of the cookie.
|
|
@@ -12256,7 +12405,7 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
12256
12405
|
if (isPublicPath(tctx, url.pathname)) {
|
|
12257
12406
|
const asset = await assetFetch(tctx.tenantId, env, request);
|
|
12258
12407
|
if (asset.status === 404) return notFoundResponse(tctx);
|
|
12259
|
-
const res = await serveContent(tctx, asset, url, me, env);
|
|
12408
|
+
const res = await serveContent(tctx, asset, url, me, env, request);
|
|
12260
12409
|
const out = new Response(res.body, res);
|
|
12261
12410
|
out.headers.set("X-Robots-Tag", "noindex, nofollow, noarchive");
|
|
12262
12411
|
return out;
|
|
@@ -12287,14 +12436,14 @@ async function handleRequest(request, env, ctx, url, trace) {
|
|
|
12287
12436
|
// Where drafts are served, the gallery and its indexes are derived from the live
|
|
12288
12437
|
// store rather than read from it — a landing is on them at once.
|
|
12289
12438
|
const derived = await derivedPage(tctx, env, url);
|
|
12290
|
-
if (derived) return stamp(derived.status === 308 ? derived : await serveContent(tctx, derived, url, me, env));
|
|
12439
|
+
if (derived) return stamp(derived.status === 308 ? derived : await serveContent(tctx, derived, url, me, env, request));
|
|
12291
12440
|
const asset = await assetFetch(tctx.tenantId, env, request, { dsDraft: ds.draft });
|
|
12292
12441
|
if (asset.status === 404) {
|
|
12293
12442
|
const virt = await virtualCanvas(tctx, request, env, url);
|
|
12294
12443
|
if (virt) return stamp(virt);
|
|
12295
12444
|
return stamp(notFoundResponse(tctx));
|
|
12296
12445
|
}
|
|
12297
|
-
return stamp(await serveContent(tctx, asset, url, me, env));
|
|
12446
|
+
return stamp(await serveContent(tctx, asset, url, me, env, request));
|
|
12298
12447
|
}
|
|
12299
12448
|
|
|
12300
12449
|
// Created canvas boards are public like published prototypes — same obscure
|
|
@@ -12376,7 +12525,7 @@ export const __testables = Object.freeze({
|
|
|
12376
12525
|
doorFacts, doorText, wantsMachineDoor, gateResponse, DOOR_DOCS, DOOR_WELL_KNOWN,
|
|
12377
12526
|
resumeAfterDormancy,
|
|
12378
12527
|
PITI_VIEW_KEY, PITI_REMARKS_KEY,
|
|
12379
|
-
publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, derivedPage, dsOverlay, isEngineChrome, publishRefusalBody, splitDraftPath,
|
|
12528
|
+
publishAuthDetailed, unitApi, unitCaller, personFace, withDoorLink, withAgentPreface, browserFetch, withDraftUi, draftUiBoot, derivedPage, dsOverlay, isEngineChrome, publishRefusalBody, splitDraftPath,
|
|
12380
12529
|
adminStorageApi,
|
|
12381
12530
|
adminCustomDomainApi,
|
|
12382
12531
|
isPrefixBacked, backedPublicPrefixes,
|
package/src/drafts/drafts.js
CHANGED
|
@@ -223,6 +223,7 @@
|
|
|
223
223
|
row.appendChild(face(l));
|
|
224
224
|
var text = el("div");
|
|
225
225
|
var who = l.by === "live" ? "Adopted from the live site" : label(l);
|
|
226
|
+
if (l.draft && l.draft.owner) who += " · draft by " + label(l.draft);
|
|
226
227
|
text.appendChild(el("div", null, "#" + l.revision + " · " + who));
|
|
227
228
|
var meta = (l.note ? l.note + " · " : "") + ago(l.at, "landed")
|
|
228
229
|
+ (l.restoredFrom ? " · restored from #" + l.restoredFrom : "")
|
package/src/tenant-do.js
CHANGED
|
@@ -739,7 +739,7 @@ function seedCount(seed) {
|
|
|
739
739
|
* `users:roster`'s `remove` list, and dropping the person here instead would let a
|
|
740
740
|
* re-invite inherit the role the last holder of that address had.
|
|
741
741
|
*/
|
|
742
|
-
function writeIdentity(sql, identity, at, written = [], refused = []) {
|
|
742
|
+
function writeIdentity(sql, identity, at, written = [], refused = [], { prune = false } = {}) {
|
|
743
743
|
if (!identity || typeof identity !== "object") return { written, refused };
|
|
744
744
|
const list = (k) => (Array.isArray(identity[k]) ? identity[k] : []);
|
|
745
745
|
const touched = (family, n) => { if (n) written.push(family); };
|
|
@@ -775,6 +775,39 @@ function writeIdentity(sql, identity, at, written = [], refused = []) {
|
|
|
775
775
|
touched("members", n);
|
|
776
776
|
if (n) markSeeded(sql, "roster", at);
|
|
777
777
|
|
|
778
|
+
// ── the people the copy does NOT name ──────────────────────────────────────────
|
|
779
|
+
//
|
|
780
|
+
// On KV the roster overlay is one document and an import replaces it, so anybody the
|
|
781
|
+
// copy does not name is gone. Here the rows above were upserted and every other row was
|
|
782
|
+
// left as it was — a member the copy does not name STAYS, with their session and their
|
|
783
|
+
// tokens, and for a while this reported `users:roster` as written and said nothing else,
|
|
784
|
+
// which read as a replace. So the ones left are counted and named:
|
|
785
|
+
//
|
|
786
|
+
// · a plain restore says "at least this" and KEEPS them — `kept` says who, so a person
|
|
787
|
+
// reading the result cannot mistake a keep for a replace;
|
|
788
|
+
// · `prune` says "exactly this" and REMOVES them the way the admin panel's remove
|
|
789
|
+
// does — a tombstone (a re-invite must not inherit the old role), no publish token,
|
|
790
|
+
// no outstanding invite. The worker revokes the session and the KV-side copies.
|
|
791
|
+
//
|
|
792
|
+
// Only overlay rows: a config member is the durable roster, not the copy's to remove.
|
|
793
|
+
// Only when the copy CARRIED the roster family — a copy of nothing but statuses says
|
|
794
|
+
// nothing about who belongs. `rosterCarried` is the worker's word for that.
|
|
795
|
+
let members;
|
|
796
|
+
if (identity.rosterCarried) {
|
|
797
|
+
const named = new Set(list("members").map((m) => lcAddr(m && m.email)).filter(Boolean));
|
|
798
|
+
const left = [...sql.exec(
|
|
799
|
+
`SELECT email FROM members WHERE source = 'overlay' AND removed_at IS NULL ORDER BY email`,
|
|
800
|
+
)].map((r) => String(r.email)).filter((e) => !named.has(e));
|
|
801
|
+
members = { kept: [], removed: [] };
|
|
802
|
+
for (const e of left) {
|
|
803
|
+
if (!prune) { members.kept.push(e); continue; }
|
|
804
|
+
sql.exec(`UPDATE members SET removed_at = ? WHERE email = ?`, at, e);
|
|
805
|
+
sql.exec(`DELETE FROM publish_tokens WHERE LOWER(label) = ?`, e);
|
|
806
|
+
sql.exec(`DELETE FROM invites WHERE email = ?`, e);
|
|
807
|
+
members.removed.push(e);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
778
811
|
n = 0;
|
|
779
812
|
for (const i of list("invites")) {
|
|
780
813
|
if (!i || !i.tokenHash) continue;
|
|
@@ -838,7 +871,7 @@ function writeIdentity(sql, identity, at, written = [], refused = []) {
|
|
|
838
871
|
}
|
|
839
872
|
touched("blobs", n);
|
|
840
873
|
|
|
841
|
-
return { written, refused };
|
|
874
|
+
return { written, refused, members };
|
|
842
875
|
}
|
|
843
876
|
|
|
844
877
|
/**
|
|
@@ -2550,11 +2583,12 @@ export class TenantStore {
|
|
|
2550
2583
|
written.push(scope ? `${family}/${scope}` : family);
|
|
2551
2584
|
}
|
|
2552
2585
|
}
|
|
2553
|
-
writeIdentity(this.sql, identity, stamp, written, refused);
|
|
2586
|
+
members = writeIdentity(this.sql, identity, stamp, written, refused, { prune }).members;
|
|
2554
2587
|
};
|
|
2588
|
+
let members;
|
|
2555
2589
|
const atomic = typeof this.ctx.storage.transactionSync === "function";
|
|
2556
2590
|
if (atomic) this.ctx.storage.transactionSync(body); else body();
|
|
2557
|
-
return { written, refused, atomic };
|
|
2591
|
+
return members ? { written, refused, atomic, members } : { written, refused, atomic };
|
|
2558
2592
|
}
|
|
2559
2593
|
|
|
2560
2594
|
/**
|
package/src/unit-object.mjs
CHANGED
|
@@ -265,11 +265,17 @@ export class UnitObject {
|
|
|
265
265
|
const rows = [...this.sql.exec(`SELECT * FROM landings ORDER BY revision DESC`)];
|
|
266
266
|
return {
|
|
267
267
|
revision: this.mainRevision(),
|
|
268
|
-
landings: rows.map((r) =>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
landings: rows.map((r) => {
|
|
269
|
+
// Any member may land any draft from the bar, so a landing has two people on it:
|
|
270
|
+
// who pressed Land (`by`) and whose draft it was. The draft row keeps the second.
|
|
271
|
+
const d = r.draft_id ? this.draft(r.draft_id) : null;
|
|
272
|
+
return {
|
|
273
|
+
revision: Number(r.revision), by: r.by || null, session: r.session || "", at: r.at, note: r.note || "",
|
|
274
|
+
draftId: r.draft_id || null, restoredFrom: r.restored_from == null ? null : Number(r.restored_from),
|
|
275
|
+
draftOwner: d ? d.owner : null, draftSession: d ? d.session : "",
|
|
276
|
+
files: Object.keys(JSON.parse(r.tbl)).length,
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
273
279
|
};
|
|
274
280
|
}
|
|
275
281
|
|