@inerrata-corporation/errata 2.0.6-dev.2008 → 2.0.6-dev.2029
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/consolidate-worker.mjs +13 -0
- package/errata.mjs +686 -409
- package/package.json +1 -1
- package/pass-worker.mjs +13 -0
package/consolidate-worker.mjs
CHANGED
|
@@ -16208,6 +16208,11 @@ var SqliteGraphStore = class {
|
|
|
16208
16208
|
countByLabel: this.db.prepare(
|
|
16209
16209
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
16210
16210
|
),
|
|
16211
|
+
// Identity twin of findByLabel — same live-row semantics, two attrs read in
|
|
16212
|
+
// SQL, no attrs JSON.parse or embedding decode (PJ-anchor-reach).
|
|
16213
|
+
identitiesByLabel: this.db.prepare(
|
|
16214
|
+
"SELECT id, json_extract(attrs_json, '$.relPath') AS relPath, json_extract(attrs_json, '$.qname') AS qname FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
16215
|
+
),
|
|
16211
16216
|
// ALL versions of a label — incl frozen/closed (valid_to set). Used by the
|
|
16212
16217
|
// clean-reindex purge so a true wipe removes history too, not just live rows.
|
|
16213
16218
|
findByLabelAll: this.db.prepare("SELECT * FROM nodes WHERE label = ?"),
|
|
@@ -16508,6 +16513,14 @@ var SqliteGraphStore = class {
|
|
|
16508
16513
|
countNodesByLabel(label) {
|
|
16509
16514
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
16510
16515
|
}
|
|
16516
|
+
findNodeIdentitiesByLabel(label) {
|
|
16517
|
+
const rows = this.stmts.identitiesByLabel.all(label);
|
|
16518
|
+
return rows.map((r) => ({
|
|
16519
|
+
id: r.id,
|
|
16520
|
+
relPath: typeof r.relPath === "string" ? r.relPath : null,
|
|
16521
|
+
qname: typeof r.qname === "string" ? r.qname : null
|
|
16522
|
+
}));
|
|
16523
|
+
}
|
|
16511
16524
|
findAllVersionsByLabel(label) {
|
|
16512
16525
|
const rows = this.stmts.findByLabelAll.all(label);
|
|
16513
16526
|
return rows.map(rowToNode);
|
package/errata.mjs
CHANGED
|
@@ -15962,6 +15962,9 @@ function projectSymbolId(projectSalt, projectId, relPath, qname, kind) {
|
|
|
15962
15962
|
const path2 = normalizeSymbolPath(relPath);
|
|
15963
15963
|
return `sym_${createHmac("sha256", projectSalt).update(`${projectId}:${path2}:${qname}:${kind}`).digest("hex").slice(0, 24)}`;
|
|
15964
15964
|
}
|
|
15965
|
+
function projectSaltedNodeId(canonicalId, salt) {
|
|
15966
|
+
return `projn_${createHmac("sha256", salt).update(canonicalId).digest("hex").slice(0, 24)}`;
|
|
15967
|
+
}
|
|
15965
15968
|
function isMembraneSaltedId(id) {
|
|
15966
15969
|
return typeof id === "string" && /^(orgn_|teamn_|projn_)/.test(id);
|
|
15967
15970
|
}
|
|
@@ -16460,6 +16463,7 @@ __export(src_exports, {
|
|
|
16460
16463
|
parsePnpmLock: () => parsePnpmLock,
|
|
16461
16464
|
prefilterEntities: () => prefilterEntities,
|
|
16462
16465
|
problemIdFromError: () => problemIdFromError,
|
|
16466
|
+
projectSaltedNodeId: () => projectSaltedNodeId,
|
|
16463
16467
|
projectSymbolId: () => projectSymbolId,
|
|
16464
16468
|
provenanceBand: () => provenanceBand,
|
|
16465
16469
|
resolveCanonical: () => resolveCanonical,
|
|
@@ -17255,6 +17259,11 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17255
17259
|
countByLabel: this.db.prepare(
|
|
17256
17260
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
17257
17261
|
),
|
|
17262
|
+
// Identity twin of findByLabel — same live-row semantics, two attrs read in
|
|
17263
|
+
// SQL, no attrs JSON.parse or embedding decode (PJ-anchor-reach).
|
|
17264
|
+
identitiesByLabel: this.db.prepare(
|
|
17265
|
+
"SELECT id, json_extract(attrs_json, '$.relPath') AS relPath, json_extract(attrs_json, '$.qname') AS qname FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
17266
|
+
),
|
|
17258
17267
|
// ALL versions of a label — incl frozen/closed (valid_to set). Used by the
|
|
17259
17268
|
// clean-reindex purge so a true wipe removes history too, not just live rows.
|
|
17260
17269
|
findByLabelAll: this.db.prepare("SELECT * FROM nodes WHERE label = ?"),
|
|
@@ -17555,6 +17564,14 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17555
17564
|
countNodesByLabel(label) {
|
|
17556
17565
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
17557
17566
|
}
|
|
17567
|
+
findNodeIdentitiesByLabel(label) {
|
|
17568
|
+
const rows = this.stmts.identitiesByLabel.all(label);
|
|
17569
|
+
return rows.map((r) => ({
|
|
17570
|
+
id: r.id,
|
|
17571
|
+
relPath: typeof r.relPath === "string" ? r.relPath : null,
|
|
17572
|
+
qname: typeof r.qname === "string" ? r.qname : null
|
|
17573
|
+
}));
|
|
17574
|
+
}
|
|
17558
17575
|
findAllVersionsByLabel(label) {
|
|
17559
17576
|
const rows = this.stmts.findByLabelAll.all(label);
|
|
17560
17577
|
return rows.map(rowToNode);
|
|
@@ -25213,6 +25230,28 @@ var init_client = __esm({
|
|
|
25213
25230
|
}));
|
|
25214
25231
|
return { nodes, edges, anchorEdges, symbolIds: res.symbolIds ?? [] };
|
|
25215
25232
|
}
|
|
25233
|
+
/**
|
|
25234
|
+
* PJ-anchor-reach: one cursor page of the project's symbol anchors, newest
|
|
25235
|
+
* stamp last. `since` is the cursor from the previous page (0 = from the
|
|
25236
|
+
* beginning); the returned `cursor` continues it. Rank-independent — the
|
|
25237
|
+
* daemon filters the page against its own checkout, and nothing about which
|
|
25238
|
+
* symbols it holds is sent. Same membership gate as priors.
|
|
25239
|
+
*/
|
|
25240
|
+
async getProjectAnchors(projectId, since, limit = 200) {
|
|
25241
|
+
const res = await this.json(
|
|
25242
|
+
"GET",
|
|
25243
|
+
`/v2/projects/${encodeURIComponent(projectId)}/anchors?since=${String(Math.max(0, Math.floor(since)))}&limit=${String(limit)}`
|
|
25244
|
+
);
|
|
25245
|
+
const { nodes } = this.toCastalia({ nodes: res.nodes ?? [], edges: [] });
|
|
25246
|
+
const anchors = (res.anchors ?? []).map((a) => ({
|
|
25247
|
+
from: a.fromCanonicalId,
|
|
25248
|
+
toSymbolId: a.toSymbolId,
|
|
25249
|
+
type: a.type,
|
|
25250
|
+
attrs: a.attrs ?? {},
|
|
25251
|
+
stampedAt: typeof a.stampedAt === "number" ? a.stampedAt : 0
|
|
25252
|
+
}));
|
|
25253
|
+
return { nodes, anchors, cursor: typeof res.cursor === "number" ? res.cursor : since };
|
|
25254
|
+
}
|
|
25216
25255
|
/** Pull cloud-induced skills + the session-bootstrap prime payload. `seed` is the
|
|
25217
25256
|
* canonical ids of the agent's recent problems — the cloud ranks the skills whose
|
|
25218
25257
|
* motifs those problems match first (the rest are baseline). `techSeed`
|
|
@@ -70009,13 +70048,13 @@ function esc3(s) {
|
|
|
70009
70048
|
function slug(name2) {
|
|
70010
70049
|
return name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
70011
70050
|
}
|
|
70012
|
-
function
|
|
70051
|
+
function isLive3(verb) {
|
|
70013
70052
|
return LIVE_VERBS.has(verb);
|
|
70014
70053
|
}
|
|
70015
70054
|
function nextHtml(n, opts) {
|
|
70016
70055
|
if (!n) return "";
|
|
70017
70056
|
const cmd2 = `errata ${n.verb}${n.arg ? " " + n.arg : ""}`;
|
|
70018
|
-
const live2 =
|
|
70057
|
+
const live2 = isLive3(n.verb);
|
|
70019
70058
|
if (!live2 && !opts.includeFutureVerbs) return "";
|
|
70020
70059
|
const hint = n.hint ? ` \u2014 ${esc3(n.hint)}` : "";
|
|
70021
70060
|
const cls = live2 ? "next" : "next soon";
|
|
@@ -77008,7 +77047,7 @@ function createWatchBreaker(deps) {
|
|
|
77008
77047
|
}
|
|
77009
77048
|
|
|
77010
77049
|
// src/engine.ts
|
|
77011
|
-
var DAEMON_VERSION = true ? "2.0.6-dev.
|
|
77050
|
+
var DAEMON_VERSION = true ? "2.0.6-dev.2029" : "2.0.0-alpha.0";
|
|
77012
77051
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
77013
77052
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
77014
77053
|
var GIT_OP_MUTE_MS = 4e3;
|
|
@@ -79337,7 +79376,7 @@ init_identity2();
|
|
|
79337
79376
|
// src/multi.ts
|
|
79338
79377
|
init_dist();
|
|
79339
79378
|
init_src5();
|
|
79340
|
-
import { existsSync as
|
|
79379
|
+
import { existsSync as existsSync31, readFileSync as readFileSync30, unlinkSync as unlinkSync3, writeFileSync as writeFileSync23 } from "node:fs";
|
|
79341
79380
|
|
|
79342
79381
|
// src/principle-sync.ts
|
|
79343
79382
|
init_src5();
|
|
@@ -80506,16 +80545,88 @@ function buildReverseSymbolMap(opts) {
|
|
|
80506
80545
|
const { store, salt, projectId } = opts;
|
|
80507
80546
|
const reverse = /* @__PURE__ */ new Map();
|
|
80508
80547
|
for (const kind of ANCHOR_SYMBOL_KINDS) {
|
|
80509
|
-
for (const n of store.
|
|
80510
|
-
const relPath = n
|
|
80511
|
-
|
|
80512
|
-
|
|
80513
|
-
const sym = projectSymbolId(salt, projectId, relPath, qname, n.label);
|
|
80548
|
+
for (const n of store.findNodeIdentitiesByLabel(kind)) {
|
|
80549
|
+
const { relPath, qname } = n;
|
|
80550
|
+
if (relPath === null || qname === null) continue;
|
|
80551
|
+
const sym = projectSymbolId(salt, projectId, relPath, qname, kind);
|
|
80514
80552
|
if (!reverse.has(sym)) reverse.set(sym, n.id);
|
|
80553
|
+
const twin = projectSaltedNodeId(sym, salt);
|
|
80554
|
+
if (!reverse.has(twin)) reverse.set(twin, n.id);
|
|
80515
80555
|
}
|
|
80516
80556
|
}
|
|
80517
80557
|
return reverse;
|
|
80518
80558
|
}
|
|
80559
|
+
var ANCHOR_SOURCE_KINDS = [
|
|
80560
|
+
"Problem",
|
|
80561
|
+
"Solution",
|
|
80562
|
+
"RootCause",
|
|
80563
|
+
"Pattern",
|
|
80564
|
+
"Technique",
|
|
80565
|
+
"AntiPattern"
|
|
80566
|
+
];
|
|
80567
|
+
function buildOwnKnowledgeMap(opts) {
|
|
80568
|
+
const { store, salt } = opts;
|
|
80569
|
+
const own = /* @__PURE__ */ new Map();
|
|
80570
|
+
for (const kind of ANCHOR_SOURCE_KINDS) {
|
|
80571
|
+
for (const n of store.findNodeIdentitiesByLabel(kind)) {
|
|
80572
|
+
if (n.id.startsWith("projn_")) continue;
|
|
80573
|
+
own.set(projectSaltedNodeId(n.id, salt), n.id);
|
|
80574
|
+
}
|
|
80575
|
+
}
|
|
80576
|
+
return own;
|
|
80577
|
+
}
|
|
80578
|
+
function resolveAnchorSource(store, from, own) {
|
|
80579
|
+
if (store.getNode(from)) return from;
|
|
80580
|
+
return own().get(from) ?? null;
|
|
80581
|
+
}
|
|
80582
|
+
function existingAnchor(store, from, to) {
|
|
80583
|
+
const matches = store.outEdges(from, ["ANCHORED_AT"]).filter((e) => e.to === to);
|
|
80584
|
+
return matches.find((e) => e.attrs["source"] !== "cloud") ?? matches[0] ?? null;
|
|
80585
|
+
}
|
|
80586
|
+
var reverseCache = /* @__PURE__ */ new WeakMap();
|
|
80587
|
+
var ownCache = /* @__PURE__ */ new WeakMap();
|
|
80588
|
+
function cachedMap(cache2, store, key, build) {
|
|
80589
|
+
let refreshed = false;
|
|
80590
|
+
const put = () => {
|
|
80591
|
+
const map2 = build();
|
|
80592
|
+
cache2.set(store, { key, map: map2 });
|
|
80593
|
+
return map2;
|
|
80594
|
+
};
|
|
80595
|
+
return {
|
|
80596
|
+
get: () => {
|
|
80597
|
+
const hit = cache2.get(store);
|
|
80598
|
+
if (hit && hit.key === key) return hit.map;
|
|
80599
|
+
refreshed = true;
|
|
80600
|
+
return put();
|
|
80601
|
+
},
|
|
80602
|
+
refresh: () => {
|
|
80603
|
+
if (refreshed) return cache2.get(store)?.map ?? put();
|
|
80604
|
+
refreshed = true;
|
|
80605
|
+
return put();
|
|
80606
|
+
}
|
|
80607
|
+
};
|
|
80608
|
+
}
|
|
80609
|
+
function lookupLive(store, m, id) {
|
|
80610
|
+
const first = m.get().get(id);
|
|
80611
|
+
if (first && isLive2(store, first)) return first;
|
|
80612
|
+
const second = m.refresh().get(id);
|
|
80613
|
+
return second && isLive2(store, second) ? second : null;
|
|
80614
|
+
}
|
|
80615
|
+
function isLive2(store, id) {
|
|
80616
|
+
const n = store.getNode(id);
|
|
80617
|
+
return n !== null && (n.validTo === null || n.validTo === void 0);
|
|
80618
|
+
}
|
|
80619
|
+
function lazy2(make) {
|
|
80620
|
+
let value;
|
|
80621
|
+
let made = false;
|
|
80622
|
+
return () => {
|
|
80623
|
+
if (!made) {
|
|
80624
|
+
value = make();
|
|
80625
|
+
made = true;
|
|
80626
|
+
}
|
|
80627
|
+
return value;
|
|
80628
|
+
};
|
|
80629
|
+
}
|
|
80519
80630
|
async function reanchorProject(opts) {
|
|
80520
80631
|
const { root, profile, store, cloud, salt } = opts;
|
|
80521
80632
|
if (!profile.projectId) return { reanchored: false, reason: "no-project" };
|
|
@@ -80526,6 +80637,7 @@ async function reanchorProject(opts) {
|
|
|
80526
80637
|
try {
|
|
80527
80638
|
const res = await cloud.getProjectPriors(profile.projectId, opts.limit ?? DEFAULT_LIMIT2);
|
|
80528
80639
|
const now = Date.now();
|
|
80640
|
+
const own = lazy2(() => buildOwnKnowledgeMap({ store, salt }));
|
|
80529
80641
|
let anchors = 0;
|
|
80530
80642
|
let dropped = 0;
|
|
80531
80643
|
for (const a of res.anchorEdges) {
|
|
@@ -80534,15 +80646,16 @@ async function reanchorProject(opts) {
|
|
|
80534
80646
|
dropped++;
|
|
80535
80647
|
continue;
|
|
80536
80648
|
}
|
|
80537
|
-
|
|
80649
|
+
const from = resolveAnchorSource(store, a.from, own);
|
|
80650
|
+
if (!from) {
|
|
80538
80651
|
dropped++;
|
|
80539
80652
|
continue;
|
|
80540
80653
|
}
|
|
80541
|
-
|
|
80542
|
-
|
|
80654
|
+
if (existingAnchor(store, from, localTo)) continue;
|
|
80655
|
+
const id = edgeId(from, "ANCHORED_AT", localTo);
|
|
80543
80656
|
const edge2 = {
|
|
80544
80657
|
id,
|
|
80545
|
-
from
|
|
80658
|
+
from,
|
|
80546
80659
|
to: localTo,
|
|
80547
80660
|
type: "ANCHORED_AT",
|
|
80548
80661
|
confidence: typeof a.attrs["confidence"] === "number" ? Math.max(0, Math.min(1, a.attrs["confidence"])) : 0.5,
|
|
@@ -80570,6 +80683,92 @@ async function reanchorProject(opts) {
|
|
|
80570
80683
|
};
|
|
80571
80684
|
}
|
|
80572
80685
|
}
|
|
80686
|
+
var SYNC_PAGE = 200;
|
|
80687
|
+
var SYNC_MAX_PAGES = 25;
|
|
80688
|
+
async function syncProjectAnchors(opts) {
|
|
80689
|
+
const { root, profile, store, cloud, salt } = opts;
|
|
80690
|
+
if (!profile.projectId) return { synced: false, reason: "no-project" };
|
|
80691
|
+
const projectId = profile.projectId;
|
|
80692
|
+
const key = `${projectId}|${salt}`;
|
|
80693
|
+
const reverse = cachedMap(reverseCache, store, key, () => buildReverseSymbolMap({ store, salt, projectId }));
|
|
80694
|
+
const own = cachedMap(ownCache, store, salt, () => buildOwnKnowledgeMap({ store, salt }));
|
|
80695
|
+
const limit = opts.limit ?? SYNC_PAGE;
|
|
80696
|
+
const maxPages = opts.maxPages ?? SYNC_MAX_PAGES;
|
|
80697
|
+
let anchors = 0;
|
|
80698
|
+
let updated = 0;
|
|
80699
|
+
let dropped = 0;
|
|
80700
|
+
let landed = 0;
|
|
80701
|
+
let cursor = profile.projectAnchorCursor ?? 0;
|
|
80702
|
+
try {
|
|
80703
|
+
for (let page = 0; page < maxPages; page++) {
|
|
80704
|
+
const res = await cloud.getProjectAnchors(projectId, cursor, limit);
|
|
80705
|
+
if (res.anchors.length === 0) break;
|
|
80706
|
+
if (reverse.get().size === 0 && reverse.refresh().size === 0) {
|
|
80707
|
+
return { synced: false, reason: "no-local-symbols" };
|
|
80708
|
+
}
|
|
80709
|
+
const byId = new Map(res.nodes.map((n) => [n.id, n]));
|
|
80710
|
+
const now = Date.now();
|
|
80711
|
+
for (const a of res.anchors) {
|
|
80712
|
+
if (a.type !== "ANCHORED_AT") continue;
|
|
80713
|
+
const localTo = lookupLive(store, reverse, a.toSymbolId);
|
|
80714
|
+
if (!localTo) {
|
|
80715
|
+
dropped++;
|
|
80716
|
+
continue;
|
|
80717
|
+
}
|
|
80718
|
+
let from = store.getNode(a.from) ? a.from : lookupLive(store, own, a.from);
|
|
80719
|
+
if (!from) {
|
|
80720
|
+
const node2 = byId.get(a.from);
|
|
80721
|
+
if (!node2) {
|
|
80722
|
+
dropped++;
|
|
80723
|
+
continue;
|
|
80724
|
+
}
|
|
80725
|
+
store.mergeNode(node2);
|
|
80726
|
+
landed++;
|
|
80727
|
+
from = node2.id;
|
|
80728
|
+
}
|
|
80729
|
+
const existing = existingAnchor(store, from, localTo);
|
|
80730
|
+
const id = existing?.id ?? edgeId(from, "ANCHORED_AT", localTo);
|
|
80731
|
+
if (existing) {
|
|
80732
|
+
if (existing.attrs["source"] !== "cloud") continue;
|
|
80733
|
+
if (existing.attrs["symbolContentHash"] === a.attrs["symbolContentHash"] && existing.attrs["observedAtCommit"] === a.attrs["observedAtCommit"]) {
|
|
80734
|
+
continue;
|
|
80735
|
+
}
|
|
80736
|
+
}
|
|
80737
|
+
const edge2 = {
|
|
80738
|
+
id,
|
|
80739
|
+
from,
|
|
80740
|
+
to: localTo,
|
|
80741
|
+
type: "ANCHORED_AT",
|
|
80742
|
+
confidence: typeof a.attrs["confidence"] === "number" ? Math.max(0, Math.min(1, a.attrs["confidence"])) : existing?.confidence ?? 0.5,
|
|
80743
|
+
extractionSource: "agent-observed",
|
|
80744
|
+
createdAt: existing?.createdAt ?? now,
|
|
80745
|
+
lastSeenAt: now,
|
|
80746
|
+
navSuccesses: existing?.navSuccesses ?? 0,
|
|
80747
|
+
navFailures: existing?.navFailures ?? 0,
|
|
80748
|
+
attrs: {
|
|
80749
|
+
...existing?.attrs ?? {},
|
|
80750
|
+
...a.attrs,
|
|
80751
|
+
source: "cloud",
|
|
80752
|
+
anchorState: classifyAnchorVersion(a.attrs, store.getNode(localTo))
|
|
80753
|
+
}
|
|
80754
|
+
};
|
|
80755
|
+
store.mergeEdge(edge2);
|
|
80756
|
+
if (existing) updated++;
|
|
80757
|
+
else anchors++;
|
|
80758
|
+
}
|
|
80759
|
+
const next = typeof res.cursor === "number" && res.cursor > cursor ? res.cursor : cursor;
|
|
80760
|
+
if (next !== cursor) {
|
|
80761
|
+
cursor = next;
|
|
80762
|
+
profile.projectAnchorCursor = cursor;
|
|
80763
|
+
saveProfile(root, profile);
|
|
80764
|
+
}
|
|
80765
|
+
if (res.anchors.length < limit) break;
|
|
80766
|
+
}
|
|
80767
|
+
return { synced: true, anchors, updated, dropped, landed, cursor };
|
|
80768
|
+
} catch (err2) {
|
|
80769
|
+
return { synced: false, reason: "error", detail: err2 instanceof Error ? err2.message : String(err2) };
|
|
80770
|
+
}
|
|
80771
|
+
}
|
|
80573
80772
|
|
|
80574
80773
|
// src/project-history.ts
|
|
80575
80774
|
init_src7();
|
|
@@ -82293,10 +82492,14 @@ function applyStopRequest(profile, now = () => /* @__PURE__ */ new Date()) {
|
|
|
82293
82492
|
}
|
|
82294
82493
|
};
|
|
82295
82494
|
}
|
|
82296
|
-
async function startHistoryStage(root,
|
|
82495
|
+
async function startHistoryStage(root, input, client, walk10 = {}) {
|
|
82496
|
+
let profile = input;
|
|
82297
82497
|
if (!profile.projectId) return { started: false, reason: "no-project", profile };
|
|
82298
|
-
const consents = historyConsents(profile);
|
|
82299
82498
|
const response = await reportHistoryStage(client, profile);
|
|
82499
|
+
if (response?.consents && walk10.applyConsents) {
|
|
82500
|
+
profile = walk10.applyConsents(profile, response.consents);
|
|
82501
|
+
}
|
|
82502
|
+
const consents = historyConsents(profile);
|
|
82300
82503
|
if (response?.stopRequested && profile.historyStage && profile.historyStage.state !== "refused") {
|
|
82301
82504
|
const stopped = applyStopRequest(profile);
|
|
82302
82505
|
await reportHistoryStage(client, stopped);
|
|
@@ -82307,17 +82510,414 @@ async function startHistoryStage(root, profile, client, walk10 = {}) {
|
|
|
82307
82510
|
return { started: true, profile: outcome.profile };
|
|
82308
82511
|
}
|
|
82309
82512
|
|
|
82513
|
+
// src/link-flow.ts
|
|
82514
|
+
init_cloud_auth();
|
|
82515
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
82516
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
82517
|
+
import { existsSync as existsSync29, writeFileSync as writeFileSync22 } from "node:fs";
|
|
82518
|
+
import { join as join34 } from "node:path";
|
|
82519
|
+
|
|
82520
|
+
// src/oauth-login.ts
|
|
82521
|
+
init_src7();
|
|
82522
|
+
init_cloud_auth();
|
|
82523
|
+
import { createServer } from "node:http";
|
|
82524
|
+
var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
82525
|
+
var strip = (u) => u.replace(/\/+$/, "");
|
|
82526
|
+
function deviceEndpoints(tokenEndpoint) {
|
|
82527
|
+
const token = new URL(tokenEndpoint);
|
|
82528
|
+
const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
|
|
82529
|
+
if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
|
|
82530
|
+
const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
|
|
82531
|
+
const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
|
|
82532
|
+
return {
|
|
82533
|
+
code: at(`${prefix}/api/auth/device/code`),
|
|
82534
|
+
token: at(`${prefix}/api/auth/device/token`),
|
|
82535
|
+
resource: at(`${prefix}/api/gate/session-token`)
|
|
82536
|
+
};
|
|
82537
|
+
}
|
|
82538
|
+
async function loginOAuthDevice(opts) {
|
|
82539
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
82540
|
+
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
82541
|
+
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
82542
|
+
const clientId = oauthClientId();
|
|
82543
|
+
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
82544
|
+
const codeResponse = await fetchFn(device.code, {
|
|
82545
|
+
method: "POST",
|
|
82546
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82547
|
+
body: JSON.stringify({ client_id: clientId, scope })
|
|
82548
|
+
});
|
|
82549
|
+
if (!codeResponse.ok) {
|
|
82550
|
+
throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
|
|
82551
|
+
}
|
|
82552
|
+
const code = await codeResponse.json();
|
|
82553
|
+
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
82554
|
+
throw new Error("device authorization returned an incomplete response");
|
|
82555
|
+
}
|
|
82556
|
+
opts.onVerification?.({
|
|
82557
|
+
url: code.verification_uri_complete ?? code.verification_uri,
|
|
82558
|
+
userCode: code.user_code,
|
|
82559
|
+
expiresInSec: code.expires_in
|
|
82560
|
+
});
|
|
82561
|
+
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
82562
|
+
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
82563
|
+
let deviceSession = null;
|
|
82564
|
+
let approvedInstallation = null;
|
|
82565
|
+
while (Date.now() < deadline) {
|
|
82566
|
+
await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
|
|
82567
|
+
const poll = await fetchFn(device.token, {
|
|
82568
|
+
method: "POST",
|
|
82569
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82570
|
+
body: JSON.stringify({
|
|
82571
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
82572
|
+
device_code: code.device_code,
|
|
82573
|
+
client_id: clientId
|
|
82574
|
+
})
|
|
82575
|
+
});
|
|
82576
|
+
const payload = await poll.json().catch(() => null);
|
|
82577
|
+
if (!poll.ok) {
|
|
82578
|
+
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
82579
|
+
if (error48 === "authorization_pending") continue;
|
|
82580
|
+
if (error48 === "slow_down") {
|
|
82581
|
+
intervalMs += 5e3;
|
|
82582
|
+
continue;
|
|
82583
|
+
}
|
|
82584
|
+
throw new Error(`device authorization failed: ${error48}`);
|
|
82585
|
+
}
|
|
82586
|
+
deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
|
|
82587
|
+
approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
|
|
82588
|
+
break;
|
|
82589
|
+
}
|
|
82590
|
+
if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
|
|
82591
|
+
if (approvedInstallation !== opts.installationId) {
|
|
82592
|
+
throw new Error("approved installation does not match the terminal request");
|
|
82593
|
+
}
|
|
82594
|
+
const resourceResponse = await fetchFn(device.resource, {
|
|
82595
|
+
method: "POST",
|
|
82596
|
+
headers: {
|
|
82597
|
+
authorization: `Bearer ${deviceSession}`,
|
|
82598
|
+
"content-type": "application/json",
|
|
82599
|
+
accept: "application/json"
|
|
82600
|
+
},
|
|
82601
|
+
body: JSON.stringify({
|
|
82602
|
+
installationId: approvedInstallation,
|
|
82603
|
+
clientId,
|
|
82604
|
+
scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
|
|
82605
|
+
})
|
|
82606
|
+
});
|
|
82607
|
+
const resource = await resourceResponse.json().catch(() => null);
|
|
82608
|
+
if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
|
|
82609
|
+
throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
|
|
82610
|
+
}
|
|
82611
|
+
return {
|
|
82612
|
+
installationId: approvedInstallation,
|
|
82613
|
+
clientId,
|
|
82614
|
+
tokenEndpoint: device.resource,
|
|
82615
|
+
tokens: {
|
|
82616
|
+
accessToken: resource["access_token"],
|
|
82617
|
+
refreshToken: deviceSession,
|
|
82618
|
+
...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
|
|
82619
|
+
}
|
|
82620
|
+
};
|
|
82621
|
+
}
|
|
82622
|
+
async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
|
|
82623
|
+
let consoleUrl = cloudUrl;
|
|
82624
|
+
try {
|
|
82625
|
+
const prm = await fetchFn(`${strip(cloudUrl)}/.well-known/oauth-protected-resource`);
|
|
82626
|
+
if (prm.ok) {
|
|
82627
|
+
const j = await prm.json();
|
|
82628
|
+
if (j.authorization_servers?.[0]) consoleUrl = j.authorization_servers[0];
|
|
82629
|
+
}
|
|
82630
|
+
} catch {
|
|
82631
|
+
}
|
|
82632
|
+
try {
|
|
82633
|
+
const asm = await fetchFn(`${strip(consoleUrl)}/.well-known/oauth-authorization-server`);
|
|
82634
|
+
if (asm.ok) {
|
|
82635
|
+
const j = await asm.json();
|
|
82636
|
+
if (j.authorization_endpoint && j.token_endpoint) {
|
|
82637
|
+
return {
|
|
82638
|
+
authorizationEndpoint: j.authorization_endpoint,
|
|
82639
|
+
tokenEndpoint: j.token_endpoint,
|
|
82640
|
+
...j.registration_endpoint ? { registrationEndpoint: j.registration_endpoint } : {}
|
|
82641
|
+
};
|
|
82642
|
+
}
|
|
82643
|
+
}
|
|
82644
|
+
} catch {
|
|
82645
|
+
}
|
|
82646
|
+
const base = strip(consoleUrl);
|
|
82647
|
+
return {
|
|
82648
|
+
authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
|
|
82649
|
+
tokenEndpoint: `${base}/api/auth/oauth2/token`
|
|
82650
|
+
};
|
|
82651
|
+
}
|
|
82652
|
+
function buildAuthorizeUrl(endpoint, p) {
|
|
82653
|
+
const u = new URL(endpoint);
|
|
82654
|
+
u.searchParams.set("response_type", "code");
|
|
82655
|
+
u.searchParams.set("client_id", p.clientId);
|
|
82656
|
+
u.searchParams.set("redirect_uri", p.redirectUri);
|
|
82657
|
+
u.searchParams.set("scope", p.scope);
|
|
82658
|
+
u.searchParams.set("state", p.state);
|
|
82659
|
+
u.searchParams.set("code_challenge", p.challenge);
|
|
82660
|
+
u.searchParams.set("code_challenge_method", p.method);
|
|
82661
|
+
return u.toString();
|
|
82662
|
+
}
|
|
82663
|
+
async function loginOAuthLoopback(opts) {
|
|
82664
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
82665
|
+
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
82666
|
+
const pkce = generatePkce();
|
|
82667
|
+
const state = randomState();
|
|
82668
|
+
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
82669
|
+
const { code, redirectUri, clientId } = await new Promise(
|
|
82670
|
+
(resolve7, reject) => {
|
|
82671
|
+
const server = createServer((req, res) => {
|
|
82672
|
+
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
82673
|
+
if (u.pathname !== "/callback") {
|
|
82674
|
+
res.writeHead(404);
|
|
82675
|
+
res.end();
|
|
82676
|
+
return;
|
|
82677
|
+
}
|
|
82678
|
+
const addr2 = server.address();
|
|
82679
|
+
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
82680
|
+
const err2 = u.searchParams.get("error");
|
|
82681
|
+
const code2 = u.searchParams.get("code");
|
|
82682
|
+
const returnedState = u.searchParams.get("state");
|
|
82683
|
+
if (err2 || !code2 || returnedState !== state) {
|
|
82684
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
82685
|
+
res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
|
|
82686
|
+
server.close();
|
|
82687
|
+
reject(new Error(err2 ?? "invalid_callback"));
|
|
82688
|
+
return;
|
|
82689
|
+
}
|
|
82690
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
82691
|
+
res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
|
|
82692
|
+
server.close();
|
|
82693
|
+
resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
|
|
82694
|
+
});
|
|
82695
|
+
let activeClientId = oauthClientId();
|
|
82696
|
+
server.on("error", reject);
|
|
82697
|
+
server.listen(0, "127.0.0.1", () => {
|
|
82698
|
+
void (async () => {
|
|
82699
|
+
try {
|
|
82700
|
+
const addr2 = server.address();
|
|
82701
|
+
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
82702
|
+
const redirectUri2 = `http://127.0.0.1:${port}/callback`;
|
|
82703
|
+
if (endpoints.registrationEndpoint) {
|
|
82704
|
+
const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
|
|
82705
|
+
redirectUri: redirectUri2,
|
|
82706
|
+
scope,
|
|
82707
|
+
clientName: "errata daemon"
|
|
82708
|
+
});
|
|
82709
|
+
activeClientId = registered.clientId;
|
|
82710
|
+
}
|
|
82711
|
+
const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
|
|
82712
|
+
clientId: activeClientId,
|
|
82713
|
+
redirectUri: redirectUri2,
|
|
82714
|
+
scope,
|
|
82715
|
+
state,
|
|
82716
|
+
challenge: pkce.challenge,
|
|
82717
|
+
method: pkce.method
|
|
82718
|
+
});
|
|
82719
|
+
if (opts.open) opts.open(url2);
|
|
82720
|
+
else console.log(`approve this login in your browser:
|
|
82721
|
+
${url2}`);
|
|
82722
|
+
} catch (err2) {
|
|
82723
|
+
server.close();
|
|
82724
|
+
reject(err2);
|
|
82725
|
+
}
|
|
82726
|
+
})();
|
|
82727
|
+
});
|
|
82728
|
+
if (opts.timeoutMs) {
|
|
82729
|
+
setTimeout(() => {
|
|
82730
|
+
server.close();
|
|
82731
|
+
reject(new Error("login timed out \u2014 run `errata login` again"));
|
|
82732
|
+
}, opts.timeoutMs).unref?.();
|
|
82733
|
+
}
|
|
82734
|
+
}
|
|
82735
|
+
);
|
|
82736
|
+
const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
|
|
82737
|
+
code,
|
|
82738
|
+
codeVerifier: pkce.verifier,
|
|
82739
|
+
redirectUri,
|
|
82740
|
+
clientId
|
|
82741
|
+
});
|
|
82742
|
+
return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
|
|
82743
|
+
}
|
|
82744
|
+
|
|
82745
|
+
// src/link-flow.ts
|
|
82746
|
+
function remoteFingerprint(locator) {
|
|
82747
|
+
return createHash15("sha256").update(locator).digest("hex");
|
|
82748
|
+
}
|
|
82749
|
+
function isDecision(value) {
|
|
82750
|
+
if (!value || typeof value !== "object") return false;
|
|
82751
|
+
const v = value;
|
|
82752
|
+
return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
|
|
82753
|
+
}
|
|
82754
|
+
async function linkThroughConsole(opts) {
|
|
82755
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
82756
|
+
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
82757
|
+
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
82758
|
+
const clientId = oauthClientId();
|
|
82759
|
+
const link = {
|
|
82760
|
+
locator: opts.facts.locator,
|
|
82761
|
+
remoteFingerprint: remoteFingerprint(opts.facts.locator),
|
|
82762
|
+
...opts.facts.name ? { name: opts.facts.name } : {},
|
|
82763
|
+
...opts.facts.rootCommitSha ? { rootCommitSha: opts.facts.rootCommitSha } : {},
|
|
82764
|
+
...opts.facts.installationId ? { installationId: opts.facts.installationId } : {},
|
|
82765
|
+
...opts.facts.agents && opts.facts.agents.length > 0 ? { agents: opts.facts.agents.slice(0, 12) } : {},
|
|
82766
|
+
...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
|
|
82767
|
+
};
|
|
82768
|
+
const codeResponse = await fetchFn(device.code, {
|
|
82769
|
+
method: "POST",
|
|
82770
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82771
|
+
body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
|
|
82772
|
+
});
|
|
82773
|
+
if (!codeResponse.ok) {
|
|
82774
|
+
const text = (await codeResponse.text()).slice(0, 200);
|
|
82775
|
+
if (codeResponse.status === 404) {
|
|
82776
|
+
throw new Error("this console does not offer the link flow yet (device authorization is off)");
|
|
82777
|
+
}
|
|
82778
|
+
throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
|
|
82779
|
+
}
|
|
82780
|
+
const code = await codeResponse.json();
|
|
82781
|
+
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
82782
|
+
throw new Error("link request returned an incomplete response");
|
|
82783
|
+
}
|
|
82784
|
+
opts.onVerification?.({
|
|
82785
|
+
url: code.verification_uri_complete ?? code.verification_uri,
|
|
82786
|
+
userCode: code.user_code,
|
|
82787
|
+
expiresInSec: code.expires_in
|
|
82788
|
+
});
|
|
82789
|
+
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
82790
|
+
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
82791
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
82792
|
+
while (Date.now() < deadline) {
|
|
82793
|
+
await sleep(intervalMs);
|
|
82794
|
+
const poll = await fetchFn(device.token, {
|
|
82795
|
+
method: "POST",
|
|
82796
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82797
|
+
body: JSON.stringify({
|
|
82798
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
82799
|
+
device_code: code.device_code,
|
|
82800
|
+
client_id: clientId
|
|
82801
|
+
})
|
|
82802
|
+
});
|
|
82803
|
+
const payload = await poll.json().catch(() => null);
|
|
82804
|
+
if (!poll.ok) {
|
|
82805
|
+
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
82806
|
+
if (error48 === "authorization_pending") continue;
|
|
82807
|
+
if (error48 === "slow_down") {
|
|
82808
|
+
intervalMs += 5e3;
|
|
82809
|
+
continue;
|
|
82810
|
+
}
|
|
82811
|
+
if (error48 === "access_denied") return { status: "denied" };
|
|
82812
|
+
if (error48 === "expired_token") return { status: "expired" };
|
|
82813
|
+
throw new Error(`link approval failed: ${error48}`);
|
|
82814
|
+
}
|
|
82815
|
+
const decision = payload?.["link"];
|
|
82816
|
+
if (!isDecision(decision)) {
|
|
82817
|
+
throw new Error("the console approved the device but returned no link decision");
|
|
82818
|
+
}
|
|
82819
|
+
return {
|
|
82820
|
+
status: "approved",
|
|
82821
|
+
decision,
|
|
82822
|
+
installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
|
|
82823
|
+
};
|
|
82824
|
+
}
|
|
82825
|
+
return { status: "expired" };
|
|
82826
|
+
}
|
|
82827
|
+
function collectRepoFacts(root) {
|
|
82828
|
+
const git3 = (args2) => {
|
|
82829
|
+
try {
|
|
82830
|
+
return execFileSync4("git", args2, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
82831
|
+
} catch {
|
|
82832
|
+
return null;
|
|
82833
|
+
}
|
|
82834
|
+
};
|
|
82835
|
+
const roots = git3(["rev-list", "--max-parents=0", "HEAD"]);
|
|
82836
|
+
const count = git3(["rev-list", "--count", "HEAD"]);
|
|
82837
|
+
const rootCommitSha = roots?.split(/\s+/).filter(Boolean).pop();
|
|
82838
|
+
const commitCount = count && /^\d+$/.test(count) ? Number(count) : void 0;
|
|
82839
|
+
return {
|
|
82840
|
+
...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
|
|
82841
|
+
...typeof commitCount === "number" ? { commitCount } : {}
|
|
82842
|
+
};
|
|
82843
|
+
}
|
|
82844
|
+
var IGNORE_STARTER = `# .errataignore \u2014 the hard control over what errata may read or send.
|
|
82845
|
+
# Paths, content keywords and tags listed here are dropped from hunks and
|
|
82846
|
+
# narrative before anything leaves this machine, and an ignored path yields no
|
|
82847
|
+
# symbol anchor at all. Scrubbing is best effort; this list is absolute.
|
|
82848
|
+
# A personal list can only add to the organization's list, never weaken it.
|
|
82849
|
+
|
|
82850
|
+
# secrets and environment
|
|
82851
|
+
.env
|
|
82852
|
+
.env.*
|
|
82853
|
+
*.pem
|
|
82854
|
+
*.key
|
|
82855
|
+
*.p12
|
|
82856
|
+
secrets/
|
|
82857
|
+
credentials*
|
|
82858
|
+
|
|
82859
|
+
# fixtures and captured data
|
|
82860
|
+
fixtures/
|
|
82861
|
+
**/testdata/
|
|
82862
|
+
*.dump
|
|
82863
|
+
*.sql.gz
|
|
82864
|
+
`;
|
|
82865
|
+
function applyConsents(profile, config2, c, decidedAt, now = Date.now) {
|
|
82866
|
+
const nextConfig = {
|
|
82867
|
+
...config2,
|
|
82868
|
+
consent: {
|
|
82869
|
+
...config2.consent,
|
|
82870
|
+
sync: c.sync !== "off",
|
|
82871
|
+
telemetry: c.telemetry,
|
|
82872
|
+
contributePackages: c.packages && c.sync !== "off"
|
|
82873
|
+
},
|
|
82874
|
+
onboardedAt: config2.onboardedAt ?? now()
|
|
82875
|
+
};
|
|
82876
|
+
const nextProfile = {
|
|
82877
|
+
...profile,
|
|
82878
|
+
syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
|
|
82879
|
+
linkConsents: {
|
|
82880
|
+
structure: c.structure,
|
|
82881
|
+
mineHistory: c.mineHistory,
|
|
82882
|
+
contributeHistory: c.contributeHistory && c.mineHistory && c.sync !== "off",
|
|
82883
|
+
decidedAt
|
|
82884
|
+
}
|
|
82885
|
+
};
|
|
82886
|
+
const prev = profile.linkConsents;
|
|
82887
|
+
const changed = profile.syncConsent !== nextProfile.syncConsent || config2.consent.sync !== nextConfig.consent.sync || config2.consent.telemetry !== nextConfig.consent.telemetry || config2.consent.contributePackages !== nextConfig.consent.contributePackages || !prev || prev.structure !== nextProfile.linkConsents.structure || prev.mineHistory !== nextProfile.linkConsents.mineHistory || prev.contributeHistory !== nextProfile.linkConsents.contributeHistory;
|
|
82888
|
+
return { profile: nextProfile, config: nextConfig, changed };
|
|
82889
|
+
}
|
|
82890
|
+
function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
|
|
82891
|
+
const applied = applyConsents(profile, config2, decision.consents, decision.approvedAt, now);
|
|
82892
|
+
const nextConfig = applied.config;
|
|
82893
|
+
const nextProfile = {
|
|
82894
|
+
...applied.profile,
|
|
82895
|
+
projectId: decision.projectId,
|
|
82896
|
+
projectLocator: decision.locator
|
|
82897
|
+
};
|
|
82898
|
+
delete nextProfile.projectLinkDisabled;
|
|
82899
|
+
let wroteIgnore = false;
|
|
82900
|
+
if (decision.writeIgnoreStarter !== false) {
|
|
82901
|
+
const target = join34(root, ".errataignore");
|
|
82902
|
+
if (!existsSync29(target)) {
|
|
82903
|
+
writeFileSync22(target, IGNORE_STARTER, "utf8");
|
|
82904
|
+
wroteIgnore = true;
|
|
82905
|
+
}
|
|
82906
|
+
}
|
|
82907
|
+
return { profile: nextProfile, config: nextConfig, wroteIgnore };
|
|
82908
|
+
}
|
|
82909
|
+
|
|
82310
82910
|
// src/multi.ts
|
|
82311
82911
|
init_registry();
|
|
82312
82912
|
|
|
82313
82913
|
// src/adopt.ts
|
|
82314
82914
|
init_git_remote();
|
|
82315
|
-
import { existsSync as
|
|
82316
|
-
import { dirname as dirname12, join as
|
|
82915
|
+
import { existsSync as existsSync30 } from "node:fs";
|
|
82916
|
+
import { dirname as dirname12, join as join35 } from "node:path";
|
|
82317
82917
|
function findGitRoot(absPath) {
|
|
82318
82918
|
let dir = absPath;
|
|
82319
82919
|
for (let depth = 0; depth < 64; depth++) {
|
|
82320
|
-
if (
|
|
82920
|
+
if (existsSync30(join35(dir, ".git"))) return dir;
|
|
82321
82921
|
const parent = dirname12(dir);
|
|
82322
82922
|
if (parent === dir) return null;
|
|
82323
82923
|
dir = parent;
|
|
@@ -82837,7 +83437,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
82837
83437
|
records.push(rec);
|
|
82838
83438
|
void ambientLinkAll();
|
|
82839
83439
|
try {
|
|
82840
|
-
|
|
83440
|
+
writeFileSync23(
|
|
82841
83441
|
rec.engine.paths.daemonLock,
|
|
82842
83442
|
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${rec.id}`, startedAt: Date.now() }),
|
|
82843
83443
|
"utf8"
|
|
@@ -83050,7 +83650,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83050
83650
|
lastActiveAt = Math.max(lastActiveAt, rec.engine.lastActivityAt());
|
|
83051
83651
|
} catch {
|
|
83052
83652
|
}
|
|
83053
|
-
const reason = retireDecision({ exists:
|
|
83653
|
+
const reason = retireDecision({ exists: existsSync31(rec.root), lastActiveAt, now });
|
|
83054
83654
|
if (reason) void retireWorkspace(rec, reason);
|
|
83055
83655
|
}
|
|
83056
83656
|
};
|
|
@@ -83118,7 +83718,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83118
83718
|
baseUrl = `http://127.0.0.1:${port}`;
|
|
83119
83719
|
try {
|
|
83120
83720
|
ensureDir(globalDir());
|
|
83121
|
-
|
|
83721
|
+
writeFileSync23(
|
|
83122
83722
|
lockPath,
|
|
83123
83723
|
JSON.stringify({ pid: process.pid, webUiUrl: baseUrl, startedAt: Date.now() }),
|
|
83124
83724
|
"utf8"
|
|
@@ -83127,7 +83727,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83127
83727
|
}
|
|
83128
83728
|
for (const r of records) {
|
|
83129
83729
|
try {
|
|
83130
|
-
|
|
83730
|
+
writeFileSync23(
|
|
83131
83731
|
r.engine.paths.daemonLock,
|
|
83132
83732
|
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${r.id}`, startedAt: Date.now() }),
|
|
83133
83733
|
"utf8"
|
|
@@ -83158,6 +83758,44 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83158
83758
|
// eslint-disable-next-line no-console
|
|
83159
83759
|
log: (line) => console.log(line)
|
|
83160
83760
|
});
|
|
83761
|
+
const pullProjectAnchors = async (r, client, salt) => {
|
|
83762
|
+
const sync = await syncProjectAnchors({
|
|
83763
|
+
root: r.root,
|
|
83764
|
+
profile: r.engine.profile,
|
|
83765
|
+
store: r.engine.store,
|
|
83766
|
+
cloud: client,
|
|
83767
|
+
salt
|
|
83768
|
+
});
|
|
83769
|
+
if (sync.synced) {
|
|
83770
|
+
if (sync.anchors + sync.updated > 0) {
|
|
83771
|
+
r.engine.markContextDirty();
|
|
83772
|
+
console.log(
|
|
83773
|
+
`[errata] anchors pulled for ${r.engine.profile.name} \u2014 ${sync.anchors} new, ${sync.updated} re-versioned, ${sync.dropped} unresolved (${sync.landed} priors landed, cursor ${sync.cursor})`
|
|
83774
|
+
);
|
|
83775
|
+
}
|
|
83776
|
+
} else if (sync.reason === "error") {
|
|
83777
|
+
console.warn(`[errata] anchor pull failed for ${r.engine.profile.name}: ${sync.detail}`);
|
|
83778
|
+
}
|
|
83779
|
+
};
|
|
83780
|
+
const ANCHOR_PULL_INTERVAL_MS = 10 * 60 * 1e3;
|
|
83781
|
+
const anchorPullAll = async () => {
|
|
83782
|
+
const c = loadConfig();
|
|
83783
|
+
if (!c.consent.sync || !hasCloudCredential(c)) return;
|
|
83784
|
+
const client = cloudNow();
|
|
83785
|
+
for (const r of records) {
|
|
83786
|
+
const projectId = r.engine.profile.projectId;
|
|
83787
|
+
if (!projectId) continue;
|
|
83788
|
+
try {
|
|
83789
|
+
const { salt } = await resolveProjectSymbolSalt(client, projectId);
|
|
83790
|
+
await pullProjectAnchors(r, client, salt);
|
|
83791
|
+
} catch (err2) {
|
|
83792
|
+
console.warn(`[errata] anchor pull skipped for ${r.engine.profile.name}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
83793
|
+
}
|
|
83794
|
+
}
|
|
83795
|
+
};
|
|
83796
|
+
const anchorPull = setInterval(() => {
|
|
83797
|
+
void anchorPullAll();
|
|
83798
|
+
}, ANCHOR_PULL_INTERVAL_MS);
|
|
83161
83799
|
const ambientLinkAll = async () => {
|
|
83162
83800
|
const c = loadConfig();
|
|
83163
83801
|
if (!c.consent.sync || !hasCloudCredential(c)) return;
|
|
@@ -83203,7 +83841,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83203
83841
|
} else if (re.reason === "error") {
|
|
83204
83842
|
console.warn(`[errata] project reanchor failed for ${r.engine.profile.name}: ${re.detail}`);
|
|
83205
83843
|
}
|
|
83206
|
-
|
|
83844
|
+
await pullProjectAnchors(r, client, salt);
|
|
83845
|
+
if (r.engine.profile.projectHydratedAt && !historyInFlight.has(r.root)) {
|
|
83207
83846
|
historyInFlight.add(r.root);
|
|
83208
83847
|
const engine = r.engine;
|
|
83209
83848
|
const root = r.root;
|
|
@@ -83217,6 +83856,22 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83217
83856
|
};
|
|
83218
83857
|
void startHistoryStage(root, engine.profile, client, {
|
|
83219
83858
|
anchors: { index: anchorIndex, salt },
|
|
83859
|
+
// PS-web projects: the console's consents (Privacy tab) win over
|
|
83860
|
+
// the profile's memory; applied to config and profile here, with
|
|
83861
|
+
// the change printed so a flipped switch is visible in the log.
|
|
83862
|
+
applyConsents: (p, consents) => {
|
|
83863
|
+
const cfg2 = loadConfig();
|
|
83864
|
+
const applied = applyConsents(p, cfg2, consents, (/* @__PURE__ */ new Date()).toISOString());
|
|
83865
|
+
if (applied.changed) {
|
|
83866
|
+
saveConfig(applied.config);
|
|
83867
|
+
Object.assign(engine.profile, applied.profile);
|
|
83868
|
+
saveProfile(root, engine.profile);
|
|
83869
|
+
console.log(
|
|
83870
|
+
`[errata] consents applied from the console for ${engine.profile.name}: sync=${consents.sync} telemetry=${consents.telemetry ? "on" : "off"} packages=${consents.packages ? "on" : "off"} structure=${consents.structure ? "on" : "off"} history=${consents.mineHistory ? consents.contributeHistory ? "mine+contribute" : "mine" : "off"}`
|
|
83871
|
+
);
|
|
83872
|
+
}
|
|
83873
|
+
return applied.profile;
|
|
83874
|
+
},
|
|
83220
83875
|
saveProfile: (p) => {
|
|
83221
83876
|
Object.assign(engine.profile, p);
|
|
83222
83877
|
saveProfile(root, engine.profile);
|
|
@@ -83838,6 +84493,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83838
84493
|
async stop() {
|
|
83839
84494
|
clearInterval(idleSweep);
|
|
83840
84495
|
clearInterval(codexWake);
|
|
84496
|
+
clearInterval(anchorPull);
|
|
83841
84497
|
clearTimeout(codexWakeBoot);
|
|
83842
84498
|
try {
|
|
83843
84499
|
const cur = readFileSync30(lockPath, "utf8");
|
|
@@ -84002,392 +84658,6 @@ function errText(err2) {
|
|
|
84002
84658
|
// src/cli.ts
|
|
84003
84659
|
init_cloud_endpoint_policy();
|
|
84004
84660
|
|
|
84005
|
-
// src/oauth-login.ts
|
|
84006
|
-
init_src7();
|
|
84007
|
-
init_cloud_auth();
|
|
84008
|
-
import { createServer } from "node:http";
|
|
84009
|
-
var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
84010
|
-
var strip = (u) => u.replace(/\/+$/, "");
|
|
84011
|
-
function deviceEndpoints(tokenEndpoint) {
|
|
84012
|
-
const token = new URL(tokenEndpoint);
|
|
84013
|
-
const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
|
|
84014
|
-
if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
|
|
84015
|
-
const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
|
|
84016
|
-
const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
|
|
84017
|
-
return {
|
|
84018
|
-
code: at(`${prefix}/api/auth/device/code`),
|
|
84019
|
-
token: at(`${prefix}/api/auth/device/token`),
|
|
84020
|
-
resource: at(`${prefix}/api/gate/session-token`)
|
|
84021
|
-
};
|
|
84022
|
-
}
|
|
84023
|
-
async function loginOAuthDevice(opts) {
|
|
84024
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84025
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84026
|
-
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
84027
|
-
const clientId = oauthClientId();
|
|
84028
|
-
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
84029
|
-
const codeResponse = await fetchFn(device.code, {
|
|
84030
|
-
method: "POST",
|
|
84031
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84032
|
-
body: JSON.stringify({ client_id: clientId, scope })
|
|
84033
|
-
});
|
|
84034
|
-
if (!codeResponse.ok) {
|
|
84035
|
-
throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
|
|
84036
|
-
}
|
|
84037
|
-
const code = await codeResponse.json();
|
|
84038
|
-
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
84039
|
-
throw new Error("device authorization returned an incomplete response");
|
|
84040
|
-
}
|
|
84041
|
-
opts.onVerification?.({
|
|
84042
|
-
url: code.verification_uri_complete ?? code.verification_uri,
|
|
84043
|
-
userCode: code.user_code,
|
|
84044
|
-
expiresInSec: code.expires_in
|
|
84045
|
-
});
|
|
84046
|
-
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
84047
|
-
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
84048
|
-
let deviceSession = null;
|
|
84049
|
-
let approvedInstallation = null;
|
|
84050
|
-
while (Date.now() < deadline) {
|
|
84051
|
-
await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
|
|
84052
|
-
const poll = await fetchFn(device.token, {
|
|
84053
|
-
method: "POST",
|
|
84054
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84055
|
-
body: JSON.stringify({
|
|
84056
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
84057
|
-
device_code: code.device_code,
|
|
84058
|
-
client_id: clientId
|
|
84059
|
-
})
|
|
84060
|
-
});
|
|
84061
|
-
const payload = await poll.json().catch(() => null);
|
|
84062
|
-
if (!poll.ok) {
|
|
84063
|
-
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
84064
|
-
if (error48 === "authorization_pending") continue;
|
|
84065
|
-
if (error48 === "slow_down") {
|
|
84066
|
-
intervalMs += 5e3;
|
|
84067
|
-
continue;
|
|
84068
|
-
}
|
|
84069
|
-
throw new Error(`device authorization failed: ${error48}`);
|
|
84070
|
-
}
|
|
84071
|
-
deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
|
|
84072
|
-
approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
|
|
84073
|
-
break;
|
|
84074
|
-
}
|
|
84075
|
-
if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
|
|
84076
|
-
if (approvedInstallation !== opts.installationId) {
|
|
84077
|
-
throw new Error("approved installation does not match the terminal request");
|
|
84078
|
-
}
|
|
84079
|
-
const resourceResponse = await fetchFn(device.resource, {
|
|
84080
|
-
method: "POST",
|
|
84081
|
-
headers: {
|
|
84082
|
-
authorization: `Bearer ${deviceSession}`,
|
|
84083
|
-
"content-type": "application/json",
|
|
84084
|
-
accept: "application/json"
|
|
84085
|
-
},
|
|
84086
|
-
body: JSON.stringify({
|
|
84087
|
-
installationId: approvedInstallation,
|
|
84088
|
-
clientId,
|
|
84089
|
-
scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
|
|
84090
|
-
})
|
|
84091
|
-
});
|
|
84092
|
-
const resource = await resourceResponse.json().catch(() => null);
|
|
84093
|
-
if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
|
|
84094
|
-
throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
|
|
84095
|
-
}
|
|
84096
|
-
return {
|
|
84097
|
-
installationId: approvedInstallation,
|
|
84098
|
-
clientId,
|
|
84099
|
-
tokenEndpoint: device.resource,
|
|
84100
|
-
tokens: {
|
|
84101
|
-
accessToken: resource["access_token"],
|
|
84102
|
-
refreshToken: deviceSession,
|
|
84103
|
-
...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
|
|
84104
|
-
}
|
|
84105
|
-
};
|
|
84106
|
-
}
|
|
84107
|
-
async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
|
|
84108
|
-
let consoleUrl = cloudUrl;
|
|
84109
|
-
try {
|
|
84110
|
-
const prm = await fetchFn(`${strip(cloudUrl)}/.well-known/oauth-protected-resource`);
|
|
84111
|
-
if (prm.ok) {
|
|
84112
|
-
const j = await prm.json();
|
|
84113
|
-
if (j.authorization_servers?.[0]) consoleUrl = j.authorization_servers[0];
|
|
84114
|
-
}
|
|
84115
|
-
} catch {
|
|
84116
|
-
}
|
|
84117
|
-
try {
|
|
84118
|
-
const asm = await fetchFn(`${strip(consoleUrl)}/.well-known/oauth-authorization-server`);
|
|
84119
|
-
if (asm.ok) {
|
|
84120
|
-
const j = await asm.json();
|
|
84121
|
-
if (j.authorization_endpoint && j.token_endpoint) {
|
|
84122
|
-
return {
|
|
84123
|
-
authorizationEndpoint: j.authorization_endpoint,
|
|
84124
|
-
tokenEndpoint: j.token_endpoint,
|
|
84125
|
-
...j.registration_endpoint ? { registrationEndpoint: j.registration_endpoint } : {}
|
|
84126
|
-
};
|
|
84127
|
-
}
|
|
84128
|
-
}
|
|
84129
|
-
} catch {
|
|
84130
|
-
}
|
|
84131
|
-
const base = strip(consoleUrl);
|
|
84132
|
-
return {
|
|
84133
|
-
authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
|
|
84134
|
-
tokenEndpoint: `${base}/api/auth/oauth2/token`
|
|
84135
|
-
};
|
|
84136
|
-
}
|
|
84137
|
-
function buildAuthorizeUrl(endpoint, p) {
|
|
84138
|
-
const u = new URL(endpoint);
|
|
84139
|
-
u.searchParams.set("response_type", "code");
|
|
84140
|
-
u.searchParams.set("client_id", p.clientId);
|
|
84141
|
-
u.searchParams.set("redirect_uri", p.redirectUri);
|
|
84142
|
-
u.searchParams.set("scope", p.scope);
|
|
84143
|
-
u.searchParams.set("state", p.state);
|
|
84144
|
-
u.searchParams.set("code_challenge", p.challenge);
|
|
84145
|
-
u.searchParams.set("code_challenge_method", p.method);
|
|
84146
|
-
return u.toString();
|
|
84147
|
-
}
|
|
84148
|
-
async function loginOAuthLoopback(opts) {
|
|
84149
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84150
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84151
|
-
const pkce = generatePkce();
|
|
84152
|
-
const state = randomState();
|
|
84153
|
-
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
84154
|
-
const { code, redirectUri, clientId } = await new Promise(
|
|
84155
|
-
(resolve7, reject) => {
|
|
84156
|
-
const server = createServer((req, res) => {
|
|
84157
|
-
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
84158
|
-
if (u.pathname !== "/callback") {
|
|
84159
|
-
res.writeHead(404);
|
|
84160
|
-
res.end();
|
|
84161
|
-
return;
|
|
84162
|
-
}
|
|
84163
|
-
const addr2 = server.address();
|
|
84164
|
-
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
84165
|
-
const err2 = u.searchParams.get("error");
|
|
84166
|
-
const code2 = u.searchParams.get("code");
|
|
84167
|
-
const returnedState = u.searchParams.get("state");
|
|
84168
|
-
if (err2 || !code2 || returnedState !== state) {
|
|
84169
|
-
res.writeHead(400, { "content-type": "text/plain" });
|
|
84170
|
-
res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
|
|
84171
|
-
server.close();
|
|
84172
|
-
reject(new Error(err2 ?? "invalid_callback"));
|
|
84173
|
-
return;
|
|
84174
|
-
}
|
|
84175
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
84176
|
-
res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
|
|
84177
|
-
server.close();
|
|
84178
|
-
resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
|
|
84179
|
-
});
|
|
84180
|
-
let activeClientId = oauthClientId();
|
|
84181
|
-
server.on("error", reject);
|
|
84182
|
-
server.listen(0, "127.0.0.1", () => {
|
|
84183
|
-
void (async () => {
|
|
84184
|
-
try {
|
|
84185
|
-
const addr2 = server.address();
|
|
84186
|
-
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
84187
|
-
const redirectUri2 = `http://127.0.0.1:${port}/callback`;
|
|
84188
|
-
if (endpoints.registrationEndpoint) {
|
|
84189
|
-
const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
|
|
84190
|
-
redirectUri: redirectUri2,
|
|
84191
|
-
scope,
|
|
84192
|
-
clientName: "errata daemon"
|
|
84193
|
-
});
|
|
84194
|
-
activeClientId = registered.clientId;
|
|
84195
|
-
}
|
|
84196
|
-
const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
|
|
84197
|
-
clientId: activeClientId,
|
|
84198
|
-
redirectUri: redirectUri2,
|
|
84199
|
-
scope,
|
|
84200
|
-
state,
|
|
84201
|
-
challenge: pkce.challenge,
|
|
84202
|
-
method: pkce.method
|
|
84203
|
-
});
|
|
84204
|
-
if (opts.open) opts.open(url2);
|
|
84205
|
-
else console.log(`approve this login in your browser:
|
|
84206
|
-
${url2}`);
|
|
84207
|
-
} catch (err2) {
|
|
84208
|
-
server.close();
|
|
84209
|
-
reject(err2);
|
|
84210
|
-
}
|
|
84211
|
-
})();
|
|
84212
|
-
});
|
|
84213
|
-
if (opts.timeoutMs) {
|
|
84214
|
-
setTimeout(() => {
|
|
84215
|
-
server.close();
|
|
84216
|
-
reject(new Error("login timed out \u2014 run `errata login` again"));
|
|
84217
|
-
}, opts.timeoutMs).unref?.();
|
|
84218
|
-
}
|
|
84219
|
-
}
|
|
84220
|
-
);
|
|
84221
|
-
const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
|
|
84222
|
-
code,
|
|
84223
|
-
codeVerifier: pkce.verifier,
|
|
84224
|
-
redirectUri,
|
|
84225
|
-
clientId
|
|
84226
|
-
});
|
|
84227
|
-
return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
|
|
84228
|
-
}
|
|
84229
|
-
|
|
84230
|
-
// src/link-flow.ts
|
|
84231
|
-
init_cloud_auth();
|
|
84232
|
-
import { createHash as createHash15 } from "node:crypto";
|
|
84233
|
-
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
84234
|
-
import { existsSync as existsSync31, writeFileSync as writeFileSync23 } from "node:fs";
|
|
84235
|
-
import { join as join35 } from "node:path";
|
|
84236
|
-
function remoteFingerprint(locator) {
|
|
84237
|
-
return createHash15("sha256").update(locator).digest("hex");
|
|
84238
|
-
}
|
|
84239
|
-
function isDecision(value) {
|
|
84240
|
-
if (!value || typeof value !== "object") return false;
|
|
84241
|
-
const v = value;
|
|
84242
|
-
return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
|
|
84243
|
-
}
|
|
84244
|
-
async function linkThroughConsole(opts) {
|
|
84245
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84246
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84247
|
-
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
84248
|
-
const clientId = oauthClientId();
|
|
84249
|
-
const link = {
|
|
84250
|
-
locator: opts.facts.locator,
|
|
84251
|
-
remoteFingerprint: remoteFingerprint(opts.facts.locator),
|
|
84252
|
-
...opts.facts.name ? { name: opts.facts.name } : {},
|
|
84253
|
-
...opts.facts.rootCommitSha ? { rootCommitSha: opts.facts.rootCommitSha } : {},
|
|
84254
|
-
...opts.facts.installationId ? { installationId: opts.facts.installationId } : {},
|
|
84255
|
-
...opts.facts.agents && opts.facts.agents.length > 0 ? { agents: opts.facts.agents.slice(0, 12) } : {},
|
|
84256
|
-
...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
|
|
84257
|
-
};
|
|
84258
|
-
const codeResponse = await fetchFn(device.code, {
|
|
84259
|
-
method: "POST",
|
|
84260
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84261
|
-
body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
|
|
84262
|
-
});
|
|
84263
|
-
if (!codeResponse.ok) {
|
|
84264
|
-
const text = (await codeResponse.text()).slice(0, 200);
|
|
84265
|
-
if (codeResponse.status === 404) {
|
|
84266
|
-
throw new Error("this console does not offer the link flow yet (device authorization is off)");
|
|
84267
|
-
}
|
|
84268
|
-
throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
|
|
84269
|
-
}
|
|
84270
|
-
const code = await codeResponse.json();
|
|
84271
|
-
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
84272
|
-
throw new Error("link request returned an incomplete response");
|
|
84273
|
-
}
|
|
84274
|
-
opts.onVerification?.({
|
|
84275
|
-
url: code.verification_uri_complete ?? code.verification_uri,
|
|
84276
|
-
userCode: code.user_code,
|
|
84277
|
-
expiresInSec: code.expires_in
|
|
84278
|
-
});
|
|
84279
|
-
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
84280
|
-
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
84281
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
84282
|
-
while (Date.now() < deadline) {
|
|
84283
|
-
await sleep(intervalMs);
|
|
84284
|
-
const poll = await fetchFn(device.token, {
|
|
84285
|
-
method: "POST",
|
|
84286
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84287
|
-
body: JSON.stringify({
|
|
84288
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
84289
|
-
device_code: code.device_code,
|
|
84290
|
-
client_id: clientId
|
|
84291
|
-
})
|
|
84292
|
-
});
|
|
84293
|
-
const payload = await poll.json().catch(() => null);
|
|
84294
|
-
if (!poll.ok) {
|
|
84295
|
-
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
84296
|
-
if (error48 === "authorization_pending") continue;
|
|
84297
|
-
if (error48 === "slow_down") {
|
|
84298
|
-
intervalMs += 5e3;
|
|
84299
|
-
continue;
|
|
84300
|
-
}
|
|
84301
|
-
if (error48 === "access_denied") return { status: "denied" };
|
|
84302
|
-
if (error48 === "expired_token") return { status: "expired" };
|
|
84303
|
-
throw new Error(`link approval failed: ${error48}`);
|
|
84304
|
-
}
|
|
84305
|
-
const decision = payload?.["link"];
|
|
84306
|
-
if (!isDecision(decision)) {
|
|
84307
|
-
throw new Error("the console approved the device but returned no link decision");
|
|
84308
|
-
}
|
|
84309
|
-
return {
|
|
84310
|
-
status: "approved",
|
|
84311
|
-
decision,
|
|
84312
|
-
installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
|
|
84313
|
-
};
|
|
84314
|
-
}
|
|
84315
|
-
return { status: "expired" };
|
|
84316
|
-
}
|
|
84317
|
-
function collectRepoFacts(root) {
|
|
84318
|
-
const git3 = (args2) => {
|
|
84319
|
-
try {
|
|
84320
|
-
return execFileSync4("git", args2, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
84321
|
-
} catch {
|
|
84322
|
-
return null;
|
|
84323
|
-
}
|
|
84324
|
-
};
|
|
84325
|
-
const roots = git3(["rev-list", "--max-parents=0", "HEAD"]);
|
|
84326
|
-
const count = git3(["rev-list", "--count", "HEAD"]);
|
|
84327
|
-
const rootCommitSha = roots?.split(/\s+/).filter(Boolean).pop();
|
|
84328
|
-
const commitCount = count && /^\d+$/.test(count) ? Number(count) : void 0;
|
|
84329
|
-
return {
|
|
84330
|
-
...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
|
|
84331
|
-
...typeof commitCount === "number" ? { commitCount } : {}
|
|
84332
|
-
};
|
|
84333
|
-
}
|
|
84334
|
-
var IGNORE_STARTER = `# .errataignore \u2014 the hard control over what errata may read or send.
|
|
84335
|
-
# Paths, content keywords and tags listed here are dropped from hunks and
|
|
84336
|
-
# narrative before anything leaves this machine, and an ignored path yields no
|
|
84337
|
-
# symbol anchor at all. Scrubbing is best effort; this list is absolute.
|
|
84338
|
-
# A personal list can only add to the organization's list, never weaken it.
|
|
84339
|
-
|
|
84340
|
-
# secrets and environment
|
|
84341
|
-
.env
|
|
84342
|
-
.env.*
|
|
84343
|
-
*.pem
|
|
84344
|
-
*.key
|
|
84345
|
-
*.p12
|
|
84346
|
-
secrets/
|
|
84347
|
-
credentials*
|
|
84348
|
-
|
|
84349
|
-
# fixtures and captured data
|
|
84350
|
-
fixtures/
|
|
84351
|
-
**/testdata/
|
|
84352
|
-
*.dump
|
|
84353
|
-
*.sql.gz
|
|
84354
|
-
`;
|
|
84355
|
-
function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
|
|
84356
|
-
const c = decision.consents;
|
|
84357
|
-
const nextConfig = {
|
|
84358
|
-
...config2,
|
|
84359
|
-
consent: {
|
|
84360
|
-
...config2.consent,
|
|
84361
|
-
sync: c.sync !== "off",
|
|
84362
|
-
telemetry: c.telemetry,
|
|
84363
|
-
contributePackages: c.packages && c.sync !== "off"
|
|
84364
|
-
},
|
|
84365
|
-
onboardedAt: config2.onboardedAt ?? now()
|
|
84366
|
-
};
|
|
84367
|
-
const nextProfile = {
|
|
84368
|
-
...profile,
|
|
84369
|
-
syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
|
|
84370
|
-
projectId: decision.projectId,
|
|
84371
|
-
projectLocator: decision.locator,
|
|
84372
|
-
linkConsents: {
|
|
84373
|
-
structure: c.structure,
|
|
84374
|
-
mineHistory: c.mineHistory,
|
|
84375
|
-
contributeHistory: c.contributeHistory && c.sync !== "off",
|
|
84376
|
-
decidedAt: decision.approvedAt
|
|
84377
|
-
}
|
|
84378
|
-
};
|
|
84379
|
-
delete nextProfile.projectLinkDisabled;
|
|
84380
|
-
let wroteIgnore = false;
|
|
84381
|
-
if (decision.writeIgnoreStarter !== false) {
|
|
84382
|
-
const target = join35(root, ".errataignore");
|
|
84383
|
-
if (!existsSync31(target)) {
|
|
84384
|
-
writeFileSync23(target, IGNORE_STARTER, "utf8");
|
|
84385
|
-
wroteIgnore = true;
|
|
84386
|
-
}
|
|
84387
|
-
}
|
|
84388
|
-
return { profile: nextProfile, config: nextConfig, wroteIgnore };
|
|
84389
|
-
}
|
|
84390
|
-
|
|
84391
84661
|
// src/device-login.ts
|
|
84392
84662
|
init_src7();
|
|
84393
84663
|
var DEFAULT_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
@@ -86070,6 +86340,13 @@ async function cmdLink(args2) {
|
|
|
86070
86340
|
}
|
|
86071
86341
|
const consents = historyConsents(applied.profile);
|
|
86072
86342
|
const reported = await reportHistoryStage(authedCloudClient(applied.config), applied.profile, (line) => console.warn(line));
|
|
86343
|
+
if (reported?.consents) {
|
|
86344
|
+
const synced = applyConsents(applied.profile, applied.config, reported.consents, (/* @__PURE__ */ new Date()).toISOString());
|
|
86345
|
+
if (synced.changed) {
|
|
86346
|
+
saveProfile(ROOT, synced.profile);
|
|
86347
|
+
saveConfig(synced.config);
|
|
86348
|
+
}
|
|
86349
|
+
}
|
|
86073
86350
|
if (!consents.mineHistory) console.log("history: not mined (consent off)");
|
|
86074
86351
|
else if (reported?.stopRequested) console.log("history: a stop request is standing in the console; the daemon honours it");
|
|
86075
86352
|
else console.log(`history: consents recorded${consents.contributeHistory ? "" : " (mine only)"} \xB7 the daemon walks the repo on its next pass`);
|
package/package.json
CHANGED
package/pass-worker.mjs
CHANGED
|
@@ -24662,6 +24662,11 @@ var SqliteGraphStore = class {
|
|
|
24662
24662
|
countByLabel: this.db.prepare(
|
|
24663
24663
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
24664
24664
|
),
|
|
24665
|
+
// Identity twin of findByLabel — same live-row semantics, two attrs read in
|
|
24666
|
+
// SQL, no attrs JSON.parse or embedding decode (PJ-anchor-reach).
|
|
24667
|
+
identitiesByLabel: this.db.prepare(
|
|
24668
|
+
"SELECT id, json_extract(attrs_json, '$.relPath') AS relPath, json_extract(attrs_json, '$.qname') AS qname FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
24669
|
+
),
|
|
24665
24670
|
// ALL versions of a label — incl frozen/closed (valid_to set). Used by the
|
|
24666
24671
|
// clean-reindex purge so a true wipe removes history too, not just live rows.
|
|
24667
24672
|
findByLabelAll: this.db.prepare("SELECT * FROM nodes WHERE label = ?"),
|
|
@@ -24962,6 +24967,14 @@ var SqliteGraphStore = class {
|
|
|
24962
24967
|
countNodesByLabel(label) {
|
|
24963
24968
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
24964
24969
|
}
|
|
24970
|
+
findNodeIdentitiesByLabel(label) {
|
|
24971
|
+
const rows = this.stmts.identitiesByLabel.all(label);
|
|
24972
|
+
return rows.map((r) => ({
|
|
24973
|
+
id: r.id,
|
|
24974
|
+
relPath: typeof r.relPath === "string" ? r.relPath : null,
|
|
24975
|
+
qname: typeof r.qname === "string" ? r.qname : null
|
|
24976
|
+
}));
|
|
24977
|
+
}
|
|
24965
24978
|
findAllVersionsByLabel(label) {
|
|
24966
24979
|
const rows = this.stmts.findByLabelAll.all(label);
|
|
24967
24980
|
return rows.map(rowToNode);
|