@trigguard/cli 0.1.1
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/README.md +70 -0
- package/data/execution-surfaces.json +28 -0
- package/dist/auth.js +20 -0
- package/dist/commands/authorize.d.ts +1 -0
- package/dist/commands/authorize.js +99 -0
- package/dist/commands/chaos.d.ts +1 -0
- package/dist/commands/chaos.js +35 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/log.d.ts +3 -0
- package/dist/commands/log.js +119 -0
- package/dist/commands/logMonitor.d.ts +1 -0
- package/dist/commands/logMonitor.js +65 -0
- package/dist/commands/login-web.d.ts +1 -0
- package/dist/commands/login-web.js +80 -0
- package/dist/commands/policy-distribution.d.ts +10 -0
- package/dist/commands/policy-distribution.js +108 -0
- package/dist/commands/policy-runtime.d.ts +4 -0
- package/dist/commands/policy-runtime.js +61 -0
- package/dist/commands/policy.d.ts +1 -0
- package/dist/commands/policy.js +123 -0
- package/dist/commands/policyLifecycle.d.ts +13 -0
- package/dist/commands/policyLifecycle.js +601 -0
- package/dist/commands/receiptFetch.d.ts +1 -0
- package/dist/commands/receiptFetch.js +44 -0
- package/dist/commands/receiptProof.d.ts +1 -0
- package/dist/commands/receiptProof.js +43 -0
- package/dist/commands/replay.d.ts +2 -0
- package/dist/commands/replay.js +130 -0
- package/dist/commands/session.d.ts +6 -0
- package/dist/commands/session.js +280 -0
- package/dist/commands/simulate.d.ts +5 -0
- package/dist/commands/simulate.js +89 -0
- package/dist/commands/tg-authorize.d.ts +12 -0
- package/dist/commands/tg-authorize.js +191 -0
- package/dist/commands/tg-init.d.ts +1 -0
- package/dist/commands/tg-init.js +149 -0
- package/dist/commands/tg-setup.d.ts +1 -0
- package/dist/commands/tg-setup.js +43 -0
- package/dist/commands/tg-surfaces.d.ts +7 -0
- package/dist/commands/tg-surfaces.js +50 -0
- package/dist/commands/tg-verify.d.ts +1 -0
- package/dist/commands/tg-verify.js +118 -0
- package/dist/commands/transparency.d.ts +2 -0
- package/dist/commands/transparency.js +65 -0
- package/dist/commands/verify.d.ts +1 -0
- package/dist/commands/verify.js +127 -0
- package/dist/commands/verifyBundle.d.ts +1 -0
- package/dist/commands/verifyBundle.js +109 -0
- package/dist/commands/verifyReceiptCmd.d.ts +1 -0
- package/dist/commands/verifyReceiptCmd.js +49 -0
- package/dist/commands/witness.d.ts +1 -0
- package/dist/commands/witness.js +22 -0
- package/dist/cp/cliDeviceAuth.d.ts +24 -0
- package/dist/cp/cliDeviceAuth.js +68 -0
- package/dist/cp/client.d.ts +19 -0
- package/dist/cp/client.js +73 -0
- package/dist/cp/config.d.ts +8 -0
- package/dist/cp/config.js +113 -0
- package/dist/cp/credentials.d.ts +9 -0
- package/dist/cp/credentials.js +31 -0
- package/dist/cp/provisionCliKey.d.ts +12 -0
- package/dist/cp/provisionCliKey.js +43 -0
- package/dist/cp/types.d.ts +37 -0
- package/dist/cp/types.js +1 -0
- package/dist/stdin.js +9 -0
- package/dist/tg/args.d.ts +3 -0
- package/dist/tg/args.js +28 -0
- package/dist/tg/authorize-format.d.ts +21 -0
- package/dist/tg/authorize-format.js +87 -0
- package/dist/tg/errors.d.ts +6 -0
- package/dist/tg/errors.js +53 -0
- package/dist/tg/gateway.d.ts +2 -0
- package/dist/tg/gateway.js +19 -0
- package/dist/tg/help.d.ts +7 -0
- package/dist/tg/help.js +164 -0
- package/dist/tg/receipt.d.ts +1 -0
- package/dist/tg/receipt.js +13 -0
- package/dist/tg/shellQuote.d.ts +1 -0
- package/dist/tg/shellQuote.js +6 -0
- package/dist/tg.js +92 -0
- package/package.json +50 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { stdin as input } from "node:process";
|
|
3
|
+
import { createExecutionClient, fetchPublicKeys, verifyReceiptSignature, } from "@trigguard/execution-sdk";
|
|
4
|
+
import { resolveBearer } from "../auth.js";
|
|
5
|
+
import { loadConfig } from "../cp/config.js";
|
|
6
|
+
import { missingConfiguredApiKeyMessage, resolveApiKey as resolveStoredApiKey, } from "../cp/credentials.js";
|
|
7
|
+
import { readStdin } from "../stdin.js";
|
|
8
|
+
import { flagValue, hasFlag } from "../tg/args.js";
|
|
9
|
+
import { defaultGatewayUrl } from "../tg/gateway.js";
|
|
10
|
+
import { buildAuthorizeDisplay, printHumanAuthorize, printJsonAuthorize, } from "../tg/authorize-format.js";
|
|
11
|
+
import { formatFetchError, formatGatewayError, missingSurfaceMessage, } from "../tg/errors.js";
|
|
12
|
+
function parseRisk(raw) {
|
|
13
|
+
if (raw === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof raw === "number" && !Number.isNaN(raw)) {
|
|
16
|
+
return raw <= 1 ? raw : raw / 100;
|
|
17
|
+
}
|
|
18
|
+
const s = String(raw).trim().toLowerCase();
|
|
19
|
+
if (!s)
|
|
20
|
+
return undefined;
|
|
21
|
+
const n = Number(s);
|
|
22
|
+
if (!Number.isNaN(n))
|
|
23
|
+
return n <= 1 ? n : n / 100;
|
|
24
|
+
if (s === "critical")
|
|
25
|
+
return 0.95;
|
|
26
|
+
if (s === "high")
|
|
27
|
+
return 0.85;
|
|
28
|
+
if (s === "medium")
|
|
29
|
+
return 0.5;
|
|
30
|
+
if (s === "low")
|
|
31
|
+
return 0.15;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export { defaultGatewayUrl } from "../tg/gateway.js";
|
|
35
|
+
export function resolveApiKey() {
|
|
36
|
+
return resolveStoredApiKey(loadConfig());
|
|
37
|
+
}
|
|
38
|
+
async function readPayloadSource(args) {
|
|
39
|
+
const file = flagValue(args, "--file");
|
|
40
|
+
if (file) {
|
|
41
|
+
const raw = file === "-" ? await readStdin() : await readFile(file, "utf8");
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
if (!input.isTTY && !flagValue(args, "--surface")) {
|
|
45
|
+
const raw = await readStdin();
|
|
46
|
+
if (raw.trim())
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
async function mergeAuthorizeInput(base, args) {
|
|
52
|
+
const fromPayload = base ?? {};
|
|
53
|
+
const surface = flagValue(args, "--surface") ?? String(fromPayload.surface ?? "");
|
|
54
|
+
const actorId = flagValue(args, "--actor") ??
|
|
55
|
+
flagValue(args, "--actor-id") ??
|
|
56
|
+
String(fromPayload.actorId ?? process.env.TRIGGUARD_ACTOR_ID ?? "tg-cli");
|
|
57
|
+
const intent = flagValue(args, "--intent") ?? fromPayload.intent;
|
|
58
|
+
const risk = flagValue(args, "--risk") ?? fromPayload.risk;
|
|
59
|
+
const environment = flagValue(args, "--environment") ??
|
|
60
|
+
fromPayload.environment ??
|
|
61
|
+
loadConfig().environment;
|
|
62
|
+
let context = fromPayload.context && typeof fromPayload.context === "object"
|
|
63
|
+
? { ...fromPayload.context }
|
|
64
|
+
: {};
|
|
65
|
+
const payloadFlag = flagValue(args, "--payload");
|
|
66
|
+
if (payloadFlag) {
|
|
67
|
+
const inline = payloadFlag.startsWith("{") || payloadFlag.startsWith("[")
|
|
68
|
+
? payloadFlag
|
|
69
|
+
: await readFile(payloadFlag, "utf8");
|
|
70
|
+
Object.assign(context, JSON.parse(inline));
|
|
71
|
+
}
|
|
72
|
+
if (intent)
|
|
73
|
+
context.intent = intent;
|
|
74
|
+
const riskScore = parseRisk(risk);
|
|
75
|
+
if (riskScore !== undefined)
|
|
76
|
+
context.riskScore = riskScore;
|
|
77
|
+
if (environment)
|
|
78
|
+
context.environment = environment;
|
|
79
|
+
if (!surface) {
|
|
80
|
+
throw new Error("missing_surface");
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
surface,
|
|
84
|
+
actorId,
|
|
85
|
+
intent,
|
|
86
|
+
risk,
|
|
87
|
+
environment,
|
|
88
|
+
context: Object.keys(context).length ? context : undefined,
|
|
89
|
+
subjectDigest: typeof fromPayload.subjectDigest === "string" ? fromPayload.subjectDigest : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function runTgAuthorize(args) {
|
|
93
|
+
const json = hasFlag(args, "--json");
|
|
94
|
+
let inputReq;
|
|
95
|
+
try {
|
|
96
|
+
const payload = await readPayloadSource(args);
|
|
97
|
+
inputReq = await mergeAuthorizeInput(payload, args);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
if (e instanceof Error && e.message === "missing_surface") {
|
|
101
|
+
console.error(missingSurfaceMessage());
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
const gatewayUrl = flagValue(args, "--gateway-url") ?? defaultGatewayUrl(inputReq.environment);
|
|
107
|
+
const apiKey = resolveApiKey();
|
|
108
|
+
if (!apiKey && process.env.TRIGGUARD_USE_GCLOUD !== "1") {
|
|
109
|
+
console.error(missingConfiguredApiKeyMessage());
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const getBearerToken = () => resolveBearer(gatewayUrl);
|
|
113
|
+
const client = createExecutionClient({
|
|
114
|
+
gatewayUrl,
|
|
115
|
+
apiKey,
|
|
116
|
+
getBearerToken: apiKey ? undefined : getBearerToken,
|
|
117
|
+
});
|
|
118
|
+
let result;
|
|
119
|
+
try {
|
|
120
|
+
result = await client.authorize({
|
|
121
|
+
surface: inputReq.surface,
|
|
122
|
+
actorId: inputReq.actorId,
|
|
123
|
+
context: inputReq.context,
|
|
124
|
+
subjectDigest: inputReq.subjectDigest,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
const msg = formatFetchError(e, gatewayUrl);
|
|
129
|
+
if (json)
|
|
130
|
+
printJsonAuthorize({ ok: false, error: msg });
|
|
131
|
+
else
|
|
132
|
+
console.error(msg);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (!result.ok || !result.receipt) {
|
|
136
|
+
const errBody = result.body && typeof result.body === "object" ? result.body : { error: result.status };
|
|
137
|
+
const msg = formatGatewayError(result.status, errBody, { apiKeyProvided: Boolean(apiKey) });
|
|
138
|
+
if (json) {
|
|
139
|
+
printJsonAuthorize({ ok: false, status: result.status, error: errBody, message: msg });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.error(msg);
|
|
143
|
+
}
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const body = (result.body && typeof result.body === "object" ? result.body : {});
|
|
147
|
+
const receipt = result.receipt;
|
|
148
|
+
let receiptValid = false;
|
|
149
|
+
try {
|
|
150
|
+
const keys = await fetchPublicKeys(gatewayUrl, apiKey ? () => Promise.resolve(apiKey) : getBearerToken);
|
|
151
|
+
receiptValid = verifyReceiptSignature(receipt, keys);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const msg = formatFetchError(e, gatewayUrl);
|
|
155
|
+
if (json)
|
|
156
|
+
printJsonAuthorize({ ok: false, error: msg, partial: body });
|
|
157
|
+
else
|
|
158
|
+
console.error(msg);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const display = buildAuthorizeDisplay(body, receiptValid);
|
|
162
|
+
if (json) {
|
|
163
|
+
printJsonAuthorize({
|
|
164
|
+
ok: true,
|
|
165
|
+
decision: display.decision,
|
|
166
|
+
reason: display.reason,
|
|
167
|
+
riskScore: display.riskScore,
|
|
168
|
+
executionId: display.executionId,
|
|
169
|
+
receiptValid,
|
|
170
|
+
receiptStatus: display.receiptStatus,
|
|
171
|
+
signatureStatus: display.signatureStatus,
|
|
172
|
+
authority: display.authority,
|
|
173
|
+
protocol: display.protocol,
|
|
174
|
+
verificationResult: display.verificationResult,
|
|
175
|
+
surface: inputReq.surface,
|
|
176
|
+
actorId: inputReq.actorId,
|
|
177
|
+
intent: inputReq.intent ?? null,
|
|
178
|
+
policyId: display.policyId,
|
|
179
|
+
policyBundleHash: display.policyBundleHash,
|
|
180
|
+
latencyMs: display.latencyMs,
|
|
181
|
+
receipt,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
printHumanAuthorize(display);
|
|
186
|
+
}
|
|
187
|
+
if (!receiptValid)
|
|
188
|
+
process.exit(1);
|
|
189
|
+
if (display.decision !== "PERMIT")
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runTgInit(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { flagValue, hasFlag } from "../tg/args.js";
|
|
4
|
+
function detectStack(cwd) {
|
|
5
|
+
const hints = new Set();
|
|
6
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
7
|
+
let packageName = null;
|
|
8
|
+
let hasPackageJson = false;
|
|
9
|
+
if (fs.existsSync(pkgPath)) {
|
|
10
|
+
hasPackageJson = true;
|
|
11
|
+
try {
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
13
|
+
packageName = pkg.name ?? null;
|
|
14
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
15
|
+
if (deps.express)
|
|
16
|
+
hints.add("express");
|
|
17
|
+
if (deps.fastify)
|
|
18
|
+
hints.add("fastify");
|
|
19
|
+
if (deps.langchain || deps["@langchain/core"])
|
|
20
|
+
hints.add("langchain");
|
|
21
|
+
if (!hints.size)
|
|
22
|
+
hints.add("node");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
hints.add("generic");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const workflowsDir = path.join(cwd, ".github", "workflows");
|
|
29
|
+
if (fs.existsSync(workflowsDir)) {
|
|
30
|
+
hints.add("github-actions");
|
|
31
|
+
}
|
|
32
|
+
if (!hints.size)
|
|
33
|
+
hints.add("generic");
|
|
34
|
+
return { hints: [...hints], hasPackageJson, packageName };
|
|
35
|
+
}
|
|
36
|
+
function writeIfAbsent(filePath, content, force) {
|
|
37
|
+
if (fs.existsSync(filePath) && !force)
|
|
38
|
+
return false;
|
|
39
|
+
fs.writeFileSync(filePath, content, { mode: 0o644 });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
export async function runTgInit(args) {
|
|
43
|
+
const json = hasFlag(args, "--json");
|
|
44
|
+
const force = hasFlag(args, "--force");
|
|
45
|
+
const dir = flagValue(args, "--dir") ?? "trigguard";
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
const outDir = path.join(cwd, dir);
|
|
48
|
+
const stack = detectStack(cwd);
|
|
49
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
50
|
+
const envExample = `# TrigGuard — copy to .env and fill in values (never commit .env)
|
|
51
|
+
TRIGGUARD_API_KEY=tg_live_REPLACE_ME
|
|
52
|
+
TRIGGUARD_GATEWAY_URL=https://api.trigguardai.com
|
|
53
|
+
TRIGGUARD_ACTOR_ID=${stack.packageName ?? "my-service"}
|
|
54
|
+
`;
|
|
55
|
+
const exampleJson = {
|
|
56
|
+
surface: "deploy.release",
|
|
57
|
+
actorId: stack.packageName ?? "my-service",
|
|
58
|
+
intent: "example governed action",
|
|
59
|
+
risk: "medium",
|
|
60
|
+
context: {
|
|
61
|
+
repository: "org/repo",
|
|
62
|
+
branch: "main",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const integrationMd = `# TrigGuard Integration
|
|
66
|
+
|
|
67
|
+
Generated by \`tg init\`. Non-destructive scaffold — review before use.
|
|
68
|
+
|
|
69
|
+
## Detected stack
|
|
70
|
+
|
|
71
|
+
${stack.hints.map((h) => `- ${h}`).join("\n")}
|
|
72
|
+
|
|
73
|
+
## 1. Configure
|
|
74
|
+
|
|
75
|
+
\`\`\`bash
|
|
76
|
+
cp ${dir}/.env.example .env
|
|
77
|
+
# Edit .env — set TRIGGUARD_API_KEY from console (Settings → API keys)
|
|
78
|
+
\`\`\`
|
|
79
|
+
|
|
80
|
+
## 2. First authorization
|
|
81
|
+
|
|
82
|
+
\`\`\`bash
|
|
83
|
+
export $(grep -v '^#' .env | xargs)
|
|
84
|
+
tg authorize --surface deploy.release --actor ${stack.packageName ?? "my-service"} --intent "test deploy"
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
## 3. Verify receipt
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
tg verify --execution-id <execution-id-from-output>
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
## 4. Integrate
|
|
94
|
+
|
|
95
|
+
See \`examples/integrations/cli-adoption/\` in the TrigGuard repo for copy-paste templates:
|
|
96
|
+
- GitHub Actions deployment gate
|
|
97
|
+
- Node.js / Express / Fastify
|
|
98
|
+
- LangChain agent pre-flight
|
|
99
|
+
- CI/CD patterns
|
|
100
|
+
|
|
101
|
+
Docs: \`docs/developer/TRIGGUARD_QUICKSTART.md\`, \`docs/developer/DESIGN_PARTNER_DEMO.md\`
|
|
102
|
+
`;
|
|
103
|
+
const written = [];
|
|
104
|
+
const skipped = [];
|
|
105
|
+
function track(relPath, filePath, content) {
|
|
106
|
+
if (writeIfAbsent(filePath, content, force))
|
|
107
|
+
written.push(relPath);
|
|
108
|
+
else
|
|
109
|
+
skipped.push(relPath);
|
|
110
|
+
}
|
|
111
|
+
track(path.join(dir, ".env.example"), path.join(outDir, ".env.example"), envExample);
|
|
112
|
+
track(path.join(dir, "trigguard.example.json"), path.join(outDir, "trigguard.example.json"), `${JSON.stringify(exampleJson, null, 2)}\n`);
|
|
113
|
+
track(path.join(dir, "TRIGGUARD_INTEGRATION.md"), path.join(outDir, "TRIGGUARD_INTEGRATION.md"), integrationMd);
|
|
114
|
+
const exampleSnippets = {
|
|
115
|
+
node: `# Run governed authorize from Node (shell out to tg)
|
|
116
|
+
# tg authorize --file trigguard/trigguard.example.json --json
|
|
117
|
+
`,
|
|
118
|
+
express: `# Express: authorize before route handler
|
|
119
|
+
# See examples/integrations/cli-adoption/express-middleware.mjs
|
|
120
|
+
`,
|
|
121
|
+
fastify: `# Fastify: preHandler hook
|
|
122
|
+
# See examples/integrations/cli-adoption/fastify-hook.mjs
|
|
123
|
+
`,
|
|
124
|
+
langchain: `# LangChain: tool pre-flight
|
|
125
|
+
# See examples/integrations/cli-adoption/langchain-preflight.mjs
|
|
126
|
+
`,
|
|
127
|
+
"github-actions": `# GitHub Actions: see examples/integrations/cli-adoption/github-actions/trigguard-authorize.yml
|
|
128
|
+
`,
|
|
129
|
+
generic: `# tg authorize --surface deploy.release --actor my-agent --intent "action"
|
|
130
|
+
`,
|
|
131
|
+
};
|
|
132
|
+
for (const hint of stack.hints) {
|
|
133
|
+
const rel = path.join(dir, `example-${hint}.md`);
|
|
134
|
+
track(rel, path.join(outDir, `example-${hint}.md`), exampleSnippets[hint] ?? exampleSnippets.generic);
|
|
135
|
+
}
|
|
136
|
+
if (json) {
|
|
137
|
+
console.log(JSON.stringify({ ok: true, directory: dir, detected: stack.hints, written, skipped }, null, 2));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log("TrigGuard integration scaffold created.");
|
|
141
|
+
console.log(` Directory: ${dir}/`);
|
|
142
|
+
console.log(` Detected: ${stack.hints.join(", ")}`);
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log("Next steps:");
|
|
145
|
+
console.log(` 1. cp ${dir}/.env.example .env`);
|
|
146
|
+
console.log(" 2. Set TRIGGUARD_API_KEY in .env");
|
|
147
|
+
console.log(` 3. tg authorize --file ${dir}/trigguard.example.json`);
|
|
148
|
+
console.log(` 4. Read ${dir}/TRIGGUARD_INTEGRATION.md`);
|
|
149
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runTgSetup(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { hasFlag } from "../tg/args.js";
|
|
2
|
+
import { loadConfig } from "../cp/config.js";
|
|
3
|
+
import { resolveApiKey, resolveApiKeySource } from "../cp/credentials.js";
|
|
4
|
+
import { resolveSessionSnapshot } from "./session.js";
|
|
5
|
+
import { runLoginWeb } from "./login-web.js";
|
|
6
|
+
import { loadExecutionSurfaces } from "./tg-surfaces.js";
|
|
7
|
+
import { shellQuoteArg } from "../tg/shellQuote.js";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
export async function runTgSetup(args) {
|
|
10
|
+
const json = hasFlag(args, "--json");
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
const snap = await resolveSessionSnapshot();
|
|
13
|
+
const hasKey = Boolean(resolveApiKey(config));
|
|
14
|
+
if (!snap.authenticated || !hasKey) {
|
|
15
|
+
if (!json)
|
|
16
|
+
console.log("Step 1/3 — Sign in (browser)");
|
|
17
|
+
await runLoginWeb(json ? ["--json"] : []);
|
|
18
|
+
}
|
|
19
|
+
const refreshed = loadConfig();
|
|
20
|
+
const snap2 = await resolveSessionSnapshot();
|
|
21
|
+
if (!snap2.authenticated || !resolveApiKey(refreshed)) {
|
|
22
|
+
throw new Error("Setup incomplete: login did not configure credentials.");
|
|
23
|
+
}
|
|
24
|
+
if (!json)
|
|
25
|
+
console.log("\nStep 2/3 — Execution surfaces");
|
|
26
|
+
const surfaces = loadExecutionSurfaces();
|
|
27
|
+
const sample = surfaces.find((s) => s.surface === "deploy.release") ?? surfaces[0];
|
|
28
|
+
if (json) {
|
|
29
|
+
console.log(JSON.stringify({
|
|
30
|
+
ok: true,
|
|
31
|
+
workspace: snap2.activeOrg?.orgId ?? refreshed.activeOrgId ?? null,
|
|
32
|
+
apiKeySource: resolveApiKeySource(refreshed),
|
|
33
|
+
recommendedSurface: sample?.surface ?? null,
|
|
34
|
+
surfaceCount: surfaces.length,
|
|
35
|
+
}, null, 2));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(` ${surfaces.length} surfaces registered. Try: tg surfaces list`);
|
|
39
|
+
const actor = shellQuoteArg(snap2.activeOrg?.name ?? os.userInfo().username ?? "demo");
|
|
40
|
+
console.log("\nStep 3/3 — First authorization");
|
|
41
|
+
console.log(` tg authorize --surface ${sample?.surface ?? "deploy.release"} --actor ${actor} --intent "setup test"`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ExecutionSurfaceEntry {
|
|
2
|
+
readonly surface: string;
|
|
3
|
+
readonly category: string;
|
|
4
|
+
readonly description: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadExecutionSurfaces(): ExecutionSurfaceEntry[];
|
|
7
|
+
export declare function runTgSurfaces(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { hasFlag } from "../tg/args.js";
|
|
5
|
+
const SURFACE_META = {
|
|
6
|
+
"deploy.release": { category: "delivery", description: "Release deployment to an environment" },
|
|
7
|
+
"infra.apply": { category: "infrastructure", description: "Apply infrastructure changes" },
|
|
8
|
+
"artifact.publish": { category: "delivery", description: "Publish build artifacts" },
|
|
9
|
+
"data.export": { category: "data", description: "Export sensitive data" },
|
|
10
|
+
"payment.execute": { category: "financial", description: "Execute payment or refund" },
|
|
11
|
+
"secrets.access": { category: "security", description: "Read or use secrets" },
|
|
12
|
+
"database.migrate": { category: "data", description: "Run database migrations" },
|
|
13
|
+
"production.write": { category: "infrastructure", description: "Write to production systems" },
|
|
14
|
+
};
|
|
15
|
+
function surfacesRegistryPath() {
|
|
16
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const candidates = [
|
|
18
|
+
path.join(here, "../../data/execution-surfaces.json"),
|
|
19
|
+
path.join(here, "../../../../protocol/execution-surfaces.json"),
|
|
20
|
+
];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (fs.existsSync(candidate))
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
throw new Error("execution-surfaces.json not found");
|
|
26
|
+
}
|
|
27
|
+
export function loadExecutionSurfaces() {
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(surfacesRegistryPath(), "utf8"));
|
|
29
|
+
const surfaces = Array.isArray(raw.surfaces) ? raw.surfaces : [];
|
|
30
|
+
return surfaces.map((surface) => {
|
|
31
|
+
const meta = SURFACE_META[surface];
|
|
32
|
+
return {
|
|
33
|
+
surface,
|
|
34
|
+
category: meta?.category ?? "general",
|
|
35
|
+
description: meta?.description ?? "Registered execution surface",
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function runTgSurfaces(args) {
|
|
40
|
+
const json = hasFlag(args, "--json");
|
|
41
|
+
const list = loadExecutionSurfaces();
|
|
42
|
+
if (json) {
|
|
43
|
+
console.log(JSON.stringify({ ok: true, surfaces: list }, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.log("Execution surfaces (read-only registry)");
|
|
47
|
+
for (const entry of list) {
|
|
48
|
+
console.log(` ${entry.surface.padEnd(28)} ${entry.category.padEnd(14)} ${entry.description}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runTgVerify(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fetchPublicKeys, verifyReceiptSignature } from "@trigguard/execution-sdk";
|
|
3
|
+
import { resolveBearer } from "../auth.js";
|
|
4
|
+
import { flagValue, hasFlag, stripFlags } from "../tg/args.js";
|
|
5
|
+
import { extractReceiptFromPayload } from "../tg/receipt.js";
|
|
6
|
+
import { resolveGatewayUrl } from "../tg/gateway.js";
|
|
7
|
+
import { resolveApiKey } from "./tg-authorize.js";
|
|
8
|
+
import { formatFetchError } from "../tg/errors.js";
|
|
9
|
+
import { printJsonAuthorize } from "../tg/authorize-format.js";
|
|
10
|
+
async function fetchReceiptByExecutionId(gatewayUrl, executionId, token) {
|
|
11
|
+
const headers = { Accept: "application/json" };
|
|
12
|
+
if (token)
|
|
13
|
+
headers.Authorization = `Bearer ${token}`;
|
|
14
|
+
const res = await fetch(`${gatewayUrl.replace(/\/$/, "")}/verify/${encodeURIComponent(executionId)}`, {
|
|
15
|
+
headers,
|
|
16
|
+
signal: AbortSignal.timeout(30_000),
|
|
17
|
+
});
|
|
18
|
+
const text = await res.text();
|
|
19
|
+
let body = {};
|
|
20
|
+
try {
|
|
21
|
+
body = text ? JSON.parse(text) : {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error(`Gateway returned non-JSON for execution ${executionId}`);
|
|
25
|
+
}
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const err = body && typeof body === "object" ? body.error : res.status;
|
|
28
|
+
throw new Error(`Cannot fetch receipt: ${String(err)} (HTTP ${res.status})`);
|
|
29
|
+
}
|
|
30
|
+
const obj = body;
|
|
31
|
+
const receipt = obj.receipt;
|
|
32
|
+
if (!receipt || typeof receipt !== "object") {
|
|
33
|
+
throw new Error(`No receipt in gateway response for ${executionId}`);
|
|
34
|
+
}
|
|
35
|
+
return receipt;
|
|
36
|
+
}
|
|
37
|
+
export async function runTgVerify(args) {
|
|
38
|
+
const json = hasFlag(args, "--json");
|
|
39
|
+
const executionId = flagValue(args, "--execution-id");
|
|
40
|
+
const gatewayUrl = resolveGatewayUrl(args);
|
|
41
|
+
const apiKey = resolveApiKey();
|
|
42
|
+
let receipt;
|
|
43
|
+
if (executionId) {
|
|
44
|
+
try {
|
|
45
|
+
receipt = await fetchReceiptByExecutionId(gatewayUrl, executionId, apiKey);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
const msg = formatFetchError(e, gatewayUrl);
|
|
49
|
+
if (json)
|
|
50
|
+
printJsonAuthorize({ ok: false, error: msg });
|
|
51
|
+
else
|
|
52
|
+
console.error(msg);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const file = stripFlags(args)[0];
|
|
58
|
+
if (!file) {
|
|
59
|
+
console.error("Usage: tg verify receipt.json | tg verify --execution-id EXEC_ID");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(await readFile(file, "utf8"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
console.error(`Cannot read receipt file: ${file}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
receipt = extractReceiptFromPayload(parsed);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
75
|
+
if (json)
|
|
76
|
+
printJsonAuthorize({ ok: false, error: msg });
|
|
77
|
+
else
|
|
78
|
+
console.error(msg);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let valid = false;
|
|
83
|
+
try {
|
|
84
|
+
const keys = await fetchPublicKeys(gatewayUrl, apiKey
|
|
85
|
+
? () => Promise.resolve(apiKey)
|
|
86
|
+
: process.env.TRIGGUARD_USE_GCLOUD === "1"
|
|
87
|
+
? () => resolveBearer(gatewayUrl)
|
|
88
|
+
: undefined);
|
|
89
|
+
valid = verifyReceiptSignature(receipt, keys);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
const msg = formatFetchError(e, gatewayUrl);
|
|
93
|
+
if (json)
|
|
94
|
+
printJsonAuthorize({ ok: false, error: msg });
|
|
95
|
+
else
|
|
96
|
+
console.error(msg);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const execId = typeof receipt.executionId === "string" ? receipt.executionId : executionId ?? null;
|
|
100
|
+
const payload = {
|
|
101
|
+
ok: valid,
|
|
102
|
+
verificationResult: valid ? "passed" : "failed",
|
|
103
|
+
executionId: execId,
|
|
104
|
+
decision: receipt.decision ?? null,
|
|
105
|
+
signatureStatus: valid ? "valid (Ed25519)" : "invalid",
|
|
106
|
+
};
|
|
107
|
+
if (json) {
|
|
108
|
+
printJsonAuthorize(payload);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`Verification: ${valid ? "passed" : "failed"}`);
|
|
112
|
+
console.log(`Execution ID: ${execId ?? "—"}`);
|
|
113
|
+
console.log(`Decision: ${String(receipt.decision ?? "—")}`);
|
|
114
|
+
console.log(`Signature Status: ${valid ? "valid (Ed25519)" : "invalid"}`);
|
|
115
|
+
}
|
|
116
|
+
if (!valid)
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computeMerkleRoot, computeReplayArtifactHash, loadReplayArtifact, loadTransparencyLog, verifyReplayReceipt, } from "@trigguard/policy-engine";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
function argValue(args, name) {
|
|
4
|
+
const i = args.indexOf(name);
|
|
5
|
+
if (i < 0)
|
|
6
|
+
return undefined;
|
|
7
|
+
return args[i + 1];
|
|
8
|
+
}
|
|
9
|
+
function hasFlag(args, name) {
|
|
10
|
+
return args.includes(name);
|
|
11
|
+
}
|
|
12
|
+
export async function runTransparencyRoot(argv) {
|
|
13
|
+
const json = hasFlag(argv, "--json");
|
|
14
|
+
const entries = loadTransparencyLog();
|
|
15
|
+
const root_hash = computeMerkleRoot(entries);
|
|
16
|
+
const out = { root_hash, entries: entries.length };
|
|
17
|
+
if (json)
|
|
18
|
+
console.log(JSON.stringify(out, null, 2));
|
|
19
|
+
else {
|
|
20
|
+
console.log(`Transparency root: ${root_hash}`);
|
|
21
|
+
console.log(`Entries: ${entries.length}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function runTransparencyVerify(argv) {
|
|
25
|
+
const receiptPath = argv[0];
|
|
26
|
+
if (!receiptPath) {
|
|
27
|
+
console.error("Usage: trigguard transparency-verify <replay-receipt.json> [--artifact artifact.json] [--public-key pem]");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const artifactPath = argValue(argv, "--artifact");
|
|
32
|
+
const publicKeyPem = argValue(argv, "--public-key");
|
|
33
|
+
const replayReceipt = JSON.parse(readFileSync(receiptPath, "utf8"));
|
|
34
|
+
const artifact = artifactPath
|
|
35
|
+
? JSON.parse(readFileSync(artifactPath, "utf8"))
|
|
36
|
+
: loadReplayArtifact(replayReceipt.receipt_id);
|
|
37
|
+
if (!artifact) {
|
|
38
|
+
console.error("Replay artifact not found; pass --artifact <path>");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const vr = verifyReplayReceipt({
|
|
43
|
+
replayReceipt,
|
|
44
|
+
artifact,
|
|
45
|
+
publicKeyPem,
|
|
46
|
+
});
|
|
47
|
+
if (!vr.ok) {
|
|
48
|
+
console.error(`INVALID ${vr.failed_check}: ${vr.detail}`);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const artifactHash = computeReplayArtifactHash(artifact);
|
|
53
|
+
const entries = loadTransparencyLog();
|
|
54
|
+
const index = entries.findIndex((x) => x.artifact_hash === artifactHash);
|
|
55
|
+
const root = computeMerkleRoot(entries);
|
|
56
|
+
if (index < 0) {
|
|
57
|
+
console.error("INVALID inclusion: artifact hash not found in transparency log");
|
|
58
|
+
process.exit(2);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log("OK transparency verification");
|
|
62
|
+
console.log(`Receipt: ${replayReceipt.receipt_id}`);
|
|
63
|
+
console.log(`Artifact index: ${index}`);
|
|
64
|
+
console.log(`Root hash: ${root}`);
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runVerify(argv: string[]): Promise<void>;
|