@mean-weasel/lineage 0.1.3 → 0.1.5
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/CHANGELOG.md +13 -0
- package/README.md +44 -1
- package/dist/cli/lineage-dev.js +856 -215
- package/dist/cli/lineage-dev.js.map +4 -4
- package/dist/cli/lineage.js +856 -215
- package/dist/cli/lineage.js.map +4 -4
- package/dist/server.js +733 -68
- package/dist/server.js.map +4 -4
- package/dist/web/assets/index-C54JeMzP.js +22 -0
- package/dist/web/assets/index-DkLQJqxa.css +1 -0
- package/dist/web/index.html +2 -2
- package/fixtures/demo-project/lineage/swissifier-rich-demo.json +1 -1
- package/package.json +2 -1
- package/dist/web/assets/index-CTsKYXYF.js +0 -19
- package/dist/web/assets/index-CegGQ4Cv.css +0 -1
package/dist/server.js
CHANGED
|
@@ -57,7 +57,7 @@ function walkLocalReviewFiles(dir, files = []) {
|
|
|
57
57
|
throw error;
|
|
58
58
|
}
|
|
59
59
|
for (const entry of entries) {
|
|
60
|
-
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "studio-uploads") continue;
|
|
60
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "studio-uploads" || entry.name === "playwright-results" || entry.name === "lineage-demo") continue;
|
|
61
61
|
if (process.env.NODE_ENV !== "test" && /^vitest-/.test(entry.name)) continue;
|
|
62
62
|
const path = join(dir, entry.name);
|
|
63
63
|
if (entry.isDirectory()) walkLocalReviewFiles(path, files);
|
|
@@ -595,6 +595,41 @@ function lineageDb() {
|
|
|
595
595
|
);
|
|
596
596
|
create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
|
|
597
597
|
create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
|
|
598
|
+
create table if not exists agent_claims (
|
|
599
|
+
id text primary key,
|
|
600
|
+
token_hash text not null,
|
|
601
|
+
project_id text not null references projects(id),
|
|
602
|
+
channel text,
|
|
603
|
+
scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
|
|
604
|
+
target_id text not null,
|
|
605
|
+
target_title text,
|
|
606
|
+
agent_id text,
|
|
607
|
+
agent_name text not null,
|
|
608
|
+
agent_kind text not null,
|
|
609
|
+
thread_id text,
|
|
610
|
+
status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
|
|
611
|
+
created_at text not null,
|
|
612
|
+
heartbeat_at text not null,
|
|
613
|
+
expires_at text not null,
|
|
614
|
+
released_at text,
|
|
615
|
+
revoked_at text,
|
|
616
|
+
revoked_by text,
|
|
617
|
+
override_reason text,
|
|
618
|
+
metadata_json text
|
|
619
|
+
);
|
|
620
|
+
create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
|
|
621
|
+
create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
|
|
622
|
+
create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
|
|
623
|
+
create table if not exists agent_claim_events (
|
|
624
|
+
id text primary key,
|
|
625
|
+
claim_id text not null references agent_claims(id) on delete cascade,
|
|
626
|
+
event_type text not null,
|
|
627
|
+
actor text,
|
|
628
|
+
message text,
|
|
629
|
+
created_at text not null,
|
|
630
|
+
metadata_json text
|
|
631
|
+
);
|
|
632
|
+
create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
|
|
598
633
|
`);
|
|
599
634
|
migrateAssetSelections(database);
|
|
600
635
|
dropLegacyAssetSelectionRootUnique(database);
|
|
@@ -1552,6 +1587,410 @@ function activeLineageWorkspaceRoot(project) {
|
|
|
1552
1587
|
}
|
|
1553
1588
|
}
|
|
1554
1589
|
|
|
1590
|
+
// src/server/agentClaims.ts
|
|
1591
|
+
import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
|
|
1592
|
+
var defaultTtlSeconds = 20 * 60;
|
|
1593
|
+
var idleAfterSeconds = 5 * 60;
|
|
1594
|
+
var staleAfterSeconds = 15 * 60;
|
|
1595
|
+
var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
|
|
1596
|
+
var AgentClaimError = class extends Error {
|
|
1597
|
+
constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
|
|
1598
|
+
super(message);
|
|
1599
|
+
this.status = status;
|
|
1600
|
+
this.code = code;
|
|
1601
|
+
this.conflicts = conflicts;
|
|
1602
|
+
}
|
|
1603
|
+
status;
|
|
1604
|
+
code;
|
|
1605
|
+
conflicts;
|
|
1606
|
+
};
|
|
1607
|
+
function isAgentClaimError(error) {
|
|
1608
|
+
return error instanceof AgentClaimError;
|
|
1609
|
+
}
|
|
1610
|
+
function parseClaimTtl(value) {
|
|
1611
|
+
if (!value) return defaultTtlSeconds;
|
|
1612
|
+
const match = value.trim().match(/^(\d+)(s|m|h)?$/);
|
|
1613
|
+
if (!match) throw new AgentClaimError(`Invalid claim ttl: ${value}`);
|
|
1614
|
+
const amount = Number(match[1]);
|
|
1615
|
+
const unit = match[2] || "s";
|
|
1616
|
+
const multiplier = unit === "h" ? 3600 : unit === "m" ? 60 : 1;
|
|
1617
|
+
const seconds = amount * multiplier;
|
|
1618
|
+
if (!Number.isInteger(seconds) || seconds < 30 || seconds > 24 * 60 * 60) {
|
|
1619
|
+
throw new AgentClaimError("Claim ttl must be between 30 seconds and 24 hours");
|
|
1620
|
+
}
|
|
1621
|
+
return seconds;
|
|
1622
|
+
}
|
|
1623
|
+
function randomId(prefix) {
|
|
1624
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
|
|
1625
|
+
}
|
|
1626
|
+
function tokenHash(token) {
|
|
1627
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
1628
|
+
}
|
|
1629
|
+
function safeEqual(a, b) {
|
|
1630
|
+
const left = Buffer.from(a);
|
|
1631
|
+
const right = Buffer.from(b);
|
|
1632
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
1633
|
+
}
|
|
1634
|
+
function expiresAtFrom(timestamp, ttlSeconds) {
|
|
1635
|
+
return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
|
|
1636
|
+
}
|
|
1637
|
+
function metadataJson(metadata2) {
|
|
1638
|
+
return metadata2 ? JSON.stringify(metadata2) : null;
|
|
1639
|
+
}
|
|
1640
|
+
function parseMetadata(value) {
|
|
1641
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
1642
|
+
try {
|
|
1643
|
+
const parsed = JSON.parse(value);
|
|
1644
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
1645
|
+
} catch {
|
|
1646
|
+
return void 0;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function derivedState(row, now = /* @__PURE__ */ new Date()) {
|
|
1650
|
+
if (row.status !== "active") return row.status === "expired" ? "expired" : "stale";
|
|
1651
|
+
if (new Date(row.expires_at).getTime() <= now.getTime()) return "expired";
|
|
1652
|
+
const ageSeconds = Math.max(0, Math.floor((now.getTime() - new Date(row.heartbeat_at).getTime()) / 1e3));
|
|
1653
|
+
if (ageSeconds >= staleAfterSeconds) return "stale";
|
|
1654
|
+
if (ageSeconds >= idleAfterSeconds) return "idle";
|
|
1655
|
+
return "active";
|
|
1656
|
+
}
|
|
1657
|
+
function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
|
|
1658
|
+
const heartbeatAt = String(row.heartbeat_at);
|
|
1659
|
+
return {
|
|
1660
|
+
id: String(row.id),
|
|
1661
|
+
project: String(row.project_id),
|
|
1662
|
+
channel: typeof row.channel === "string" ? row.channel : void 0,
|
|
1663
|
+
scope_type: String(row.scope_type),
|
|
1664
|
+
target_id: String(row.target_id),
|
|
1665
|
+
target_title: typeof row.target_title === "string" ? row.target_title : void 0,
|
|
1666
|
+
agent_id: typeof row.agent_id === "string" ? row.agent_id : void 0,
|
|
1667
|
+
agent_name: String(row.agent_name),
|
|
1668
|
+
agent_kind: String(row.agent_kind),
|
|
1669
|
+
thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
|
|
1670
|
+
status: String(row.status),
|
|
1671
|
+
created_at: String(row.created_at),
|
|
1672
|
+
heartbeat_at: heartbeatAt,
|
|
1673
|
+
expires_at: String(row.expires_at),
|
|
1674
|
+
released_at: typeof row.released_at === "string" ? row.released_at : void 0,
|
|
1675
|
+
revoked_at: typeof row.revoked_at === "string" ? row.revoked_at : void 0,
|
|
1676
|
+
revoked_by: typeof row.revoked_by === "string" ? row.revoked_by : void 0,
|
|
1677
|
+
override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
|
|
1678
|
+
metadata: parseMetadata(row.metadata_json),
|
|
1679
|
+
heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
|
|
1680
|
+
derived_state: derivedState({
|
|
1681
|
+
expires_at: String(row.expires_at),
|
|
1682
|
+
heartbeat_at: heartbeatAt,
|
|
1683
|
+
status: String(row.status)
|
|
1684
|
+
}, now)
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function eventToRow(row) {
|
|
1688
|
+
return {
|
|
1689
|
+
claim_id: String(row.claim_id),
|
|
1690
|
+
event_type: String(row.event_type),
|
|
1691
|
+
actor: typeof row.actor === "string" ? row.actor : void 0,
|
|
1692
|
+
message: typeof row.message === "string" ? row.message : void 0,
|
|
1693
|
+
created_at: String(row.created_at),
|
|
1694
|
+
metadata: parseMetadata(row.metadata_json)
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
function ensureProject3(database, project) {
|
|
1698
|
+
const timestamp = nowIso();
|
|
1699
|
+
database.prepare(`
|
|
1700
|
+
insert into projects (id, product, created_at, updated_at)
|
|
1701
|
+
values (?, ?, ?, ?)
|
|
1702
|
+
on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
|
|
1703
|
+
`).run(project, project, timestamp, timestamp);
|
|
1704
|
+
}
|
|
1705
|
+
function recordEvent(database, claimId, eventType, actor, message, metadata2) {
|
|
1706
|
+
database.prepare(`
|
|
1707
|
+
insert into agent_claim_events (id, claim_id, event_type, actor, message, created_at, metadata_json)
|
|
1708
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
1709
|
+
`).run(randomId("claim_event"), claimId, eventType, actor || null, message || null, nowIso(), metadataJson(metadata2));
|
|
1710
|
+
}
|
|
1711
|
+
function expireActiveClaims(database) {
|
|
1712
|
+
const timestamp = nowIso();
|
|
1713
|
+
const expired = database.prepare(`
|
|
1714
|
+
select id from agent_claims where status = 'active' and expires_at <= ?
|
|
1715
|
+
`).all(timestamp);
|
|
1716
|
+
if (expired.length === 0) return;
|
|
1717
|
+
database.prepare(`
|
|
1718
|
+
update agent_claims set status = 'expired' where status = 'active' and expires_at <= ?
|
|
1719
|
+
`).run(timestamp);
|
|
1720
|
+
for (const claim of expired) recordEvent(database, claim.id, "expired", "system", "Claim expired after missed heartbeat.");
|
|
1721
|
+
}
|
|
1722
|
+
function normalizeScope(scopeType) {
|
|
1723
|
+
if (!scopes.has(scopeType)) throw new AgentClaimError(`Unsupported claim scope: ${scopeType}`);
|
|
1724
|
+
return scopeType;
|
|
1725
|
+
}
|
|
1726
|
+
function channelOverlaps(left, right) {
|
|
1727
|
+
return !left || !right || left === right;
|
|
1728
|
+
}
|
|
1729
|
+
function claimOverlaps(candidate, existing) {
|
|
1730
|
+
if (candidate.project !== existing.project) return false;
|
|
1731
|
+
if (!channelOverlaps(candidate.channel, existing.channel)) return false;
|
|
1732
|
+
if (candidate.scope_type === existing.scope_type && candidate.target_id === existing.target_id) return true;
|
|
1733
|
+
return candidate.scope_type === "project_channel" || existing.scope_type === "project_channel";
|
|
1734
|
+
}
|
|
1735
|
+
function activeClaims(database, project) {
|
|
1736
|
+
const rows = project ? database.prepare("select * from agent_claims where project_id = ? and status = 'active' order by heartbeat_at desc").all(project) : database.prepare("select * from agent_claims where status = 'active' order by project_id, heartbeat_at desc").all();
|
|
1737
|
+
return rows.map((row) => rowToClaim(row));
|
|
1738
|
+
}
|
|
1739
|
+
function findClaimById(database, claimId, project) {
|
|
1740
|
+
const row = project ? database.prepare("select * from agent_claims where project_id = ? and id = ?").get(project, claimId) : database.prepare("select * from agent_claims where id = ?").get(claimId);
|
|
1741
|
+
return row ? rowToClaim(row) : null;
|
|
1742
|
+
}
|
|
1743
|
+
function findClaimRowByToken(database, claimToken) {
|
|
1744
|
+
const claimId = claimToken.split(".")[0];
|
|
1745
|
+
const row = database.prepare("select * from agent_claims where id = ?").get(claimId);
|
|
1746
|
+
if (!row) return null;
|
|
1747
|
+
return safeEqual(String(row.token_hash), tokenHash(claimToken)) ? row : null;
|
|
1748
|
+
}
|
|
1749
|
+
function denied(code, message, conflicts = []) {
|
|
1750
|
+
return { ok: false, code, message, conflicts };
|
|
1751
|
+
}
|
|
1752
|
+
function scopeAllowsWrite(claim, scopeType, targetId, writeKind) {
|
|
1753
|
+
if (claim.scope_type === scopeType && claim.target_id === targetId) return true;
|
|
1754
|
+
if (claim.scope_type === "project_channel") return true;
|
|
1755
|
+
if (claim.scope_type === "lineage_workspace" && scopeType === "lineage_workspace") return claim.target_id === targetId;
|
|
1756
|
+
if (claim.scope_type === "content_queue_lane" && writeKind === "content_queue_next") return true;
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
function createAgentClaim(fields) {
|
|
1760
|
+
const project = fields.project.trim();
|
|
1761
|
+
const targetId = fields.targetId.trim();
|
|
1762
|
+
const agentName = fields.agentName.trim();
|
|
1763
|
+
if (!project) throw new AgentClaimError("Agent claim requires project");
|
|
1764
|
+
if (!targetId) throw new AgentClaimError("Agent claim requires target");
|
|
1765
|
+
if (!agentName) throw new AgentClaimError("Agent claim requires agent name");
|
|
1766
|
+
const scopeType = normalizeScope(fields.scopeType);
|
|
1767
|
+
const ttlSeconds = fields.ttlSeconds || defaultTtlSeconds;
|
|
1768
|
+
const database = lineageDb();
|
|
1769
|
+
try {
|
|
1770
|
+
ensureProject3(database, project);
|
|
1771
|
+
expireActiveClaims(database);
|
|
1772
|
+
const candidate = { project, channel: fields.channel?.trim() || void 0, scope_type: scopeType, target_id: targetId };
|
|
1773
|
+
const conflicts = activeClaims(database, project).filter((claim2) => claimOverlaps(candidate, claim2));
|
|
1774
|
+
if (conflicts.length > 0 && !fields.force) {
|
|
1775
|
+
throw new AgentClaimError("Target already has an active overlapping agent claim.", 409, "target_already_claimed", conflicts);
|
|
1776
|
+
}
|
|
1777
|
+
if (conflicts.length > 0 && !fields.reason?.trim()) {
|
|
1778
|
+
throw new AgentClaimError("Overriding an active claim requires --reason.", 400, "override_reason_required", conflicts);
|
|
1779
|
+
}
|
|
1780
|
+
const timestamp = nowIso();
|
|
1781
|
+
for (const conflict of conflicts) {
|
|
1782
|
+
database.prepare(`
|
|
1783
|
+
update agent_claims
|
|
1784
|
+
set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1785
|
+
where id = ? and status = 'active'
|
|
1786
|
+
`).run(timestamp, agentName, fields.reason || null, conflict.id);
|
|
1787
|
+
recordEvent(database, conflict.id, "revoked", agentName, fields.reason || "Revoked by forced claim takeover.");
|
|
1788
|
+
recordEvent(database, conflict.id, "conflict", agentName, `Overridden by ${agentName}.`, { new_claim_target: targetId });
|
|
1789
|
+
}
|
|
1790
|
+
const id = randomId("claim");
|
|
1791
|
+
const secret = randomBytes(24).toString("base64url");
|
|
1792
|
+
const claimToken = `${id}.${secret}`;
|
|
1793
|
+
const expiresAt = expiresAtFrom(timestamp, ttlSeconds);
|
|
1794
|
+
database.prepare(`
|
|
1795
|
+
insert into agent_claims (
|
|
1796
|
+
id, token_hash, project_id, channel, scope_type, target_id, target_title, agent_id, agent_name,
|
|
1797
|
+
agent_kind, thread_id, status, created_at, heartbeat_at, expires_at, metadata_json
|
|
1798
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
|
|
1799
|
+
`).run(
|
|
1800
|
+
id,
|
|
1801
|
+
tokenHash(claimToken),
|
|
1802
|
+
project,
|
|
1803
|
+
candidate.channel || null,
|
|
1804
|
+
scopeType,
|
|
1805
|
+
targetId,
|
|
1806
|
+
fields.targetTitle?.trim() || null,
|
|
1807
|
+
fields.agentId?.trim() || null,
|
|
1808
|
+
agentName,
|
|
1809
|
+
fields.agentKind?.trim() || "codex",
|
|
1810
|
+
fields.threadId?.trim() || null,
|
|
1811
|
+
timestamp,
|
|
1812
|
+
timestamp,
|
|
1813
|
+
expiresAt,
|
|
1814
|
+
metadataJson(fields.metadata)
|
|
1815
|
+
);
|
|
1816
|
+
recordEvent(database, id, "created", agentName, `Claimed ${scopeType} ${targetId}.`, { ttl_seconds: ttlSeconds });
|
|
1817
|
+
const claim = findClaimById(database, id);
|
|
1818
|
+
return { ok: true, claim, claim_token: claimToken, conflicts_revoked: conflicts.map((conflict) => conflict.id) };
|
|
1819
|
+
} finally {
|
|
1820
|
+
database.close();
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
function listAgentClaims(project) {
|
|
1824
|
+
const database = lineageDb();
|
|
1825
|
+
try {
|
|
1826
|
+
expireActiveClaims(database);
|
|
1827
|
+
const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
|
|
1828
|
+
return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
|
|
1829
|
+
} finally {
|
|
1830
|
+
database.close();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function inspectAgentClaim(claimId, project) {
|
|
1834
|
+
const database = lineageDb();
|
|
1835
|
+
try {
|
|
1836
|
+
expireActiveClaims(database);
|
|
1837
|
+
const claim = findClaimById(database, claimId, project);
|
|
1838
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1839
|
+
const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
|
|
1840
|
+
return { ok: true, claim, events: events.map(eventToRow) };
|
|
1841
|
+
} finally {
|
|
1842
|
+
database.close();
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
function heartbeatAgentClaim(claimToken, ttlSeconds = defaultTtlSeconds) {
|
|
1846
|
+
const database = lineageDb();
|
|
1847
|
+
try {
|
|
1848
|
+
expireActiveClaims(database);
|
|
1849
|
+
const row = findClaimRowByToken(database, claimToken);
|
|
1850
|
+
if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
|
|
1851
|
+
const claim = rowToClaim(row);
|
|
1852
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1853
|
+
const timestamp = nowIso();
|
|
1854
|
+
database.prepare("update agent_claims set heartbeat_at = ?, expires_at = ? where id = ?").run(timestamp, expiresAtFrom(timestamp, ttlSeconds), claim.id);
|
|
1855
|
+
recordEvent(database, claim.id, "heartbeat", claim.agent_name, "Claim heartbeat received.");
|
|
1856
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1857
|
+
} finally {
|
|
1858
|
+
database.close();
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function releaseAgentClaim(claimToken) {
|
|
1862
|
+
const database = lineageDb();
|
|
1863
|
+
try {
|
|
1864
|
+
expireActiveClaims(database);
|
|
1865
|
+
const row = findClaimRowByToken(database, claimToken);
|
|
1866
|
+
if (!row) throw new AgentClaimError("Unknown or invalid agent claim token.", 401, "claim_token_invalid");
|
|
1867
|
+
const claim = rowToClaim(row);
|
|
1868
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1869
|
+
const timestamp = nowIso();
|
|
1870
|
+
database.prepare("update agent_claims set status = 'released', released_at = ? where id = ?").run(timestamp, claim.id);
|
|
1871
|
+
recordEvent(database, claim.id, "released", claim.agent_name, "Claim released by token holder.");
|
|
1872
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1873
|
+
} finally {
|
|
1874
|
+
database.close();
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function releaseStaleAgentClaim(project, claimId, fields) {
|
|
1878
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Releasing a stale agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1879
|
+
if (!fields.reason?.trim()) throw new AgentClaimError("Releasing a stale agent claim requires a reason.", 400, "reason_required");
|
|
1880
|
+
const database = lineageDb();
|
|
1881
|
+
try {
|
|
1882
|
+
expireActiveClaims(database);
|
|
1883
|
+
const claim = findClaimById(database, claimId, project);
|
|
1884
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1885
|
+
if (claim.status !== "active" || claim.derived_state !== "stale") {
|
|
1886
|
+
throw new AgentClaimError("Only stale active claims can be released without the claim token.", 409, "claim_not_stale", [claim]);
|
|
1887
|
+
}
|
|
1888
|
+
const timestamp = nowIso();
|
|
1889
|
+
database.prepare("update agent_claims set status = 'released', released_at = ?, revoked_by = ?, override_reason = ? where id = ?").run(timestamp, fields.actor || "human", fields.reason, claim.id);
|
|
1890
|
+
recordEvent(database, claim.id, "released", fields.actor || "human", fields.reason);
|
|
1891
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1892
|
+
} finally {
|
|
1893
|
+
database.close();
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
function revokeAgentClaim(project, claimId, fields) {
|
|
1897
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Revoking an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1898
|
+
if (!fields.reason?.trim()) throw new AgentClaimError("Revoking an agent claim requires a reason.", 400, "reason_required");
|
|
1899
|
+
const database = lineageDb();
|
|
1900
|
+
try {
|
|
1901
|
+
expireActiveClaims(database);
|
|
1902
|
+
const claim = findClaimById(database, claimId, project);
|
|
1903
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1904
|
+
const timestamp = nowIso();
|
|
1905
|
+
database.prepare(`
|
|
1906
|
+
update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
|
|
1907
|
+
where id = ?
|
|
1908
|
+
`).run(timestamp, fields.actor || "human", fields.reason, claim.id);
|
|
1909
|
+
recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
|
|
1910
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1911
|
+
} finally {
|
|
1912
|
+
database.close();
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function transferAgentClaim(project, claimId, fields) {
|
|
1916
|
+
if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
|
|
1917
|
+
const toAgentName = fields.toAgentName.trim();
|
|
1918
|
+
if (!toAgentName) throw new AgentClaimError("Transfer requires toAgentName.", 400, "agent_name_required");
|
|
1919
|
+
const database = lineageDb();
|
|
1920
|
+
try {
|
|
1921
|
+
expireActiveClaims(database);
|
|
1922
|
+
const claim = findClaimById(database, claimId, project);
|
|
1923
|
+
if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
|
|
1924
|
+
if (claim.status !== "active") throw new AgentClaimError(`Agent claim is ${claim.status}.`, 409, "claim_not_active");
|
|
1925
|
+
database.prepare("update agent_claims set agent_name = ? where id = ?").run(toAgentName, claim.id);
|
|
1926
|
+
recordEvent(database, claim.id, "transferred", fields.actor || "human", fields.reason || `Transferred claim to ${toAgentName}.`, { to_agent_name: toAgentName });
|
|
1927
|
+
return { ok: true, claim: findClaimById(database, claim.id) };
|
|
1928
|
+
} finally {
|
|
1929
|
+
database.close();
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function validateAgentClaimForWrite(fields) {
|
|
1933
|
+
if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
|
|
1934
|
+
return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
|
|
1935
|
+
}
|
|
1936
|
+
if (!fields.claimToken) return denied("claim_required", "Mutating agent write requires a matching claim token.");
|
|
1937
|
+
const database = lineageDb();
|
|
1938
|
+
try {
|
|
1939
|
+
expireActiveClaims(database);
|
|
1940
|
+
const row = findClaimRowByToken(database, fields.claimToken);
|
|
1941
|
+
if (!row) return denied("claim_token_invalid", "Unknown or invalid claim token.");
|
|
1942
|
+
const claim = rowToClaim(row);
|
|
1943
|
+
if (claim.status !== "active") return denied("claim_not_active", `Agent claim is ${claim.status}.`);
|
|
1944
|
+
if (new Date(claim.expires_at).getTime() <= Date.now()) return denied("claim_expired", "Agent claim has expired.");
|
|
1945
|
+
if (claim.project !== fields.project) return denied("claim_project_mismatch", `Claim project ${claim.project} does not match ${fields.project}.`, [claim]);
|
|
1946
|
+
if (fields.channel && claim.channel && claim.channel !== fields.channel) {
|
|
1947
|
+
return denied("claim_channel_mismatch", `Claim channel ${claim.channel} does not match ${fields.channel}.`, [claim]);
|
|
1948
|
+
}
|
|
1949
|
+
if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
|
|
1950
|
+
return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
|
|
1951
|
+
}
|
|
1952
|
+
recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
|
|
1953
|
+
danger_level: fields.dangerLevel,
|
|
1954
|
+
target_id: fields.targetId,
|
|
1955
|
+
write_kind: fields.writeKind
|
|
1956
|
+
});
|
|
1957
|
+
return { ok: true, claim, warnings: [] };
|
|
1958
|
+
} finally {
|
|
1959
|
+
database.close();
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// src/server/lineageClaimGuards.ts
|
|
1964
|
+
function channelOverlaps2(left, right) {
|
|
1965
|
+
return !left || !right || left === right;
|
|
1966
|
+
}
|
|
1967
|
+
function hasActiveLineageWorkspaceClaim(project, targetId, channel) {
|
|
1968
|
+
return listAgentClaims(project).claims.some((claim) => {
|
|
1969
|
+
if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
|
|
1970
|
+
if (claim.scope_type === "lineage_workspace") return claim.target_id === targetId;
|
|
1971
|
+
return claim.scope_type === "project_channel" && channelOverlaps2(channel, claim.channel);
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
function requireLineageWorkspaceClaimForWrite(fields) {
|
|
1975
|
+
if (!fields.confirmWrite) return;
|
|
1976
|
+
const targetId = lineageWorkspaceId(fields.project, fields.rootAssetId);
|
|
1977
|
+
if (!fields.claimToken && !hasActiveLineageWorkspaceClaim(fields.project, targetId, fields.channel)) return;
|
|
1978
|
+
const validation = validateAgentClaimForWrite({
|
|
1979
|
+
channel: fields.channel,
|
|
1980
|
+
claimToken: fields.claimToken,
|
|
1981
|
+
confirmWrite: fields.confirmWrite,
|
|
1982
|
+
dangerLevel: "enforce",
|
|
1983
|
+
project: fields.project,
|
|
1984
|
+
scopeType: "lineage_workspace",
|
|
1985
|
+
targetId,
|
|
1986
|
+
writeKind: fields.writeKind
|
|
1987
|
+
});
|
|
1988
|
+
if (!validation.ok) {
|
|
1989
|
+
const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
|
|
1990
|
+
throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1555
1994
|
// src/server/assetLineage.ts
|
|
1556
1995
|
var LineageError = class extends Error {
|
|
1557
1996
|
constructor(message, status = 400) {
|
|
@@ -1646,6 +2085,45 @@ function rootFor(database, project, assetId) {
|
|
|
1646
2085
|
}
|
|
1647
2086
|
return assetId;
|
|
1648
2087
|
}
|
|
2088
|
+
function explicitWorkspaceRoot(database, project, assetId) {
|
|
2089
|
+
const row = database.prepare(`
|
|
2090
|
+
select root_asset_id from lineage_workspaces
|
|
2091
|
+
where project_id = ? and root_asset_id = ? and status != 'archived'
|
|
2092
|
+
`).get(project, assetId);
|
|
2093
|
+
return row?.root_asset_id;
|
|
2094
|
+
}
|
|
2095
|
+
function nearestWorkspaceRoot(database, project, assetId) {
|
|
2096
|
+
let current = assetId;
|
|
2097
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2098
|
+
while (!seen.has(current)) {
|
|
2099
|
+
seen.add(current);
|
|
2100
|
+
const explicit = explicitWorkspaceRoot(database, project, current);
|
|
2101
|
+
if (explicit) return explicit;
|
|
2102
|
+
const parent = parentOf(database, project, current);
|
|
2103
|
+
if (!parent) return current;
|
|
2104
|
+
current = parent;
|
|
2105
|
+
}
|
|
2106
|
+
return assetId;
|
|
2107
|
+
}
|
|
2108
|
+
function assetChannel(database, project, assetId) {
|
|
2109
|
+
const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2110
|
+
return row?.channel;
|
|
2111
|
+
}
|
|
2112
|
+
function lineageWriteClaimContext(database, project, assetId) {
|
|
2113
|
+
return {
|
|
2114
|
+
channel: assetChannel(database, project, assetId),
|
|
2115
|
+
rootAssetId: nearestWorkspaceRoot(database, project, assetId)
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function getLineageWriteClaimContext(project, assetId) {
|
|
2119
|
+
const database = lineageDb();
|
|
2120
|
+
try {
|
|
2121
|
+
requireAsset2(database, project, assetId);
|
|
2122
|
+
return lineageWriteClaimContext(database, project, assetId);
|
|
2123
|
+
} finally {
|
|
2124
|
+
database.close();
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
1649
2127
|
function latestSelectedRoot(database, project) {
|
|
1650
2128
|
const row = database.prepare("select root_asset_id from asset_selections where project_id = ? order by selected_at desc limit 1").get(project);
|
|
1651
2129
|
return row?.root_asset_id;
|
|
@@ -1676,6 +2154,20 @@ function linkLineageAssets(project, fields) {
|
|
|
1676
2154
|
requireAsset2(database, project, fields.parentAssetId);
|
|
1677
2155
|
requireAsset2(database, project, fields.childAssetId);
|
|
1678
2156
|
if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
|
|
2157
|
+
const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
|
|
2158
|
+
try {
|
|
2159
|
+
requireLineageWorkspaceClaimForWrite({
|
|
2160
|
+
channel: claimContext.channel,
|
|
2161
|
+
claimToken: fields.claimToken,
|
|
2162
|
+
confirmWrite: fields.confirmWrite,
|
|
2163
|
+
project,
|
|
2164
|
+
rootAssetId: claimContext.rootAssetId,
|
|
2165
|
+
writeKind: "lineage_link"
|
|
2166
|
+
});
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
database.close();
|
|
2169
|
+
throw error;
|
|
2170
|
+
}
|
|
1679
2171
|
const edge = {
|
|
1680
2172
|
id: edgeId(project, fields.parentAssetId, fields.childAssetId),
|
|
1681
2173
|
parent_asset_id: fields.parentAssetId,
|
|
@@ -1712,7 +2204,7 @@ function descendants(database, project, root) {
|
|
|
1712
2204
|
function getLineageSnapshot(project, assetId) {
|
|
1713
2205
|
const database = lineageDb();
|
|
1714
2206
|
requireAsset2(database, project, assetId);
|
|
1715
|
-
const root = rootFor(database, project, assetId);
|
|
2207
|
+
const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
|
|
1716
2208
|
const edges = descendants(database, project, root);
|
|
1717
2209
|
const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
|
|
1718
2210
|
const placeholders2 = ids.map(() => "?").join(",");
|
|
@@ -1766,6 +2258,19 @@ function updateLineageLayout(project, fields) {
|
|
|
1766
2258
|
const database = lineageDb();
|
|
1767
2259
|
requireAsset2(database, project, fields.rootAssetId);
|
|
1768
2260
|
for (const position of fields.positions) requireAsset2(database, project, position.assetId);
|
|
2261
|
+
try {
|
|
2262
|
+
requireLineageWorkspaceClaimForWrite({
|
|
2263
|
+
channel: assetChannel(database, project, fields.rootAssetId),
|
|
2264
|
+
claimToken: fields.claimToken,
|
|
2265
|
+
confirmWrite: fields.confirmWrite,
|
|
2266
|
+
project,
|
|
2267
|
+
rootAssetId: fields.rootAssetId,
|
|
2268
|
+
writeKind: "lineage_layout"
|
|
2269
|
+
});
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
database.close();
|
|
2272
|
+
throw error;
|
|
2273
|
+
}
|
|
1769
2274
|
if (!fields.confirmWrite) {
|
|
1770
2275
|
database.close();
|
|
1771
2276
|
return { ok: true, dryRun: true, root_asset_id: fields.rootAssetId, positions: fields.positions };
|
|
@@ -1987,9 +2492,24 @@ function getLineageBrief(project, rootAssetId) {
|
|
|
1987
2492
|
function linkSelectedLineageChild(project, fields) {
|
|
1988
2493
|
const next = getLineageNextAsset(project, fields.rootAssetId);
|
|
1989
2494
|
if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
|
|
2495
|
+
if (fields.confirmWrite) {
|
|
2496
|
+
const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
|
|
2497
|
+
const validation = validateAgentClaimForWrite({
|
|
2498
|
+
channel: claimContext.channel,
|
|
2499
|
+
claimToken: fields.claimToken,
|
|
2500
|
+
confirmWrite: fields.confirmWrite,
|
|
2501
|
+
dangerLevel: "enforce",
|
|
2502
|
+
project,
|
|
2503
|
+
scopeType: "lineage_workspace",
|
|
2504
|
+
targetId: lineageWorkspaceId(project, claimContext.rootAssetId),
|
|
2505
|
+
writeKind: "link_child"
|
|
2506
|
+
});
|
|
2507
|
+
if (!validation.ok) throw new AgentClaimError(validation.message, validation.code === "claim_required" ? 401 : 403, validation.code, validation.conflicts);
|
|
2508
|
+
}
|
|
1990
2509
|
const result = linkLineageAssets(project, {
|
|
1991
2510
|
childAssetId: fields.childAssetId,
|
|
1992
2511
|
confirmWrite: fields.confirmWrite,
|
|
2512
|
+
claimToken: fields.claimToken,
|
|
1993
2513
|
parentAssetId: next.next_asset.asset_id
|
|
1994
2514
|
});
|
|
1995
2515
|
return {
|
|
@@ -2007,6 +2527,10 @@ function requireAsset3(database, project, assetId) {
|
|
|
2007
2527
|
const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2008
2528
|
if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
|
|
2009
2529
|
}
|
|
2530
|
+
function assetChannel2(database, project, assetId) {
|
|
2531
|
+
const row = database.prepare("select channel from assets where project_id = ? and id = ?").get(project, assetId);
|
|
2532
|
+
return row?.channel;
|
|
2533
|
+
}
|
|
2010
2534
|
function parentOf2(database, project, assetId) {
|
|
2011
2535
|
const row = database.prepare("select parent_asset_id from asset_edges where project_id = ? and child_asset_id = ? order by created_at limit 1").get(project, assetId);
|
|
2012
2536
|
return row?.parent_asset_id;
|
|
@@ -2053,6 +2577,19 @@ function removeLineageNode(project, fields) {
|
|
|
2053
2577
|
database.close();
|
|
2054
2578
|
throw new LineageError(`Asset ${fields.assetId} is not in lineage rooted at ${root}`, 404);
|
|
2055
2579
|
}
|
|
2580
|
+
try {
|
|
2581
|
+
requireLineageWorkspaceClaimForWrite({
|
|
2582
|
+
channel: assetChannel2(database, project, root),
|
|
2583
|
+
claimToken: fields.claimToken,
|
|
2584
|
+
confirmWrite: fields.confirmWrite,
|
|
2585
|
+
project,
|
|
2586
|
+
rootAssetId: root,
|
|
2587
|
+
writeKind: "lineage_remove_node"
|
|
2588
|
+
});
|
|
2589
|
+
} catch (error) {
|
|
2590
|
+
database.close();
|
|
2591
|
+
throw error;
|
|
2592
|
+
}
|
|
2056
2593
|
const parentEdges = snapshot.edges.filter((edge) => edge.child_asset_id === fields.assetId);
|
|
2057
2594
|
const childEdges = snapshot.edges.filter((edge) => edge.parent_asset_id === fields.assetId);
|
|
2058
2595
|
const removedIds = [...parentEdges, ...childEdges].map((edge) => edge.id);
|
|
@@ -2867,7 +3404,7 @@ var AssetSelectionError = class extends Error {
|
|
|
2867
3404
|
function isAssetSelectionError(error) {
|
|
2868
3405
|
return error instanceof AssetSelectionError;
|
|
2869
3406
|
}
|
|
2870
|
-
function
|
|
3407
|
+
function ensureProject4(database, project) {
|
|
2871
3408
|
const timestamp = nowIso();
|
|
2872
3409
|
database.prepare(`
|
|
2873
3410
|
insert into projects (id, product, created_at, updated_at)
|
|
@@ -2953,7 +3490,7 @@ function getCurrentSet(database, project) {
|
|
|
2953
3490
|
return found;
|
|
2954
3491
|
}
|
|
2955
3492
|
function ensureSelectionSet(database, project, fields) {
|
|
2956
|
-
|
|
3493
|
+
ensureProject4(database, project);
|
|
2957
3494
|
const timestamp = nowIso();
|
|
2958
3495
|
const id = setId(project, fields.kind, fields.key);
|
|
2959
3496
|
database.prepare(`
|
|
@@ -3183,6 +3720,78 @@ function assetSelectionRouter(projectFrom2) {
|
|
|
3183
3720
|
return router;
|
|
3184
3721
|
}
|
|
3185
3722
|
|
|
3723
|
+
// src/server/agentClaimRoutes.ts
|
|
3724
|
+
function claimTokenFromRequest(req) {
|
|
3725
|
+
const header = req.header("X-Lineage-Claim-Token");
|
|
3726
|
+
if (header) return header;
|
|
3727
|
+
return typeof req.body?.claimToken === "string" ? req.body.claimToken : void 0;
|
|
3728
|
+
}
|
|
3729
|
+
function stringBody(req, key) {
|
|
3730
|
+
const value = req.body[key];
|
|
3731
|
+
return typeof value === "string" ? value : void 0;
|
|
3732
|
+
}
|
|
3733
|
+
function boolBody(req, key) {
|
|
3734
|
+
return req.body[key] === true;
|
|
3735
|
+
}
|
|
3736
|
+
function registerAgentClaimRoutes(app2, projectFrom2, asyncRoute2) {
|
|
3737
|
+
app2.get("/api/agent-claims", asyncRoute2((req, res) => {
|
|
3738
|
+
const project = typeof req.query.project === "string" ? req.query.project : void 0;
|
|
3739
|
+
res.json(listAgentClaims(project));
|
|
3740
|
+
}));
|
|
3741
|
+
app2.post("/api/agent-claims", asyncRoute2((req, res) => {
|
|
3742
|
+
res.json(createAgentClaim({
|
|
3743
|
+
agentId: stringBody(req, "agentId"),
|
|
3744
|
+
agentKind: stringBody(req, "agentKind"),
|
|
3745
|
+
agentName: stringBody(req, "agentName") || stringBody(req, "agent_name") || "",
|
|
3746
|
+
channel: stringBody(req, "channel"),
|
|
3747
|
+
force: boolBody(req, "force"),
|
|
3748
|
+
metadata: typeof req.body.metadata === "object" && req.body.metadata !== null ? req.body.metadata : void 0,
|
|
3749
|
+
project: projectFrom2({ body: req.body, query: req.query }),
|
|
3750
|
+
reason: stringBody(req, "reason"),
|
|
3751
|
+
scopeType: stringBody(req, "scopeType") || stringBody(req, "scope_type") || "",
|
|
3752
|
+
targetId: stringBody(req, "targetId") || stringBody(req, "target_id") || "",
|
|
3753
|
+
targetTitle: stringBody(req, "targetTitle") || stringBody(req, "target_title"),
|
|
3754
|
+
threadId: stringBody(req, "threadId") || stringBody(req, "thread_id"),
|
|
3755
|
+
ttlSeconds: parseClaimTtl(stringBody(req, "ttl"))
|
|
3756
|
+
}));
|
|
3757
|
+
}));
|
|
3758
|
+
app2.get("/api/agent-claims/:claimId", asyncRoute2((req, res) => {
|
|
3759
|
+
res.json(inspectAgentClaim(req.params.claimId, typeof req.query.project === "string" ? req.query.project : void 0));
|
|
3760
|
+
}));
|
|
3761
|
+
app2.post("/api/agent-claims/:claimId/heartbeat", asyncRoute2((req, res) => {
|
|
3762
|
+
const claimToken = claimTokenFromRequest(req);
|
|
3763
|
+
if (!claimToken) throw new AgentClaimError("Heartbeat requires claimToken", 400, "claim_token_required");
|
|
3764
|
+
res.json(heartbeatAgentClaim(claimToken, parseClaimTtl(stringBody(req, "ttl"))));
|
|
3765
|
+
}));
|
|
3766
|
+
app2.post("/api/agent-claims/:claimId/release", asyncRoute2((req, res) => {
|
|
3767
|
+
const claimToken = claimTokenFromRequest(req);
|
|
3768
|
+
if (!claimToken) throw new AgentClaimError("Release requires claimToken", 400, "claim_token_required");
|
|
3769
|
+
res.json(releaseAgentClaim(claimToken));
|
|
3770
|
+
}));
|
|
3771
|
+
app2.post("/api/agent-claims/:claimId/release-stale", asyncRoute2((req, res) => {
|
|
3772
|
+
res.json(releaseStaleAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
|
|
3773
|
+
actor: stringBody(req, "actor") || "human",
|
|
3774
|
+
confirmWrite: boolBody(req, "confirmWrite"),
|
|
3775
|
+
reason: stringBody(req, "reason")
|
|
3776
|
+
}));
|
|
3777
|
+
}));
|
|
3778
|
+
app2.post("/api/agent-claims/:claimId/revoke", asyncRoute2((req, res) => {
|
|
3779
|
+
res.json(revokeAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
|
|
3780
|
+
actor: stringBody(req, "actor") || "human",
|
|
3781
|
+
confirmWrite: boolBody(req, "confirmWrite"),
|
|
3782
|
+
reason: stringBody(req, "reason")
|
|
3783
|
+
}));
|
|
3784
|
+
}));
|
|
3785
|
+
app2.post("/api/agent-claims/:claimId/transfer", asyncRoute2((req, res) => {
|
|
3786
|
+
res.json(transferAgentClaim(projectFrom2({ body: req.body, query: req.query }), req.params.claimId, {
|
|
3787
|
+
actor: stringBody(req, "actor") || "human",
|
|
3788
|
+
confirmWrite: boolBody(req, "confirmWrite"),
|
|
3789
|
+
reason: stringBody(req, "reason"),
|
|
3790
|
+
toAgentName: stringBody(req, "toAgentName") || stringBody(req, "to_agent_name") || ""
|
|
3791
|
+
}));
|
|
3792
|
+
}));
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3186
3795
|
// src/server/adapters/adapterSettings.ts
|
|
3187
3796
|
var AdapterSettingsError = class extends Error {
|
|
3188
3797
|
constructor(message, status = 400) {
|
|
@@ -3227,7 +3836,7 @@ var definitions = [
|
|
|
3227
3836
|
secret_ref: null
|
|
3228
3837
|
}
|
|
3229
3838
|
];
|
|
3230
|
-
function
|
|
3839
|
+
function ensureProject5(database, project) {
|
|
3231
3840
|
const timestamp = nowIso();
|
|
3232
3841
|
database.prepare(`
|
|
3233
3842
|
insert into projects (id, product, created_at, updated_at)
|
|
@@ -3241,7 +3850,7 @@ function definitionFor(adapterType, provider) {
|
|
|
3241
3850
|
return definition;
|
|
3242
3851
|
}
|
|
3243
3852
|
function seedDefaults(database, project) {
|
|
3244
|
-
|
|
3853
|
+
ensureProject5(database, project);
|
|
3245
3854
|
const timestamp = nowIso();
|
|
3246
3855
|
for (const definition of definitions) {
|
|
3247
3856
|
database.prepare(`
|
|
@@ -3500,7 +4109,7 @@ var ContentBatchError = class extends Error {
|
|
|
3500
4109
|
function isContentBatchError(error) {
|
|
3501
4110
|
return error instanceof ContentBatchError;
|
|
3502
4111
|
}
|
|
3503
|
-
function
|
|
4112
|
+
function ensureProject6(database, project) {
|
|
3504
4113
|
const timestamp = nowIso();
|
|
3505
4114
|
database.prepare(`
|
|
3506
4115
|
insert into projects (id, product, created_at, updated_at)
|
|
@@ -3628,7 +4237,7 @@ function createContentBatch(project, fields) {
|
|
|
3628
4237
|
const database = lineageDb();
|
|
3629
4238
|
const timestamp = nowIso();
|
|
3630
4239
|
try {
|
|
3631
|
-
|
|
4240
|
+
ensureProject6(database, project);
|
|
3632
4241
|
database.prepare(`
|
|
3633
4242
|
insert into content_batches (id, project_id, title, campaign, channel, status, notes, created_at, updated_at)
|
|
3634
4243
|
values (?, ?, ?, ?, ?, 'active', ?, ?, ?)
|
|
@@ -3650,7 +4259,7 @@ function createContentPost(project, fields) {
|
|
|
3650
4259
|
const database = lineageDb();
|
|
3651
4260
|
const timestamp = nowIso();
|
|
3652
4261
|
try {
|
|
3653
|
-
|
|
4262
|
+
ensureProject6(database, project);
|
|
3654
4263
|
const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, batchId);
|
|
3655
4264
|
if (!batch) throw new ContentBatchError(`Unknown content batch: ${batchId}`, 404);
|
|
3656
4265
|
database.prepare(`
|
|
@@ -3675,6 +4284,30 @@ function findContentPost(database, project, postId) {
|
|
|
3675
4284
|
if (!row) throw new ContentBatchError(`Unknown content post: ${postId}`, 404);
|
|
3676
4285
|
return postFromRow(row, assetsForPost(database, project, postId));
|
|
3677
4286
|
}
|
|
4287
|
+
function requireContentPostClaim(project, post, claimToken, writeKind) {
|
|
4288
|
+
if (!claimToken && !hasActiveContentPostClaim(project, post)) return;
|
|
4289
|
+
const validation = validateAgentClaimForWrite({
|
|
4290
|
+
channel: post.channel,
|
|
4291
|
+
claimToken,
|
|
4292
|
+
confirmWrite: true,
|
|
4293
|
+
dangerLevel: "enforce",
|
|
4294
|
+
project,
|
|
4295
|
+
scopeType: "content_post",
|
|
4296
|
+
targetId: post.id,
|
|
4297
|
+
writeKind
|
|
4298
|
+
});
|
|
4299
|
+
if (!validation.ok) {
|
|
4300
|
+
const status = validation.code === "claim_required" || validation.code === "claim_token_invalid" ? 401 : 409;
|
|
4301
|
+
throw new AgentClaimError(validation.message, status, validation.code, validation.conflicts);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
function hasActiveContentPostClaim(project, post) {
|
|
4305
|
+
return listAgentClaims(project).claims.some((claim) => {
|
|
4306
|
+
if (claim.project !== project || claim.status !== "active" || claim.derived_state === "expired") return false;
|
|
4307
|
+
if (claim.scope_type === "content_post") return claim.target_id === post.id;
|
|
4308
|
+
return claim.scope_type === "project_channel" && (!claim.channel || claim.channel === post.channel);
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
3678
4311
|
function listContentPosts(project, filters = {}) {
|
|
3679
4312
|
const database = lineageDb();
|
|
3680
4313
|
try {
|
|
@@ -3697,6 +4330,7 @@ function updateContentPost(project, fields) {
|
|
|
3697
4330
|
const timestamp = nowIso();
|
|
3698
4331
|
try {
|
|
3699
4332
|
const current = findContentPost(database, project, postId);
|
|
4333
|
+
requireContentPostClaim(project, current, fields.claimToken, fields.phase ? "content_post_phase" : "content_post_update");
|
|
3700
4334
|
if (fields.batchId) {
|
|
3701
4335
|
const batch = database.prepare("select id from content_batches where project_id = ? and id = ?").get(project, fields.batchId);
|
|
3702
4336
|
if (!batch) throw new ContentBatchError(`Unknown content batch: ${fields.batchId}`, 404);
|
|
@@ -3734,7 +4368,8 @@ function attachContentPostAsset(project, fields) {
|
|
|
3734
4368
|
const database = lineageDb();
|
|
3735
4369
|
const timestamp = nowIso();
|
|
3736
4370
|
try {
|
|
3737
|
-
findContentPost(database, project, postId);
|
|
4371
|
+
const current = findContentPost(database, project, postId);
|
|
4372
|
+
requireContentPostClaim(project, current, fields.claimToken, "content_post_attach_asset");
|
|
3738
4373
|
database.prepare(`
|
|
3739
4374
|
insert into content_post_assets (id, project_id, post_id, asset_id, role, notes, attached_at)
|
|
3740
4375
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -3752,6 +4387,8 @@ function detachContentPostAsset(project, fields) {
|
|
|
3752
4387
|
if (!fields.confirmWrite) return { ok: true, dryRun: true, message: `Would detach ${assetId} from ${postId}`, preview: { postId, assetId, role } };
|
|
3753
4388
|
const database = lineageDb();
|
|
3754
4389
|
try {
|
|
4390
|
+
const current = findContentPost(database, project, postId);
|
|
4391
|
+
requireContentPostClaim(project, current, fields.claimToken, "content_post_detach_asset");
|
|
3755
4392
|
database.prepare("delete from content_post_assets where project_id = ? and post_id = ? and asset_id = ? and role = ?").run(project, postId, assetId, role);
|
|
3756
4393
|
return { ok: true, message: `Detached ${assetId} from ${postId}`, post: findContentPost(database, project, postId) };
|
|
3757
4394
|
} finally {
|
|
@@ -3867,7 +4504,8 @@ function setContentTarget(project, fields) {
|
|
|
3867
4504
|
const database = lineageDb();
|
|
3868
4505
|
const timestamp = nowIso();
|
|
3869
4506
|
try {
|
|
3870
|
-
findPost(database, project, postId);
|
|
4507
|
+
const post = findPost(database, project, postId);
|
|
4508
|
+
requireContentPostClaim(project, post, fields.claimToken, "content_target_set");
|
|
3871
4509
|
database.prepare(`
|
|
3872
4510
|
insert into content_targets (project_id, post_id, notes, selected_at, updated_at)
|
|
3873
4511
|
values (?, ?, ?, ?, ?)
|
|
@@ -3880,10 +4518,19 @@ function setContentTarget(project, fields) {
|
|
|
3880
4518
|
}
|
|
3881
4519
|
return { ok: true, message: `Selected content target ${postId}`, ...getContentTarget(project) };
|
|
3882
4520
|
}
|
|
3883
|
-
function clearContentTarget(project, confirmWrite) {
|
|
4521
|
+
function clearContentTarget(project, confirmWrite, claimToken) {
|
|
3884
4522
|
if (!confirmWrite) return { ok: true, dryRun: true, message: "Would clear selected content target" };
|
|
3885
4523
|
const database = lineageDb();
|
|
3886
4524
|
try {
|
|
4525
|
+
const row = database.prepare("select post_id from content_targets where project_id = ?").get(project);
|
|
4526
|
+
if (row?.post_id) {
|
|
4527
|
+
try {
|
|
4528
|
+
const post = findPost(database, project, String(row.post_id));
|
|
4529
|
+
requireContentPostClaim(project, post, claimToken, "content_target_clear");
|
|
4530
|
+
} catch (error) {
|
|
4531
|
+
if (!(error instanceof ContentBatchError && error.status === 404)) throw error;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
3887
4534
|
database.prepare("delete from content_targets where project_id = ?").run(project);
|
|
3888
4535
|
} finally {
|
|
3889
4536
|
database.close();
|
|
@@ -4302,13 +4949,19 @@ function getContentOpsQueue(project) {
|
|
|
4302
4949
|
}
|
|
4303
4950
|
|
|
4304
4951
|
// src/server/contentBatchRoutes.ts
|
|
4305
|
-
function
|
|
4952
|
+
function stringBody2(req, key) {
|
|
4306
4953
|
const value = req.body[key];
|
|
4307
4954
|
return typeof value === "string" ? value : void 0;
|
|
4308
4955
|
}
|
|
4309
|
-
function
|
|
4956
|
+
function boolBody2(req, key) {
|
|
4310
4957
|
return req.body[key] === true;
|
|
4311
4958
|
}
|
|
4959
|
+
function claimTokenFromRequest2(req) {
|
|
4960
|
+
const header = req.header("X-Lineage-Claim-Token");
|
|
4961
|
+
if (header) return header;
|
|
4962
|
+
const value = req.body.claimToken;
|
|
4963
|
+
return typeof value === "string" ? value : void 0;
|
|
4964
|
+
}
|
|
4312
4965
|
function bodyProjectShape(req) {
|
|
4313
4966
|
return { body: req.body, query: req.query };
|
|
4314
4967
|
}
|
|
@@ -4319,22 +4972,22 @@ function contentBatchRouter(projectFrom2) {
|
|
|
4319
4972
|
});
|
|
4320
4973
|
router.post("/batches", (req, res) => {
|
|
4321
4974
|
res.json(createContentBatch(projectFrom2(bodyProjectShape(req)), {
|
|
4322
|
-
batchId:
|
|
4323
|
-
campaign:
|
|
4324
|
-
channel:
|
|
4325
|
-
confirmWrite:
|
|
4326
|
-
notes:
|
|
4327
|
-
title:
|
|
4975
|
+
batchId: stringBody2(req, "batchId") || stringBody2(req, "id") || "",
|
|
4976
|
+
campaign: stringBody2(req, "campaign"),
|
|
4977
|
+
channel: stringBody2(req, "channel"),
|
|
4978
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
4979
|
+
notes: stringBody2(req, "notes"),
|
|
4980
|
+
title: stringBody2(req, "title") || ""
|
|
4328
4981
|
}));
|
|
4329
4982
|
});
|
|
4330
4983
|
router.post("/import/demo", (req, res) => {
|
|
4331
|
-
const kind =
|
|
4984
|
+
const kind = stringBody2(req, "kind");
|
|
4332
4985
|
res.json(importDemoContentBatch(projectFrom2(bodyProjectShape(req)), {
|
|
4333
|
-
batchId:
|
|
4334
|
-
campaign:
|
|
4335
|
-
confirmWrite:
|
|
4986
|
+
batchId: stringBody2(req, "batchId") || "",
|
|
4987
|
+
campaign: stringBody2(req, "campaign"),
|
|
4988
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
4336
4989
|
kind: kind === "concepts" || kind === "drafts" || kind === "all" ? kind : "all",
|
|
4337
|
-
title:
|
|
4990
|
+
title: stringBody2(req, "title")
|
|
4338
4991
|
}));
|
|
4339
4992
|
});
|
|
4340
4993
|
router.get("/target", (req, res) => {
|
|
@@ -4345,13 +4998,14 @@ function contentBatchRouter(projectFrom2) {
|
|
|
4345
4998
|
});
|
|
4346
4999
|
router.post("/target", (req, res) => {
|
|
4347
5000
|
res.json(setContentTarget(projectFrom2(bodyProjectShape(req)), {
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
5001
|
+
claimToken: claimTokenFromRequest2(req),
|
|
5002
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
5003
|
+
notes: stringBody2(req, "notes"),
|
|
5004
|
+
postId: stringBody2(req, "postId") || ""
|
|
4351
5005
|
}));
|
|
4352
5006
|
});
|
|
4353
5007
|
router.post("/target/clear", (req, res) => {
|
|
4354
|
-
res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)),
|
|
5008
|
+
res.json(clearContentTarget(projectFrom2(bodyProjectShape(req)), boolBody2(req, "confirmWrite"), claimTokenFromRequest2(req)));
|
|
4355
5009
|
});
|
|
4356
5010
|
router.get("/batches/:batchId", (req, res) => {
|
|
4357
5011
|
res.json(getContentBatch(projectFrom2(req), req.params.batchId));
|
|
@@ -4365,52 +5019,55 @@ function contentBatchRouter(projectFrom2) {
|
|
|
4365
5019
|
});
|
|
4366
5020
|
router.post("/posts", (req, res) => {
|
|
4367
5021
|
res.json(createContentPost(projectFrom2(bodyProjectShape(req)), {
|
|
4368
|
-
batchId:
|
|
4369
|
-
body:
|
|
4370
|
-
campaign:
|
|
4371
|
-
channel:
|
|
4372
|
-
confirmWrite:
|
|
4373
|
-
cta:
|
|
4374
|
-
notes:
|
|
4375
|
-
phase:
|
|
4376
|
-
postId:
|
|
4377
|
-
sourcePath:
|
|
4378
|
-
title:
|
|
5022
|
+
batchId: stringBody2(req, "batchId") || "",
|
|
5023
|
+
body: stringBody2(req, "body"),
|
|
5024
|
+
campaign: stringBody2(req, "campaign"),
|
|
5025
|
+
channel: stringBody2(req, "channel") || "",
|
|
5026
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
5027
|
+
cta: stringBody2(req, "cta"),
|
|
5028
|
+
notes: stringBody2(req, "notes"),
|
|
5029
|
+
phase: stringBody2(req, "phase"),
|
|
5030
|
+
postId: stringBody2(req, "postId") || stringBody2(req, "id") || "",
|
|
5031
|
+
sourcePath: stringBody2(req, "sourcePath"),
|
|
5032
|
+
title: stringBody2(req, "title") || ""
|
|
4379
5033
|
}));
|
|
4380
5034
|
});
|
|
4381
5035
|
router.post("/posts/:postId", (req, res) => {
|
|
4382
5036
|
res.json(updateContentPost(projectFrom2(bodyProjectShape(req)), {
|
|
4383
|
-
batchId:
|
|
4384
|
-
body:
|
|
4385
|
-
campaign:
|
|
4386
|
-
channel:
|
|
4387
|
-
confirmWrite:
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
5037
|
+
batchId: stringBody2(req, "batchId"),
|
|
5038
|
+
body: stringBody2(req, "body"),
|
|
5039
|
+
campaign: stringBody2(req, "campaign"),
|
|
5040
|
+
channel: stringBody2(req, "channel"),
|
|
5041
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
5042
|
+
claimToken: claimTokenFromRequest2(req),
|
|
5043
|
+
cta: stringBody2(req, "cta"),
|
|
5044
|
+
notes: stringBody2(req, "notes"),
|
|
5045
|
+
phase: stringBody2(req, "phase"),
|
|
5046
|
+
postedAt: stringBody2(req, "postedAt"),
|
|
4392
5047
|
postId: req.params.postId,
|
|
4393
|
-
scheduledAt:
|
|
4394
|
-
sourcePath:
|
|
4395
|
-
title:
|
|
4396
|
-
url:
|
|
5048
|
+
scheduledAt: stringBody2(req, "scheduledAt"),
|
|
5049
|
+
sourcePath: stringBody2(req, "sourcePath"),
|
|
5050
|
+
title: stringBody2(req, "title"),
|
|
5051
|
+
url: stringBody2(req, "url")
|
|
4397
5052
|
}));
|
|
4398
5053
|
});
|
|
4399
5054
|
router.post("/posts/:postId/assets", (req, res) => {
|
|
4400
5055
|
res.json(attachContentPostAsset(projectFrom2(bodyProjectShape(req)), {
|
|
4401
|
-
assetId:
|
|
4402
|
-
confirmWrite:
|
|
4403
|
-
|
|
5056
|
+
assetId: stringBody2(req, "assetId") || "",
|
|
5057
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
5058
|
+
claimToken: claimTokenFromRequest2(req),
|
|
5059
|
+
notes: stringBody2(req, "notes"),
|
|
4404
5060
|
postId: req.params.postId,
|
|
4405
|
-
role:
|
|
5061
|
+
role: stringBody2(req, "role")
|
|
4406
5062
|
}));
|
|
4407
5063
|
});
|
|
4408
5064
|
router.post("/posts/:postId/assets/detach", (req, res) => {
|
|
4409
5065
|
res.json(detachContentPostAsset(projectFrom2(bodyProjectShape(req)), {
|
|
4410
|
-
assetId:
|
|
4411
|
-
confirmWrite:
|
|
5066
|
+
assetId: stringBody2(req, "assetId") || "",
|
|
5067
|
+
confirmWrite: boolBody2(req, "confirmWrite"),
|
|
5068
|
+
claimToken: claimTokenFromRequest2(req),
|
|
4412
5069
|
postId: req.params.postId,
|
|
4413
|
-
role:
|
|
5070
|
+
role: stringBody2(req, "role")
|
|
4414
5071
|
}));
|
|
4415
5072
|
});
|
|
4416
5073
|
return router;
|
|
@@ -4526,7 +5183,7 @@ function listImageGenerationJobs(project = defaultProject, fields = {}) {
|
|
|
4526
5183
|
}
|
|
4527
5184
|
|
|
4528
5185
|
// src/server/assetLineageDemo.ts
|
|
4529
|
-
import { createHash as
|
|
5186
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4530
5187
|
import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4531
5188
|
import { tmpdir } from "node:os";
|
|
4532
5189
|
import { dirname as dirname3, join as join8, posix } from "node:path";
|
|
@@ -4609,7 +5266,7 @@ function swissifierMediaState(manifest) {
|
|
|
4609
5266
|
return { invalid, missing };
|
|
4610
5267
|
}
|
|
4611
5268
|
function sha256Hex(input) {
|
|
4612
|
-
return
|
|
5269
|
+
return createHash3("sha256").update(input).digest("hex");
|
|
4613
5270
|
}
|
|
4614
5271
|
function safeTarEntryName(rawName) {
|
|
4615
5272
|
const stripped = rawName.replace(/\0.*$/, "").replace(/^\.\//, "");
|
|
@@ -4870,7 +5527,7 @@ function svg(label, fill, stroke) {
|
|
|
4870
5527
|
`;
|
|
4871
5528
|
}
|
|
4872
5529
|
function assetIdFor(asset) {
|
|
4873
|
-
return `local-${
|
|
5530
|
+
return `local-${createHash3("sha256").update(svg(asset.label, asset.fill, asset.stroke)).digest("hex").slice(0, 12)}`;
|
|
4874
5531
|
}
|
|
4875
5532
|
function demoAssetIds() {
|
|
4876
5533
|
return Object.fromEntries(demoAssets.map((asset) => [asset.key, assetIdFor(asset)]));
|
|
@@ -4916,7 +5573,7 @@ function upsertDemoAssets(project, ids) {
|
|
|
4916
5573
|
ids[asset.key],
|
|
4917
5574
|
project,
|
|
4918
5575
|
demoRelativePath(project, asset),
|
|
4919
|
-
|
|
5576
|
+
createHash3("sha256").update(body).digest("hex"),
|
|
4920
5577
|
asset.label,
|
|
4921
5578
|
asset.channel,
|
|
4922
5579
|
Buffer.byteLength(body),
|
|
@@ -5222,6 +5879,7 @@ app.post("/api/assets/lookup", asyncRoute((req, res) => {
|
|
|
5222
5879
|
res.json(lookupAssets(projectFrom(req), Array.isArray(req.body.assetIds) ? req.body.assetIds.map(String) : []));
|
|
5223
5880
|
}));
|
|
5224
5881
|
registerAdapterRoutes(app, projectFrom, asyncRoute);
|
|
5882
|
+
registerAgentClaimRoutes(app, projectFrom, asyncRoute);
|
|
5225
5883
|
app.use("/api/content", contentBatchRouter(projectFrom));
|
|
5226
5884
|
app.use("/api/selections", assetSelectionRouter(projectFrom));
|
|
5227
5885
|
app.get("/api/generation/jobs", asyncRoute((req, res) => {
|
|
@@ -5283,13 +5941,14 @@ app.post(
|
|
|
5283
5941
|
linkLineageAssets(projectFrom(req), {
|
|
5284
5942
|
parentAssetId: String(req.body.parentAssetId || ""),
|
|
5285
5943
|
childAssetId: String(req.body.childAssetId || ""),
|
|
5286
|
-
confirmWrite: req.body.confirmWrite === true
|
|
5944
|
+
confirmWrite: req.body.confirmWrite === true,
|
|
5945
|
+
claimToken: claimTokenFromRequest(req)
|
|
5287
5946
|
})
|
|
5288
5947
|
);
|
|
5289
5948
|
})
|
|
5290
5949
|
);
|
|
5291
5950
|
app.post("/api/lineage/remove-node", asyncRoute((req, res) => {
|
|
5292
|
-
res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true }));
|
|
5951
|
+
res.json(removeLineageNode(projectFrom(req), { assetId: String(req.body.assetId || ""), rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0, confirmWrite: req.body.confirmWrite === true, claimToken: claimTokenFromRequest(req) }));
|
|
5293
5952
|
}));
|
|
5294
5953
|
app.post(
|
|
5295
5954
|
"/api/lineage/layout",
|
|
@@ -5303,7 +5962,8 @@ app.post(
|
|
|
5303
5962
|
x: Number(position.x),
|
|
5304
5963
|
y: Number(position.y)
|
|
5305
5964
|
})),
|
|
5306
|
-
confirmWrite: req.body.confirmWrite === true
|
|
5965
|
+
confirmWrite: req.body.confirmWrite === true,
|
|
5966
|
+
claimToken: claimTokenFromRequest(req)
|
|
5307
5967
|
})
|
|
5308
5968
|
);
|
|
5309
5969
|
})
|
|
@@ -5315,7 +5975,8 @@ app.post(
|
|
|
5315
5975
|
linkSelectedLineageChild(projectFrom(req), {
|
|
5316
5976
|
rootAssetId: typeof req.body.rootAssetId === "string" ? req.body.rootAssetId : void 0,
|
|
5317
5977
|
childAssetId: String(req.body.childAssetId || ""),
|
|
5318
|
-
confirmWrite: req.body.confirmWrite === true
|
|
5978
|
+
confirmWrite: req.body.confirmWrite === true,
|
|
5979
|
+
claimToken: claimTokenFromRequest(req)
|
|
5319
5980
|
})
|
|
5320
5981
|
);
|
|
5321
5982
|
})
|
|
@@ -5539,6 +6200,10 @@ app.use((error, _req, res, _next) => {
|
|
|
5539
6200
|
res.status(error.status).json({ error: error.message });
|
|
5540
6201
|
return;
|
|
5541
6202
|
}
|
|
6203
|
+
if (isAgentClaimError(error)) {
|
|
6204
|
+
res.status(error.status).json({ error: error.code, message: error.message, conflicts: error.conflicts });
|
|
6205
|
+
return;
|
|
6206
|
+
}
|
|
5542
6207
|
if (isLineageWorkspaceError(error)) {
|
|
5543
6208
|
res.status(error.status).json({ error: error.message });
|
|
5544
6209
|
return;
|