@inerrata-corporation/errata 2.0.6-dev.1989 → 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 +18 -3
- package/errata.mjs +705 -434
- package/package.json +1 -1
- package/pass-worker.mjs +18 -3
package/errata.mjs
CHANGED
|
@@ -15711,6 +15711,15 @@ function provenanceBand(source) {
|
|
|
15711
15711
|
function isSeedProvenance(source) {
|
|
15712
15712
|
return provenanceBand(source) === "seed";
|
|
15713
15713
|
}
|
|
15714
|
+
function anchorVersionAttrs(attrs) {
|
|
15715
|
+
if (!attrs) return {};
|
|
15716
|
+
const commit = attrs["observedAtCommit"];
|
|
15717
|
+
const hash2 = attrs["symbolContentHash"];
|
|
15718
|
+
return {
|
|
15719
|
+
...typeof commit === "string" && ANCHOR_COMMIT_SHA_RE.test(commit) ? { observedAtCommit: commit } : {},
|
|
15720
|
+
...typeof hash2 === "string" && ANCHOR_BODY_HASH_RE.test(hash2) ? { symbolContentHash: hash2 } : {}
|
|
15721
|
+
};
|
|
15722
|
+
}
|
|
15714
15723
|
function validateCastaliaPayload(p) {
|
|
15715
15724
|
const errors = [];
|
|
15716
15725
|
const labelByCid = /* @__PURE__ */ new Map();
|
|
@@ -15740,7 +15749,7 @@ function summarizeIngestResult(r) {
|
|
|
15740
15749
|
...r.patternReconciliation ? { patternReconciliation: r.patternReconciliation } : {}
|
|
15741
15750
|
};
|
|
15742
15751
|
}
|
|
15743
|
-
var CLOUD_NODE_LABELS, CLOUD_WRITABLE_NODE_LABELS, PROJECT_ONLY_NODE_LABELS, PROJECT_ONLY_NODE_LABEL_SET, PROJECT_ONLY_EDGE_TYPES, PROJECT_ONLY_EDGE_TYPE_SET, CLOUD_WRITABLE_NODE_LABEL_SET, CLOUD_EDGE_TYPES, INGEST_SOURCES, INGEST_EXTRACTION_SOURCES, QUARANTINE_EXTRACTION_SOURCE_PREFIX, MAX_NODES_PER_PAYLOAD, MAX_EDGES_PER_PAYLOAD, CastaliaIngestNodeSchema, RouteContextCountWireSchema, CastaliaIngestEdgeSchema, SymbolSummarySidecarSchema, CastaliaIngestPayloadSchema;
|
|
15752
|
+
var CLOUD_NODE_LABELS, CLOUD_WRITABLE_NODE_LABELS, PROJECT_ONLY_NODE_LABELS, PROJECT_ONLY_NODE_LABEL_SET, PROJECT_ONLY_EDGE_TYPES, PROJECT_ONLY_EDGE_TYPE_SET, CLOUD_WRITABLE_NODE_LABEL_SET, CLOUD_EDGE_TYPES, INGEST_SOURCES, INGEST_EXTRACTION_SOURCES, QUARANTINE_EXTRACTION_SOURCE_PREFIX, MAX_NODES_PER_PAYLOAD, MAX_EDGES_PER_PAYLOAD, CastaliaIngestNodeSchema, RouteContextCountWireSchema, ANCHOR_COMMIT_SHA_RE, ANCHOR_BODY_HASH_RE, CastaliaIngestEdgeSchema, SymbolSummarySidecarSchema, CastaliaIngestPayloadSchema;
|
|
15744
15753
|
var init_wire = __esm({
|
|
15745
15754
|
"../../packages/shared/src/wire.ts"() {
|
|
15746
15755
|
"use strict";
|
|
@@ -15844,6 +15853,8 @@ var init_wire = __esm({
|
|
|
15844
15853
|
independent: external_exports.number().int().min(0).optional(),
|
|
15845
15854
|
inferredIndependent: external_exports.number().int().min(0).optional()
|
|
15846
15855
|
});
|
|
15856
|
+
ANCHOR_COMMIT_SHA_RE = /^[0-9a-f]{7,40}$/;
|
|
15857
|
+
ANCHOR_BODY_HASH_RE = /^[0-9a-f]{8,64}$/;
|
|
15847
15858
|
CastaliaIngestEdgeSchema = external_exports.object({
|
|
15848
15859
|
fromCanonicalId: external_exports.string().min(6).max(160),
|
|
15849
15860
|
toCanonicalId: external_exports.string().min(6).max(160),
|
|
@@ -15862,8 +15873,8 @@ var init_wire = __esm({
|
|
|
15862
15873
|
* (reanchor) compares the hash with its local symbol's `bodyHash` to
|
|
15863
15874
|
* classify the anchor current / pre-change; absent ⇒ the pre-versioning
|
|
15864
15875
|
* contract, unverified. Only meaningful on `ANCHORED_AT`. */
|
|
15865
|
-
observedAtCommit: external_exports.string().regex(
|
|
15866
|
-
symbolContentHash: external_exports.string().regex(
|
|
15876
|
+
observedAtCommit: external_exports.string().regex(ANCHOR_COMMIT_SHA_RE).optional(),
|
|
15877
|
+
symbolContentHash: external_exports.string().regex(ANCHOR_BODY_HASH_RE).optional()
|
|
15867
15878
|
}).optional()
|
|
15868
15879
|
});
|
|
15869
15880
|
SymbolSummarySidecarSchema = external_exports.record(
|
|
@@ -15951,6 +15962,9 @@ function projectSymbolId(projectSalt, projectId, relPath, qname, kind) {
|
|
|
15951
15962
|
const path2 = normalizeSymbolPath(relPath);
|
|
15952
15963
|
return `sym_${createHmac("sha256", projectSalt).update(`${projectId}:${path2}:${qname}:${kind}`).digest("hex").slice(0, 24)}`;
|
|
15953
15964
|
}
|
|
15965
|
+
function projectSaltedNodeId(canonicalId, salt) {
|
|
15966
|
+
return `projn_${createHmac("sha256", salt).update(canonicalId).digest("hex").slice(0, 24)}`;
|
|
15967
|
+
}
|
|
15954
15968
|
function isMembraneSaltedId(id) {
|
|
15955
15969
|
return typeof id === "string" && /^(orgn_|teamn_|projn_)/.test(id);
|
|
15956
15970
|
}
|
|
@@ -16334,6 +16348,8 @@ __export(src_exports, {
|
|
|
16334
16348
|
ALL_EDGE_TYPES: () => ALL_EDGE_TYPES,
|
|
16335
16349
|
ALL_NODE_LABELS: () => ALL_NODE_LABELS,
|
|
16336
16350
|
AMBIGUOUS_ALIASES: () => AMBIGUOUS_ALIASES,
|
|
16351
|
+
ANCHOR_BODY_HASH_RE: () => ANCHOR_BODY_HASH_RE,
|
|
16352
|
+
ANCHOR_COMMIT_SHA_RE: () => ANCHOR_COMMIT_SHA_RE,
|
|
16337
16353
|
ANCHOR_REFUSAL_REASONS: () => ANCHOR_REFUSAL_REASONS,
|
|
16338
16354
|
BELIEF_EDGES: () => BELIEF_EDGES,
|
|
16339
16355
|
BRIDGE_EDGES: () => BRIDGE_EDGES,
|
|
@@ -16392,6 +16408,7 @@ __export(src_exports, {
|
|
|
16392
16408
|
VALIDATION_SOURCES: () => VALIDATION_SOURCES,
|
|
16393
16409
|
aliasesLongestFirst: () => aliasesLongestFirst,
|
|
16394
16410
|
allEntities: () => allEntities,
|
|
16411
|
+
anchorVersionAttrs: () => anchorVersionAttrs,
|
|
16395
16412
|
assertGraphEventVocabulary: () => assertGraphEventVocabulary,
|
|
16396
16413
|
buildSpine: () => buildSpine,
|
|
16397
16414
|
canonicalClaimId: () => canonicalClaimId,
|
|
@@ -16446,6 +16463,7 @@ __export(src_exports, {
|
|
|
16446
16463
|
parsePnpmLock: () => parsePnpmLock,
|
|
16447
16464
|
prefilterEntities: () => prefilterEntities,
|
|
16448
16465
|
problemIdFromError: () => problemIdFromError,
|
|
16466
|
+
projectSaltedNodeId: () => projectSaltedNodeId,
|
|
16449
16467
|
projectSymbolId: () => projectSymbolId,
|
|
16450
16468
|
provenanceBand: () => provenanceBand,
|
|
16451
16469
|
resolveCanonical: () => resolveCanonical,
|
|
@@ -17241,6 +17259,11 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17241
17259
|
countByLabel: this.db.prepare(
|
|
17242
17260
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
17243
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
|
+
),
|
|
17244
17267
|
// ALL versions of a label — incl frozen/closed (valid_to set). Used by the
|
|
17245
17268
|
// clean-reindex purge so a true wipe removes history too, not just live rows.
|
|
17246
17269
|
findByLabelAll: this.db.prepare("SELECT * FROM nodes WHERE label = ?"),
|
|
@@ -17541,6 +17564,14 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17541
17564
|
countNodesByLabel(label) {
|
|
17542
17565
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
17543
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
|
+
}
|
|
17544
17575
|
findAllVersionsByLabel(label) {
|
|
17545
17576
|
const rows = this.stmts.findByLabelAll.all(label);
|
|
17546
17577
|
return rows.map(rowToNode);
|
|
@@ -24536,7 +24567,7 @@ function toWirePayload(batch, runId) {
|
|
|
24536
24567
|
...typeof e.confidence === "number" ? { confidence: clamp012(e.confidence) } : {},
|
|
24537
24568
|
...perContext ? { perContext } : {},
|
|
24538
24569
|
// PJ-anchor-version: the two opaque version stamps ride on anchors only.
|
|
24539
|
-
...e.type === "ANCHORED_AT" ?
|
|
24570
|
+
...e.type === "ANCHORED_AT" ? anchorVersionAttrs(e.attrs) : {}
|
|
24540
24571
|
}
|
|
24541
24572
|
};
|
|
24542
24573
|
});
|
|
@@ -24572,15 +24603,6 @@ function textOrThrow(value, field) {
|
|
|
24572
24603
|
if (!text) throw new TypeError(`${field} must be a non-empty string`);
|
|
24573
24604
|
return text;
|
|
24574
24605
|
}
|
|
24575
|
-
function wireAnchorVersion(attrs) {
|
|
24576
|
-
if (!attrs) return {};
|
|
24577
|
-
const commit = attrs["observedAtCommit"];
|
|
24578
|
-
const hash2 = attrs["symbolContentHash"];
|
|
24579
|
-
return {
|
|
24580
|
-
...typeof commit === "string" && HEX_SHA.test(commit) ? { observedAtCommit: commit } : {},
|
|
24581
|
-
...typeof hash2 === "string" && HEX_BODY.test(hash2) ? { symbolContentHash: hash2 } : {}
|
|
24582
|
-
};
|
|
24583
|
-
}
|
|
24584
24606
|
function optionalEdgeAttrs(confidence) {
|
|
24585
24607
|
return typeof confidence === "number" ? { confidence: clamp012(confidence) } : void 0;
|
|
24586
24608
|
}
|
|
@@ -24706,15 +24728,13 @@ function provenanceHeaders(provenance) {
|
|
|
24706
24728
|
...provenance.agentModel ? { "x-inerrata-agent-model": provenance.agentModel } : {}
|
|
24707
24729
|
};
|
|
24708
24730
|
}
|
|
24709
|
-
var asWireCount,
|
|
24731
|
+
var asWireCount, INGEST_NODE_CHUNK, E6B_PUBLIC_CONTEXT_SENTINEL, CloudClient, CloudError;
|
|
24710
24732
|
var init_client = __esm({
|
|
24711
24733
|
"../../packages/cloud-client/src/client.ts"() {
|
|
24712
24734
|
"use strict";
|
|
24713
24735
|
init_oauth();
|
|
24714
24736
|
init_src();
|
|
24715
24737
|
asWireCount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
|
|
24716
|
-
HEX_SHA = /^[0-9a-f]{7,40}$/;
|
|
24717
|
-
HEX_BODY = /^[0-9a-f]{8,64}$/;
|
|
24718
24738
|
INGEST_NODE_CHUNK = 8;
|
|
24719
24739
|
E6B_PUBLIC_CONTEXT_SENTINEL = "00000000-0000-4e6b-8000-000000000e6b";
|
|
24720
24740
|
CloudClient = class {
|
|
@@ -25210,6 +25230,28 @@ var init_client = __esm({
|
|
|
25210
25230
|
}));
|
|
25211
25231
|
return { nodes, edges, anchorEdges, symbolIds: res.symbolIds ?? [] };
|
|
25212
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
|
+
}
|
|
25213
25255
|
/** Pull cloud-induced skills + the session-bootstrap prime payload. `seed` is the
|
|
25214
25256
|
* canonical ids of the agent's recent problems — the cloud ranks the skills whose
|
|
25215
25257
|
* motifs those problems match first (the rest are baseline). `techSeed`
|
|
@@ -70006,13 +70048,13 @@ function esc3(s) {
|
|
|
70006
70048
|
function slug(name2) {
|
|
70007
70049
|
return name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
|
|
70008
70050
|
}
|
|
70009
|
-
function
|
|
70051
|
+
function isLive3(verb) {
|
|
70010
70052
|
return LIVE_VERBS.has(verb);
|
|
70011
70053
|
}
|
|
70012
70054
|
function nextHtml(n, opts) {
|
|
70013
70055
|
if (!n) return "";
|
|
70014
70056
|
const cmd2 = `errata ${n.verb}${n.arg ? " " + n.arg : ""}`;
|
|
70015
|
-
const live2 =
|
|
70057
|
+
const live2 = isLive3(n.verb);
|
|
70016
70058
|
if (!live2 && !opts.includeFutureVerbs) return "";
|
|
70017
70059
|
const hint = n.hint ? ` \u2014 ${esc3(n.hint)}` : "";
|
|
70018
70060
|
const cls = live2 ? "next" : "next soon";
|
|
@@ -77005,7 +77047,7 @@ function createWatchBreaker(deps) {
|
|
|
77005
77047
|
}
|
|
77006
77048
|
|
|
77007
77049
|
// src/engine.ts
|
|
77008
|
-
var DAEMON_VERSION = true ? "2.0.6-dev.
|
|
77050
|
+
var DAEMON_VERSION = true ? "2.0.6-dev.2029" : "2.0.0-alpha.0";
|
|
77009
77051
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
77010
77052
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
77011
77053
|
var GIT_OP_MUTE_MS = 4e3;
|
|
@@ -79334,7 +79376,7 @@ init_identity2();
|
|
|
79334
79376
|
// src/multi.ts
|
|
79335
79377
|
init_dist();
|
|
79336
79378
|
init_src5();
|
|
79337
|
-
import { existsSync as
|
|
79379
|
+
import { existsSync as existsSync31, readFileSync as readFileSync30, unlinkSync as unlinkSync3, writeFileSync as writeFileSync23 } from "node:fs";
|
|
79338
79380
|
|
|
79339
79381
|
// src/principle-sync.ts
|
|
79340
79382
|
init_src5();
|
|
@@ -79729,15 +79771,6 @@ function contributedWireId(n) {
|
|
|
79729
79771
|
}
|
|
79730
79772
|
var WIRE_CANONICAL_ID_MIN = 6;
|
|
79731
79773
|
var SHIP_TERMINAL_REFUSALS = 3;
|
|
79732
|
-
function anchorVersionAttrs(attrs) {
|
|
79733
|
-
if (!attrs) return {};
|
|
79734
|
-
const commit = attrs["observedAtCommit"];
|
|
79735
|
-
const hash2 = attrs["symbolContentHash"];
|
|
79736
|
-
return {
|
|
79737
|
-
...typeof commit === "string" && /^[0-9a-f]{7,40}$/.test(commit) ? { observedAtCommit: commit } : {},
|
|
79738
|
-
...typeof hash2 === "string" && /^[0-9a-f]{8,64}$/.test(hash2) ? { symbolContentHash: hash2 } : {}
|
|
79739
|
-
};
|
|
79740
|
-
}
|
|
79741
79774
|
function refusalFingerprint(reason, description) {
|
|
79742
79775
|
return digest(`${reason}\0${description}`);
|
|
79743
79776
|
}
|
|
@@ -80512,16 +80545,88 @@ function buildReverseSymbolMap(opts) {
|
|
|
80512
80545
|
const { store, salt, projectId } = opts;
|
|
80513
80546
|
const reverse = /* @__PURE__ */ new Map();
|
|
80514
80547
|
for (const kind of ANCHOR_SYMBOL_KINDS) {
|
|
80515
|
-
for (const n of store.
|
|
80516
|
-
const relPath = n
|
|
80517
|
-
|
|
80518
|
-
|
|
80519
|
-
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);
|
|
80520
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);
|
|
80521
80555
|
}
|
|
80522
80556
|
}
|
|
80523
80557
|
return reverse;
|
|
80524
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
|
+
}
|
|
80525
80630
|
async function reanchorProject(opts) {
|
|
80526
80631
|
const { root, profile, store, cloud, salt } = opts;
|
|
80527
80632
|
if (!profile.projectId) return { reanchored: false, reason: "no-project" };
|
|
@@ -80532,6 +80637,7 @@ async function reanchorProject(opts) {
|
|
|
80532
80637
|
try {
|
|
80533
80638
|
const res = await cloud.getProjectPriors(profile.projectId, opts.limit ?? DEFAULT_LIMIT2);
|
|
80534
80639
|
const now = Date.now();
|
|
80640
|
+
const own = lazy2(() => buildOwnKnowledgeMap({ store, salt }));
|
|
80535
80641
|
let anchors = 0;
|
|
80536
80642
|
let dropped = 0;
|
|
80537
80643
|
for (const a of res.anchorEdges) {
|
|
@@ -80540,15 +80646,16 @@ async function reanchorProject(opts) {
|
|
|
80540
80646
|
dropped++;
|
|
80541
80647
|
continue;
|
|
80542
80648
|
}
|
|
80543
|
-
|
|
80649
|
+
const from = resolveAnchorSource(store, a.from, own);
|
|
80650
|
+
if (!from) {
|
|
80544
80651
|
dropped++;
|
|
80545
80652
|
continue;
|
|
80546
80653
|
}
|
|
80547
|
-
|
|
80548
|
-
|
|
80654
|
+
if (existingAnchor(store, from, localTo)) continue;
|
|
80655
|
+
const id = edgeId(from, "ANCHORED_AT", localTo);
|
|
80549
80656
|
const edge2 = {
|
|
80550
80657
|
id,
|
|
80551
|
-
from
|
|
80658
|
+
from,
|
|
80552
80659
|
to: localTo,
|
|
80553
80660
|
type: "ANCHORED_AT",
|
|
80554
80661
|
confidence: typeof a.attrs["confidence"] === "number" ? Math.max(0, Math.min(1, a.attrs["confidence"])) : 0.5,
|
|
@@ -80576,6 +80683,92 @@ async function reanchorProject(opts) {
|
|
|
80576
80683
|
};
|
|
80577
80684
|
}
|
|
80578
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
|
+
}
|
|
80579
80772
|
|
|
80580
80773
|
// src/project-history.ts
|
|
80581
80774
|
init_src7();
|
|
@@ -82299,10 +82492,14 @@ function applyStopRequest(profile, now = () => /* @__PURE__ */ new Date()) {
|
|
|
82299
82492
|
}
|
|
82300
82493
|
};
|
|
82301
82494
|
}
|
|
82302
|
-
async function startHistoryStage(root,
|
|
82495
|
+
async function startHistoryStage(root, input, client, walk10 = {}) {
|
|
82496
|
+
let profile = input;
|
|
82303
82497
|
if (!profile.projectId) return { started: false, reason: "no-project", profile };
|
|
82304
|
-
const consents = historyConsents(profile);
|
|
82305
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);
|
|
82306
82503
|
if (response?.stopRequested && profile.historyStage && profile.historyStage.state !== "refused") {
|
|
82307
82504
|
const stopped = applyStopRequest(profile);
|
|
82308
82505
|
await reportHistoryStage(client, stopped);
|
|
@@ -82313,17 +82510,414 @@ async function startHistoryStage(root, profile, client, walk10 = {}) {
|
|
|
82313
82510
|
return { started: true, profile: outcome.profile };
|
|
82314
82511
|
}
|
|
82315
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
|
+
|
|
82316
82910
|
// src/multi.ts
|
|
82317
82911
|
init_registry();
|
|
82318
82912
|
|
|
82319
82913
|
// src/adopt.ts
|
|
82320
82914
|
init_git_remote();
|
|
82321
|
-
import { existsSync as
|
|
82322
|
-
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";
|
|
82323
82917
|
function findGitRoot(absPath) {
|
|
82324
82918
|
let dir = absPath;
|
|
82325
82919
|
for (let depth = 0; depth < 64; depth++) {
|
|
82326
|
-
if (
|
|
82920
|
+
if (existsSync30(join35(dir, ".git"))) return dir;
|
|
82327
82921
|
const parent = dirname12(dir);
|
|
82328
82922
|
if (parent === dir) return null;
|
|
82329
82923
|
dir = parent;
|
|
@@ -82843,7 +83437,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
82843
83437
|
records.push(rec);
|
|
82844
83438
|
void ambientLinkAll();
|
|
82845
83439
|
try {
|
|
82846
|
-
|
|
83440
|
+
writeFileSync23(
|
|
82847
83441
|
rec.engine.paths.daemonLock,
|
|
82848
83442
|
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${rec.id}`, startedAt: Date.now() }),
|
|
82849
83443
|
"utf8"
|
|
@@ -83056,7 +83650,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83056
83650
|
lastActiveAt = Math.max(lastActiveAt, rec.engine.lastActivityAt());
|
|
83057
83651
|
} catch {
|
|
83058
83652
|
}
|
|
83059
|
-
const reason = retireDecision({ exists:
|
|
83653
|
+
const reason = retireDecision({ exists: existsSync31(rec.root), lastActiveAt, now });
|
|
83060
83654
|
if (reason) void retireWorkspace(rec, reason);
|
|
83061
83655
|
}
|
|
83062
83656
|
};
|
|
@@ -83124,7 +83718,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83124
83718
|
baseUrl = `http://127.0.0.1:${port}`;
|
|
83125
83719
|
try {
|
|
83126
83720
|
ensureDir(globalDir());
|
|
83127
|
-
|
|
83721
|
+
writeFileSync23(
|
|
83128
83722
|
lockPath,
|
|
83129
83723
|
JSON.stringify({ pid: process.pid, webUiUrl: baseUrl, startedAt: Date.now() }),
|
|
83130
83724
|
"utf8"
|
|
@@ -83133,7 +83727,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83133
83727
|
}
|
|
83134
83728
|
for (const r of records) {
|
|
83135
83729
|
try {
|
|
83136
|
-
|
|
83730
|
+
writeFileSync23(
|
|
83137
83731
|
r.engine.paths.daemonLock,
|
|
83138
83732
|
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${r.id}`, startedAt: Date.now() }),
|
|
83139
83733
|
"utf8"
|
|
@@ -83164,6 +83758,44 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83164
83758
|
// eslint-disable-next-line no-console
|
|
83165
83759
|
log: (line) => console.log(line)
|
|
83166
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);
|
|
83167
83799
|
const ambientLinkAll = async () => {
|
|
83168
83800
|
const c = loadConfig();
|
|
83169
83801
|
if (!c.consent.sync || !hasCloudCredential(c)) return;
|
|
@@ -83209,7 +83841,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83209
83841
|
} else if (re.reason === "error") {
|
|
83210
83842
|
console.warn(`[errata] project reanchor failed for ${r.engine.profile.name}: ${re.detail}`);
|
|
83211
83843
|
}
|
|
83212
|
-
|
|
83844
|
+
await pullProjectAnchors(r, client, salt);
|
|
83845
|
+
if (r.engine.profile.projectHydratedAt && !historyInFlight.has(r.root)) {
|
|
83213
83846
|
historyInFlight.add(r.root);
|
|
83214
83847
|
const engine = r.engine;
|
|
83215
83848
|
const root = r.root;
|
|
@@ -83223,6 +83856,22 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83223
83856
|
};
|
|
83224
83857
|
void startHistoryStage(root, engine.profile, client, {
|
|
83225
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
|
+
},
|
|
83226
83875
|
saveProfile: (p) => {
|
|
83227
83876
|
Object.assign(engine.profile, p);
|
|
83228
83877
|
saveProfile(root, engine.profile);
|
|
@@ -83844,6 +84493,7 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83844
84493
|
async stop() {
|
|
83845
84494
|
clearInterval(idleSweep);
|
|
83846
84495
|
clearInterval(codexWake);
|
|
84496
|
+
clearInterval(anchorPull);
|
|
83847
84497
|
clearTimeout(codexWakeBoot);
|
|
83848
84498
|
try {
|
|
83849
84499
|
const cur = readFileSync30(lockPath, "utf8");
|
|
@@ -84008,392 +84658,6 @@ function errText(err2) {
|
|
|
84008
84658
|
// src/cli.ts
|
|
84009
84659
|
init_cloud_endpoint_policy();
|
|
84010
84660
|
|
|
84011
|
-
// src/oauth-login.ts
|
|
84012
|
-
init_src7();
|
|
84013
|
-
init_cloud_auth();
|
|
84014
|
-
import { createServer } from "node:http";
|
|
84015
|
-
var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
84016
|
-
var strip = (u) => u.replace(/\/+$/, "");
|
|
84017
|
-
function deviceEndpoints(tokenEndpoint) {
|
|
84018
|
-
const token = new URL(tokenEndpoint);
|
|
84019
|
-
const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
|
|
84020
|
-
if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
|
|
84021
|
-
const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
|
|
84022
|
-
const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
|
|
84023
|
-
return {
|
|
84024
|
-
code: at(`${prefix}/api/auth/device/code`),
|
|
84025
|
-
token: at(`${prefix}/api/auth/device/token`),
|
|
84026
|
-
resource: at(`${prefix}/api/gate/session-token`)
|
|
84027
|
-
};
|
|
84028
|
-
}
|
|
84029
|
-
async function loginOAuthDevice(opts) {
|
|
84030
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84031
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84032
|
-
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
84033
|
-
const clientId = oauthClientId();
|
|
84034
|
-
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
84035
|
-
const codeResponse = await fetchFn(device.code, {
|
|
84036
|
-
method: "POST",
|
|
84037
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84038
|
-
body: JSON.stringify({ client_id: clientId, scope })
|
|
84039
|
-
});
|
|
84040
|
-
if (!codeResponse.ok) {
|
|
84041
|
-
throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
|
|
84042
|
-
}
|
|
84043
|
-
const code = await codeResponse.json();
|
|
84044
|
-
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
84045
|
-
throw new Error("device authorization returned an incomplete response");
|
|
84046
|
-
}
|
|
84047
|
-
opts.onVerification?.({
|
|
84048
|
-
url: code.verification_uri_complete ?? code.verification_uri,
|
|
84049
|
-
userCode: code.user_code,
|
|
84050
|
-
expiresInSec: code.expires_in
|
|
84051
|
-
});
|
|
84052
|
-
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
84053
|
-
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
84054
|
-
let deviceSession = null;
|
|
84055
|
-
let approvedInstallation = null;
|
|
84056
|
-
while (Date.now() < deadline) {
|
|
84057
|
-
await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
|
|
84058
|
-
const poll = await fetchFn(device.token, {
|
|
84059
|
-
method: "POST",
|
|
84060
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84061
|
-
body: JSON.stringify({
|
|
84062
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
84063
|
-
device_code: code.device_code,
|
|
84064
|
-
client_id: clientId
|
|
84065
|
-
})
|
|
84066
|
-
});
|
|
84067
|
-
const payload = await poll.json().catch(() => null);
|
|
84068
|
-
if (!poll.ok) {
|
|
84069
|
-
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
84070
|
-
if (error48 === "authorization_pending") continue;
|
|
84071
|
-
if (error48 === "slow_down") {
|
|
84072
|
-
intervalMs += 5e3;
|
|
84073
|
-
continue;
|
|
84074
|
-
}
|
|
84075
|
-
throw new Error(`device authorization failed: ${error48}`);
|
|
84076
|
-
}
|
|
84077
|
-
deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
|
|
84078
|
-
approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
|
|
84079
|
-
break;
|
|
84080
|
-
}
|
|
84081
|
-
if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
|
|
84082
|
-
if (approvedInstallation !== opts.installationId) {
|
|
84083
|
-
throw new Error("approved installation does not match the terminal request");
|
|
84084
|
-
}
|
|
84085
|
-
const resourceResponse = await fetchFn(device.resource, {
|
|
84086
|
-
method: "POST",
|
|
84087
|
-
headers: {
|
|
84088
|
-
authorization: `Bearer ${deviceSession}`,
|
|
84089
|
-
"content-type": "application/json",
|
|
84090
|
-
accept: "application/json"
|
|
84091
|
-
},
|
|
84092
|
-
body: JSON.stringify({
|
|
84093
|
-
installationId: approvedInstallation,
|
|
84094
|
-
clientId,
|
|
84095
|
-
scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
|
|
84096
|
-
})
|
|
84097
|
-
});
|
|
84098
|
-
const resource = await resourceResponse.json().catch(() => null);
|
|
84099
|
-
if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
|
|
84100
|
-
throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
|
|
84101
|
-
}
|
|
84102
|
-
return {
|
|
84103
|
-
installationId: approvedInstallation,
|
|
84104
|
-
clientId,
|
|
84105
|
-
tokenEndpoint: device.resource,
|
|
84106
|
-
tokens: {
|
|
84107
|
-
accessToken: resource["access_token"],
|
|
84108
|
-
refreshToken: deviceSession,
|
|
84109
|
-
...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
|
|
84110
|
-
}
|
|
84111
|
-
};
|
|
84112
|
-
}
|
|
84113
|
-
async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
|
|
84114
|
-
let consoleUrl = cloudUrl;
|
|
84115
|
-
try {
|
|
84116
|
-
const prm = await fetchFn(`${strip(cloudUrl)}/.well-known/oauth-protected-resource`);
|
|
84117
|
-
if (prm.ok) {
|
|
84118
|
-
const j = await prm.json();
|
|
84119
|
-
if (j.authorization_servers?.[0]) consoleUrl = j.authorization_servers[0];
|
|
84120
|
-
}
|
|
84121
|
-
} catch {
|
|
84122
|
-
}
|
|
84123
|
-
try {
|
|
84124
|
-
const asm = await fetchFn(`${strip(consoleUrl)}/.well-known/oauth-authorization-server`);
|
|
84125
|
-
if (asm.ok) {
|
|
84126
|
-
const j = await asm.json();
|
|
84127
|
-
if (j.authorization_endpoint && j.token_endpoint) {
|
|
84128
|
-
return {
|
|
84129
|
-
authorizationEndpoint: j.authorization_endpoint,
|
|
84130
|
-
tokenEndpoint: j.token_endpoint,
|
|
84131
|
-
...j.registration_endpoint ? { registrationEndpoint: j.registration_endpoint } : {}
|
|
84132
|
-
};
|
|
84133
|
-
}
|
|
84134
|
-
}
|
|
84135
|
-
} catch {
|
|
84136
|
-
}
|
|
84137
|
-
const base = strip(consoleUrl);
|
|
84138
|
-
return {
|
|
84139
|
-
authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
|
|
84140
|
-
tokenEndpoint: `${base}/api/auth/oauth2/token`
|
|
84141
|
-
};
|
|
84142
|
-
}
|
|
84143
|
-
function buildAuthorizeUrl(endpoint, p) {
|
|
84144
|
-
const u = new URL(endpoint);
|
|
84145
|
-
u.searchParams.set("response_type", "code");
|
|
84146
|
-
u.searchParams.set("client_id", p.clientId);
|
|
84147
|
-
u.searchParams.set("redirect_uri", p.redirectUri);
|
|
84148
|
-
u.searchParams.set("scope", p.scope);
|
|
84149
|
-
u.searchParams.set("state", p.state);
|
|
84150
|
-
u.searchParams.set("code_challenge", p.challenge);
|
|
84151
|
-
u.searchParams.set("code_challenge_method", p.method);
|
|
84152
|
-
return u.toString();
|
|
84153
|
-
}
|
|
84154
|
-
async function loginOAuthLoopback(opts) {
|
|
84155
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84156
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84157
|
-
const pkce = generatePkce();
|
|
84158
|
-
const state = randomState();
|
|
84159
|
-
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
84160
|
-
const { code, redirectUri, clientId } = await new Promise(
|
|
84161
|
-
(resolve7, reject) => {
|
|
84162
|
-
const server = createServer((req, res) => {
|
|
84163
|
-
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
84164
|
-
if (u.pathname !== "/callback") {
|
|
84165
|
-
res.writeHead(404);
|
|
84166
|
-
res.end();
|
|
84167
|
-
return;
|
|
84168
|
-
}
|
|
84169
|
-
const addr2 = server.address();
|
|
84170
|
-
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
84171
|
-
const err2 = u.searchParams.get("error");
|
|
84172
|
-
const code2 = u.searchParams.get("code");
|
|
84173
|
-
const returnedState = u.searchParams.get("state");
|
|
84174
|
-
if (err2 || !code2 || returnedState !== state) {
|
|
84175
|
-
res.writeHead(400, { "content-type": "text/plain" });
|
|
84176
|
-
res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
|
|
84177
|
-
server.close();
|
|
84178
|
-
reject(new Error(err2 ?? "invalid_callback"));
|
|
84179
|
-
return;
|
|
84180
|
-
}
|
|
84181
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
84182
|
-
res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
|
|
84183
|
-
server.close();
|
|
84184
|
-
resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
|
|
84185
|
-
});
|
|
84186
|
-
let activeClientId = oauthClientId();
|
|
84187
|
-
server.on("error", reject);
|
|
84188
|
-
server.listen(0, "127.0.0.1", () => {
|
|
84189
|
-
void (async () => {
|
|
84190
|
-
try {
|
|
84191
|
-
const addr2 = server.address();
|
|
84192
|
-
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
84193
|
-
const redirectUri2 = `http://127.0.0.1:${port}/callback`;
|
|
84194
|
-
if (endpoints.registrationEndpoint) {
|
|
84195
|
-
const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
|
|
84196
|
-
redirectUri: redirectUri2,
|
|
84197
|
-
scope,
|
|
84198
|
-
clientName: "errata daemon"
|
|
84199
|
-
});
|
|
84200
|
-
activeClientId = registered.clientId;
|
|
84201
|
-
}
|
|
84202
|
-
const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
|
|
84203
|
-
clientId: activeClientId,
|
|
84204
|
-
redirectUri: redirectUri2,
|
|
84205
|
-
scope,
|
|
84206
|
-
state,
|
|
84207
|
-
challenge: pkce.challenge,
|
|
84208
|
-
method: pkce.method
|
|
84209
|
-
});
|
|
84210
|
-
if (opts.open) opts.open(url2);
|
|
84211
|
-
else console.log(`approve this login in your browser:
|
|
84212
|
-
${url2}`);
|
|
84213
|
-
} catch (err2) {
|
|
84214
|
-
server.close();
|
|
84215
|
-
reject(err2);
|
|
84216
|
-
}
|
|
84217
|
-
})();
|
|
84218
|
-
});
|
|
84219
|
-
if (opts.timeoutMs) {
|
|
84220
|
-
setTimeout(() => {
|
|
84221
|
-
server.close();
|
|
84222
|
-
reject(new Error("login timed out \u2014 run `errata login` again"));
|
|
84223
|
-
}, opts.timeoutMs).unref?.();
|
|
84224
|
-
}
|
|
84225
|
-
}
|
|
84226
|
-
);
|
|
84227
|
-
const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
|
|
84228
|
-
code,
|
|
84229
|
-
codeVerifier: pkce.verifier,
|
|
84230
|
-
redirectUri,
|
|
84231
|
-
clientId
|
|
84232
|
-
});
|
|
84233
|
-
return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
|
|
84234
|
-
}
|
|
84235
|
-
|
|
84236
|
-
// src/link-flow.ts
|
|
84237
|
-
init_cloud_auth();
|
|
84238
|
-
import { createHash as createHash15 } from "node:crypto";
|
|
84239
|
-
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
84240
|
-
import { existsSync as existsSync31, writeFileSync as writeFileSync23 } from "node:fs";
|
|
84241
|
-
import { join as join35 } from "node:path";
|
|
84242
|
-
function remoteFingerprint(locator) {
|
|
84243
|
-
return createHash15("sha256").update(locator).digest("hex");
|
|
84244
|
-
}
|
|
84245
|
-
function isDecision(value) {
|
|
84246
|
-
if (!value || typeof value !== "object") return false;
|
|
84247
|
-
const v = value;
|
|
84248
|
-
return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
|
|
84249
|
-
}
|
|
84250
|
-
async function linkThroughConsole(opts) {
|
|
84251
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84252
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84253
|
-
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
84254
|
-
const clientId = oauthClientId();
|
|
84255
|
-
const link = {
|
|
84256
|
-
locator: opts.facts.locator,
|
|
84257
|
-
remoteFingerprint: remoteFingerprint(opts.facts.locator),
|
|
84258
|
-
...opts.facts.name ? { name: opts.facts.name } : {},
|
|
84259
|
-
...opts.facts.rootCommitSha ? { rootCommitSha: opts.facts.rootCommitSha } : {},
|
|
84260
|
-
...opts.facts.installationId ? { installationId: opts.facts.installationId } : {},
|
|
84261
|
-
...opts.facts.agents && opts.facts.agents.length > 0 ? { agents: opts.facts.agents.slice(0, 12) } : {},
|
|
84262
|
-
...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
|
|
84263
|
-
};
|
|
84264
|
-
const codeResponse = await fetchFn(device.code, {
|
|
84265
|
-
method: "POST",
|
|
84266
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84267
|
-
body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
|
|
84268
|
-
});
|
|
84269
|
-
if (!codeResponse.ok) {
|
|
84270
|
-
const text = (await codeResponse.text()).slice(0, 200);
|
|
84271
|
-
if (codeResponse.status === 404) {
|
|
84272
|
-
throw new Error("this console does not offer the link flow yet (device authorization is off)");
|
|
84273
|
-
}
|
|
84274
|
-
throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
|
|
84275
|
-
}
|
|
84276
|
-
const code = await codeResponse.json();
|
|
84277
|
-
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
84278
|
-
throw new Error("link request returned an incomplete response");
|
|
84279
|
-
}
|
|
84280
|
-
opts.onVerification?.({
|
|
84281
|
-
url: code.verification_uri_complete ?? code.verification_uri,
|
|
84282
|
-
userCode: code.user_code,
|
|
84283
|
-
expiresInSec: code.expires_in
|
|
84284
|
-
});
|
|
84285
|
-
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
84286
|
-
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
84287
|
-
const sleep = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
84288
|
-
while (Date.now() < deadline) {
|
|
84289
|
-
await sleep(intervalMs);
|
|
84290
|
-
const poll = await fetchFn(device.token, {
|
|
84291
|
-
method: "POST",
|
|
84292
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84293
|
-
body: JSON.stringify({
|
|
84294
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
84295
|
-
device_code: code.device_code,
|
|
84296
|
-
client_id: clientId
|
|
84297
|
-
})
|
|
84298
|
-
});
|
|
84299
|
-
const payload = await poll.json().catch(() => null);
|
|
84300
|
-
if (!poll.ok) {
|
|
84301
|
-
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
84302
|
-
if (error48 === "authorization_pending") continue;
|
|
84303
|
-
if (error48 === "slow_down") {
|
|
84304
|
-
intervalMs += 5e3;
|
|
84305
|
-
continue;
|
|
84306
|
-
}
|
|
84307
|
-
if (error48 === "access_denied") return { status: "denied" };
|
|
84308
|
-
if (error48 === "expired_token") return { status: "expired" };
|
|
84309
|
-
throw new Error(`link approval failed: ${error48}`);
|
|
84310
|
-
}
|
|
84311
|
-
const decision = payload?.["link"];
|
|
84312
|
-
if (!isDecision(decision)) {
|
|
84313
|
-
throw new Error("the console approved the device but returned no link decision");
|
|
84314
|
-
}
|
|
84315
|
-
return {
|
|
84316
|
-
status: "approved",
|
|
84317
|
-
decision,
|
|
84318
|
-
installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
|
|
84319
|
-
};
|
|
84320
|
-
}
|
|
84321
|
-
return { status: "expired" };
|
|
84322
|
-
}
|
|
84323
|
-
function collectRepoFacts(root) {
|
|
84324
|
-
const git3 = (args2) => {
|
|
84325
|
-
try {
|
|
84326
|
-
return execFileSync4("git", args2, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
84327
|
-
} catch {
|
|
84328
|
-
return null;
|
|
84329
|
-
}
|
|
84330
|
-
};
|
|
84331
|
-
const roots = git3(["rev-list", "--max-parents=0", "HEAD"]);
|
|
84332
|
-
const count = git3(["rev-list", "--count", "HEAD"]);
|
|
84333
|
-
const rootCommitSha = roots?.split(/\s+/).filter(Boolean).pop();
|
|
84334
|
-
const commitCount = count && /^\d+$/.test(count) ? Number(count) : void 0;
|
|
84335
|
-
return {
|
|
84336
|
-
...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
|
|
84337
|
-
...typeof commitCount === "number" ? { commitCount } : {}
|
|
84338
|
-
};
|
|
84339
|
-
}
|
|
84340
|
-
var IGNORE_STARTER = `# .errataignore \u2014 the hard control over what errata may read or send.
|
|
84341
|
-
# Paths, content keywords and tags listed here are dropped from hunks and
|
|
84342
|
-
# narrative before anything leaves this machine, and an ignored path yields no
|
|
84343
|
-
# symbol anchor at all. Scrubbing is best effort; this list is absolute.
|
|
84344
|
-
# A personal list can only add to the organization's list, never weaken it.
|
|
84345
|
-
|
|
84346
|
-
# secrets and environment
|
|
84347
|
-
.env
|
|
84348
|
-
.env.*
|
|
84349
|
-
*.pem
|
|
84350
|
-
*.key
|
|
84351
|
-
*.p12
|
|
84352
|
-
secrets/
|
|
84353
|
-
credentials*
|
|
84354
|
-
|
|
84355
|
-
# fixtures and captured data
|
|
84356
|
-
fixtures/
|
|
84357
|
-
**/testdata/
|
|
84358
|
-
*.dump
|
|
84359
|
-
*.sql.gz
|
|
84360
|
-
`;
|
|
84361
|
-
function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
|
|
84362
|
-
const c = decision.consents;
|
|
84363
|
-
const nextConfig = {
|
|
84364
|
-
...config2,
|
|
84365
|
-
consent: {
|
|
84366
|
-
...config2.consent,
|
|
84367
|
-
sync: c.sync !== "off",
|
|
84368
|
-
telemetry: c.telemetry,
|
|
84369
|
-
contributePackages: c.packages && c.sync !== "off"
|
|
84370
|
-
},
|
|
84371
|
-
onboardedAt: config2.onboardedAt ?? now()
|
|
84372
|
-
};
|
|
84373
|
-
const nextProfile = {
|
|
84374
|
-
...profile,
|
|
84375
|
-
syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
|
|
84376
|
-
projectId: decision.projectId,
|
|
84377
|
-
projectLocator: decision.locator,
|
|
84378
|
-
linkConsents: {
|
|
84379
|
-
structure: c.structure,
|
|
84380
|
-
mineHistory: c.mineHistory,
|
|
84381
|
-
contributeHistory: c.contributeHistory && c.sync !== "off",
|
|
84382
|
-
decidedAt: decision.approvedAt
|
|
84383
|
-
}
|
|
84384
|
-
};
|
|
84385
|
-
delete nextProfile.projectLinkDisabled;
|
|
84386
|
-
let wroteIgnore = false;
|
|
84387
|
-
if (decision.writeIgnoreStarter !== false) {
|
|
84388
|
-
const target = join35(root, ".errataignore");
|
|
84389
|
-
if (!existsSync31(target)) {
|
|
84390
|
-
writeFileSync23(target, IGNORE_STARTER, "utf8");
|
|
84391
|
-
wroteIgnore = true;
|
|
84392
|
-
}
|
|
84393
|
-
}
|
|
84394
|
-
return { profile: nextProfile, config: nextConfig, wroteIgnore };
|
|
84395
|
-
}
|
|
84396
|
-
|
|
84397
84661
|
// src/device-login.ts
|
|
84398
84662
|
init_src7();
|
|
84399
84663
|
var DEFAULT_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
@@ -86076,6 +86340,13 @@ async function cmdLink(args2) {
|
|
|
86076
86340
|
}
|
|
86077
86341
|
const consents = historyConsents(applied.profile);
|
|
86078
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
|
+
}
|
|
86079
86350
|
if (!consents.mineHistory) console.log("history: not mined (consent off)");
|
|
86080
86351
|
else if (reported?.stopRequested) console.log("history: a stop request is standing in the console; the daemon honours it");
|
|
86081
86352
|
else console.log(`history: consents recorded${consents.contributeHistory ? "" : " (mine only)"} \xB7 the daemon walks the repo on its next pass`);
|