@huynle/hub 0.7.1 → 0.7.2
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 +70 -0
- package/Dockerfile +3 -0
- package/README.md +148 -11
- package/package.json +3 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/main.ts +4 -4
- package/packages/claude-code/src/wrapper.ts +3 -2
- package/packages/cli/dist/hub-cli.js +19334 -7849
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/main.ts +3 -2
- package/packages/codex/src/spoke.ts +4 -1
- package/packages/codex/src/wrapper.ts +7 -3
- package/packages/node/package.json +1 -1
- package/packages/node/src/harnesses.ts +39 -4
- package/packages/node/src/run.ts +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/client.ts +55 -4
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/sse.ts +4 -1
- package/packages/sdk/src/types.ts +59 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/enroll.ts +26 -4
- package/src/central/agent/app-tokens.ts +27 -17
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/directory-routes.ts +155 -12
- package/src/central/agent/directory.ts +1 -1
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/names.ts +1 -1
- package/src/central/agent/read-routes.ts +32 -36
- package/src/central/agent/routes.ts +14 -11
- package/src/central/agent/watch.ts +65 -174
- package/src/central/agent/webhook-routes.ts +10 -2
- package/src/central/agent/webhooks.ts +49 -5
- package/src/central/auth.ts +127 -1
- package/src/central/backend/index.ts +3 -0
- package/src/central/backend/types.ts +2 -0
- package/src/central/http.ts +115 -32
- package/src/central/mcp-http.ts +116 -0
- package/src/central/store.ts +53 -0
- package/src/config.ts +70 -7
- package/src/index.ts +90 -91
- package/src/spoke/launchers.ts +109 -3
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +10 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/assets/index-D9zMbxSg.css +0 -1
- package/web/dist/assets/index-db1z4duA.js +0 -20
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// The hubctl release tool (`bun scripts/hubctl-release.ts <command>`), used by .github/workflows/release.yml and by
|
|
2
|
+
// maintainers. It is not part of the hub/hubctl command surface.
|
|
3
|
+
//
|
|
4
|
+
// build [TARGET…] [--outdir dist] [--trust-root FILE] [--require-trust-root]
|
|
5
|
+
// Compiles packages/cli/src/main.ts into dist/hubctl-<target> for the supported targets (all by default),
|
|
6
|
+
// embedding the trust root (public keys only). --require-trust-root refuses a build without one.
|
|
7
|
+
// manifest --minimum-version V --out FILE [--dist dist] [--pins FILE] [--release V] [--issued-at MS] [--targets T,…]
|
|
8
|
+
// Writes the unsigned manifest body: every dist/hubctl-<target> with its size and sha256, and the reviewed
|
|
9
|
+
// harness pins after re-downloading and digesting every pinned file.
|
|
10
|
+
// sign --in FILE --out FILE [--trust-root FILE] [--key-id ID] [--targets T,…]
|
|
11
|
+
// Signs the body with the key in $HUB_RELEASE_SIGNING_KEY (never printed) and verifies the result against
|
|
12
|
+
// the trust root the binaries embed. Writes nothing unless it verifies.
|
|
13
|
+
// verify --manifest FILE [--trust-root FILE] (--dist DIR [--targets T,…] | --target T --file BIN)
|
|
14
|
+
// [--installed V --installed-sha256 D] [--floor V] [--allow-downgrade]
|
|
15
|
+
// Runs the installer's checks (verify.ts) on one or every artifact.
|
|
16
|
+
//
|
|
17
|
+
// --targets (comma-separated) makes a partial release: exactly the listed targets, each still required and checked
|
|
18
|
+
// in full. Without it every supported target is required. sign needs the same list to sign a partial body, so a
|
|
19
|
+
// partial release is never signed by accident.
|
|
20
|
+
// trust-root [--key-id ID]
|
|
21
|
+
// Prints the v1 trust root (public key only) for the key in $HUB_RELEASE_SIGNING_KEY, for the maintainer
|
|
22
|
+
// who sets up the release key (README.md next to this file).
|
|
23
|
+
//
|
|
24
|
+
// Exit codes: 0 ok, 1 other, 2 usage, 11 artifact rejected (contracts §3.11).
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
26
|
+
import path from "node:path"
|
|
27
|
+
import pkg from "../../../package.json" with { type: "json" }
|
|
28
|
+
import {
|
|
29
|
+
BOOTSTRAP_EXIT,
|
|
30
|
+
SUPPORTED_TARGETS,
|
|
31
|
+
canonicalJSON,
|
|
32
|
+
checkTarget,
|
|
33
|
+
exitFor,
|
|
34
|
+
isRecord,
|
|
35
|
+
supportedTarget,
|
|
36
|
+
type Issue,
|
|
37
|
+
type Result,
|
|
38
|
+
type TrustRoot,
|
|
39
|
+
} from "../contracts/index.ts"
|
|
40
|
+
import { buildManifestBody, manifestJSON, parseSigningKey, signVerified, trustRootFor } from "./manifest.ts"
|
|
41
|
+
import { PIN_LIST_FILE, validatePinList, verifyPinDownloads, type Downloader } from "./pins.ts"
|
|
42
|
+
import { shippedTrustRoot, trustRootDefine } from "./trust-root.ts"
|
|
43
|
+
import { digestBytes, parseManifest, verifyHubctlRelease } from "./verify.ts"
|
|
44
|
+
|
|
45
|
+
/** The environment variable holding the release signing key (a GitHub Actions secret; decision Q5). */
|
|
46
|
+
export const SIGNING_KEY_ENV = "HUB_RELEASE_SIGNING_KEY"
|
|
47
|
+
/** The trust root the release binaries embed, relative to the repository root (checked in once the key exists). */
|
|
48
|
+
export const TRUST_ROOT_FILE = "packages/cli/src/bootstrap/release/trust-root.json"
|
|
49
|
+
/** The program compiled into hubctl (I3). */
|
|
50
|
+
export const HUBCTL_ENTRY = "packages/cli/src/main.ts"
|
|
51
|
+
export const DEFAULT_KEY_ID = "hub-release-1"
|
|
52
|
+
/** The version hubctl --version prints, and the release the manifest names. */
|
|
53
|
+
export const HUBCTL_VERSION: string = pkg.version
|
|
54
|
+
|
|
55
|
+
export interface ReleaseIo {
|
|
56
|
+
readonly env: Readonly<Record<string, string | undefined>>
|
|
57
|
+
/** The repository root; relative paths resolve against it. */
|
|
58
|
+
readonly cwd: string
|
|
59
|
+
readonly stdout: (s: string) => void
|
|
60
|
+
readonly stderr: (s: string) => void
|
|
61
|
+
/** Runs a word list (no shell) in `cwd`; resolves to its exit code. */
|
|
62
|
+
readonly run: (argv: string[]) => Promise<number>
|
|
63
|
+
readonly download: Downloader
|
|
64
|
+
readonly now: () => number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const EXIT_USAGE = BOOTSTRAP_EXIT.usage
|
|
68
|
+
const EXIT_OTHER = BOOTSTRAP_EXIT.other
|
|
69
|
+
|
|
70
|
+
class Usage extends Error {}
|
|
71
|
+
class Failure extends Error {
|
|
72
|
+
constructor(
|
|
73
|
+
message: string,
|
|
74
|
+
readonly code: number,
|
|
75
|
+
) {
|
|
76
|
+
super(message)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface Parsed {
|
|
81
|
+
readonly values: Map<string, string>
|
|
82
|
+
readonly flags: Set<string>
|
|
83
|
+
readonly positional: string[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseArgs(argv: readonly string[], valued: readonly string[], booleans: readonly string[], positional = false): Parsed {
|
|
87
|
+
const values = new Map<string, string>()
|
|
88
|
+
const flags = new Set<string>()
|
|
89
|
+
const rest: string[] = []
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const word = argv[i]!
|
|
92
|
+
if (valued.includes(word)) {
|
|
93
|
+
const value = argv[++i]
|
|
94
|
+
if (value === undefined || value.startsWith("--")) throw new Usage(`${word} needs a value`)
|
|
95
|
+
values.set(word, value)
|
|
96
|
+
} else if (booleans.includes(word)) flags.add(word)
|
|
97
|
+
else if (word.startsWith("-") || !positional) throw new Usage(`unknown argument ${word}`)
|
|
98
|
+
else rest.push(word)
|
|
99
|
+
}
|
|
100
|
+
return { values, flags, positional: rest }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const required = (p: Parsed, flag: string): string => {
|
|
104
|
+
const value = p.values.get(flag)
|
|
105
|
+
if (value === undefined) throw new Usage(`${flag} is required`)
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const describe = (issues: readonly Issue[]) => issues.map((i) => ` ${i.code} ${i.path || "/"}: ${i.message}`).join("\n")
|
|
110
|
+
|
|
111
|
+
/** Throws a Failure carrying the exit code of the first issue. */
|
|
112
|
+
function unwrap<T>(r: Result<T>, what: string): T {
|
|
113
|
+
if (r.ok) return r.value
|
|
114
|
+
throw new Failure(`${what}:\n${describe(r.issues)}`, exitFor(r.issues[0]?.code ?? "internal"))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readJSON(file: string, what: string): unknown {
|
|
118
|
+
let text: string
|
|
119
|
+
try {
|
|
120
|
+
text = readFileSync(file, "utf8")
|
|
121
|
+
} catch {
|
|
122
|
+
throw new Failure(`cannot read ${what} ${file}`, EXIT_OTHER)
|
|
123
|
+
}
|
|
124
|
+
const r = parseManifest(text)
|
|
125
|
+
if (!r.ok) throw new Failure(`${what} ${file} is not JSON`, EXIT_OTHER)
|
|
126
|
+
return r.value
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadTrustRoot(file: string): TrustRoot {
|
|
130
|
+
const value = readJSON(file, "trust root")
|
|
131
|
+
const r = shippedTrustRoot(JSON.stringify(value))
|
|
132
|
+
if (!r.ok) throw new Failure(`trust root ${file}:\n${describe(r.issues)}`, EXIT_OTHER)
|
|
133
|
+
return r.value
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function signingKey(io: ReleaseIo) {
|
|
137
|
+
const secret = io.env[SIGNING_KEY_ENV]
|
|
138
|
+
if (!secret) throw new Usage(`${SIGNING_KEY_ENV} is not set (the release signing key; a GitHub Actions secret)`)
|
|
139
|
+
try {
|
|
140
|
+
return parseSigningKey(secret)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Failure((error as Error).message, EXIT_OTHER)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The --targets list: supported targets, each named once; undefined when not given (every supported target). */
|
|
147
|
+
function targetsArg(p: Parsed): string[] | undefined {
|
|
148
|
+
const text = p.values.get("--targets")
|
|
149
|
+
if (text === undefined) return undefined
|
|
150
|
+
const list = text.split(",").map((t) => t.trim())
|
|
151
|
+
if (list.some((t) => t === "")) throw new Usage("--targets takes a comma-separated list of targets (e.g. linux-x64,darwin-arm64)")
|
|
152
|
+
for (const target of list) {
|
|
153
|
+
const r = checkTarget(target)
|
|
154
|
+
if (!r.ok) throw new Usage(r.issues[0]!.message)
|
|
155
|
+
}
|
|
156
|
+
if (new Set(list).size !== list.length) throw new Usage("--targets names a target twice")
|
|
157
|
+
return SUPPORTED_TARGETS.map((t) => t.target).filter((t) => list.includes(t))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The `bun build --compile` words for one target's hubctl. */
|
|
161
|
+
export function hubctlBuildArgv(target: string, options: { readonly bun: string; readonly outdir: string; readonly root?: TrustRoot }): string[] {
|
|
162
|
+
const t = supportedTarget(target)
|
|
163
|
+
if (!t) throw new Error(`${target} is not a supported target`)
|
|
164
|
+
return [
|
|
165
|
+
options.bun,
|
|
166
|
+
"build",
|
|
167
|
+
"--compile",
|
|
168
|
+
"--minify",
|
|
169
|
+
"--sourcemap",
|
|
170
|
+
`--target=${t.bunTarget}`,
|
|
171
|
+
...(options.root ? trustRootDefine(options.root) : []),
|
|
172
|
+
HUBCTL_ENTRY,
|
|
173
|
+
"--outfile",
|
|
174
|
+
path.join(options.outdir, `hubctl-${t.target}`),
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function build(argv: readonly string[], io: ReleaseIo): Promise<number> {
|
|
179
|
+
const p = parseArgs(argv, ["--outdir", "--trust-root", "--bun"], ["--require-trust-root"], true)
|
|
180
|
+
const targets = p.positional.length > 0 ? p.positional : SUPPORTED_TARGETS.map((t) => t.target)
|
|
181
|
+
for (const target of targets) {
|
|
182
|
+
const r = checkTarget(target)
|
|
183
|
+
if (!r.ok) throw new Usage(r.issues[0]!.message)
|
|
184
|
+
}
|
|
185
|
+
const rootFile = path.resolve(io.cwd, p.values.get("--trust-root") ?? TRUST_ROOT_FILE)
|
|
186
|
+
let root: TrustRoot | undefined
|
|
187
|
+
if (existsSync(rootFile)) root = loadTrustRoot(rootFile)
|
|
188
|
+
else if (p.flags.has("--require-trust-root")) throw new Failure(`no trust root at ${rootFile}: a release build must embed one (see README.md next to ${TRUST_ROOT_FILE})`, EXIT_OTHER)
|
|
189
|
+
else io.stderr(`warning: no trust root at ${rootFile}; this dev build cannot verify releases\n`)
|
|
190
|
+
const outdir = path.resolve(io.cwd, p.values.get("--outdir") ?? "dist")
|
|
191
|
+
mkdirSync(outdir, { recursive: true })
|
|
192
|
+
const bun = p.values.get("--bun") ?? process.execPath
|
|
193
|
+
for (const target of targets) {
|
|
194
|
+
const words = hubctlBuildArgv(target, { bun, outdir, ...(root ? { root } : {}) })
|
|
195
|
+
io.stderr(`==> ${words.at(-1)}\n`)
|
|
196
|
+
const code = await io.run(words)
|
|
197
|
+
if (code !== 0) throw new Failure(`bun build failed for ${target} (exit ${code})`, EXIT_OTHER)
|
|
198
|
+
}
|
|
199
|
+
return 0
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function manifest(argv: readonly string[], io: ReleaseIo): Promise<number> {
|
|
203
|
+
const p = parseArgs(argv, ["--dist", "--pins", "--minimum-version", "--release", "--issued-at", "--out", "--targets"], [])
|
|
204
|
+
const listed = targetsArg(p)
|
|
205
|
+
const targets = listed ?? SUPPORTED_TARGETS.map((t) => t.target)
|
|
206
|
+
const minimumVersion = required(p, "--minimum-version")
|
|
207
|
+
const out = path.resolve(io.cwd, required(p, "--out"))
|
|
208
|
+
const dist = path.resolve(io.cwd, p.values.get("--dist") ?? "dist")
|
|
209
|
+
const issuedText = p.values.get("--issued-at")
|
|
210
|
+
const issuedAt = issuedText === undefined ? io.now() : Number(issuedText)
|
|
211
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 0) throw new Usage("--issued-at takes milliseconds since the epoch")
|
|
212
|
+
const pins = unwrap(validatePinList(readJSON(path.resolve(io.cwd, p.values.get("--pins") ?? PIN_LIST_FILE), "pin list")), "the pin list is invalid")
|
|
213
|
+
const artifacts = targets.map((target) => {
|
|
214
|
+
const file = `hubctl-${target}`
|
|
215
|
+
let bytes: Uint8Array
|
|
216
|
+
try {
|
|
217
|
+
bytes = readFileSync(path.join(dist, file))
|
|
218
|
+
} catch {
|
|
219
|
+
throw new Failure(`missing ${path.join(dist, file)}: build ${listed ? "every listed target" : "every supported target (or name a subset with --targets)"} first`, EXIT_OTHER)
|
|
220
|
+
}
|
|
221
|
+
return { target, file, ...digestBytes(bytes) }
|
|
222
|
+
})
|
|
223
|
+
io.stderr(`re-downloading ${pins.pins.length} pinned harness file(s)\n`)
|
|
224
|
+
const harnesses = unwrap(await verifyPinDownloads(pins, io.download), "a pinned harness file does not match its pin")
|
|
225
|
+
const body = unwrap(
|
|
226
|
+
buildManifestBody({ release: p.values.get("--release") ?? HUBCTL_VERSION, minimumVersion, issuedAt, artifacts, harnesses, ...(listed ? { targets: listed } : {}) }),
|
|
227
|
+
"the manifest is invalid",
|
|
228
|
+
)
|
|
229
|
+
writeFileSync(out, `${canonicalJSON(body)}\n`)
|
|
230
|
+
io.stdout(`${out}: hubctl ${body.release} (${body.channel}), ${body.artifacts.length} artifacts (${body.artifacts.map((a) => a.target).join(", ")}), ${body.harnesses.length} harness pins\n`)
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function sign(argv: readonly string[], io: ReleaseIo): Promise<number> {
|
|
235
|
+
const p = parseArgs(argv, ["--in", "--out", "--trust-root", "--key-id", "--targets"], [])
|
|
236
|
+
const listed = targetsArg(p)
|
|
237
|
+
const input = path.resolve(io.cwd, required(p, "--in"))
|
|
238
|
+
const out = path.resolve(io.cwd, required(p, "--out"))
|
|
239
|
+
const root = loadTrustRoot(path.resolve(io.cwd, p.values.get("--trust-root") ?? TRUST_ROOT_FILE))
|
|
240
|
+
const body = readJSON(input, "manifest body")
|
|
241
|
+
if (!isRecord(body) || "signatures" in body) throw new Failure(`${input} is not an unsigned manifest body (it has signatures, or is not an object)`, EXIT_OTHER)
|
|
242
|
+
const rebuilt = unwrap(
|
|
243
|
+
buildManifestBody({
|
|
244
|
+
release: body.release as string,
|
|
245
|
+
minimumVersion: body.minimumVersion as string,
|
|
246
|
+
issuedAt: body.issuedAt as number,
|
|
247
|
+
artifacts: body.artifacts as never,
|
|
248
|
+
harnesses: body.harnesses as never,
|
|
249
|
+
...(listed ? { targets: listed } : {}),
|
|
250
|
+
}),
|
|
251
|
+
"the manifest body is invalid (a partial release needs --targets naming its targets)",
|
|
252
|
+
)
|
|
253
|
+
if (canonicalJSON(rebuilt) !== canonicalJSON(body)) throw new Failure(`${input} is not a manifest body as \`manifest\` writes it`, EXIT_OTHER)
|
|
254
|
+
const privateKey = signingKey(io)
|
|
255
|
+
const active = root.keys.filter((k) => k.status === "active")
|
|
256
|
+
const keyID = p.values.get("--key-id") ?? (active.length === 1 ? active[0]!.keyID : DEFAULT_KEY_ID)
|
|
257
|
+
const verified = unwrap(signVerified(rebuilt, { keyID, privateKey }, root), "the signed manifest does not verify against the trust root (is the secret the key the binaries embed?)")
|
|
258
|
+
writeFileSync(out, manifestJSON(verified))
|
|
259
|
+
io.stdout(`${out}: signed by ${keyID}, verified against trust root v${root.version}\n`)
|
|
260
|
+
return 0
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function verify(argv: readonly string[], io: ReleaseIo): Promise<number> {
|
|
264
|
+
const p = parseArgs(argv, ["--manifest", "--trust-root", "--dist", "--target", "--file", "--installed", "--installed-sha256", "--floor", "--targets"], ["--allow-downgrade"])
|
|
265
|
+
const listed = targetsArg(p)
|
|
266
|
+
const root = loadTrustRoot(path.resolve(io.cwd, p.values.get("--trust-root") ?? TRUST_ROOT_FILE))
|
|
267
|
+
const document = readJSON(path.resolve(io.cwd, required(p, "--manifest")), "manifest")
|
|
268
|
+
const dist = p.values.get("--dist")
|
|
269
|
+
if (listed && !dist) throw new Usage("--targets goes with --dist")
|
|
270
|
+
const jobs: { target: string; file: string }[] = dist
|
|
271
|
+
? (listed ?? SUPPORTED_TARGETS.map((t) => t.target)).map((target) => ({ target, file: path.resolve(io.cwd, dist, `hubctl-${target}`) }))
|
|
272
|
+
: [{ target: required(p, "--target"), file: path.resolve(io.cwd, required(p, "--file")) }]
|
|
273
|
+
const installedVersion = p.values.get("--installed")
|
|
274
|
+
const installedSha = p.values.get("--installed-sha256")
|
|
275
|
+
if ((installedVersion === undefined) !== (installedSha === undefined)) throw new Usage("--installed and --installed-sha256 go together")
|
|
276
|
+
const floor = p.values.get("--floor")
|
|
277
|
+
for (const job of jobs) {
|
|
278
|
+
let bytes: Uint8Array
|
|
279
|
+
try {
|
|
280
|
+
bytes = readFileSync(job.file)
|
|
281
|
+
} catch {
|
|
282
|
+
throw new Failure(`cannot read ${job.file}`, EXIT_OTHER)
|
|
283
|
+
}
|
|
284
|
+
const accepted = unwrap(
|
|
285
|
+
verifyHubctlRelease({
|
|
286
|
+
manifest: document,
|
|
287
|
+
root,
|
|
288
|
+
target: job.target,
|
|
289
|
+
bytes: digestBytes(bytes),
|
|
290
|
+
...(installedVersion !== undefined && installedSha !== undefined ? { installed: { version: installedVersion, sha256: installedSha } } : {}),
|
|
291
|
+
...(floor !== undefined ? { floor } : {}),
|
|
292
|
+
allowDowngrade: p.flags.has("--allow-downgrade"),
|
|
293
|
+
}),
|
|
294
|
+
`${job.target}: rejected`,
|
|
295
|
+
)
|
|
296
|
+
io.stdout(`ok ${job.target} hubctl ${accepted.manifest.release} ${accepted.kind} ${accepted.artifact.sha256}\n`)
|
|
297
|
+
}
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function trustRoot(argv: readonly string[], io: ReleaseIo): Promise<number> {
|
|
302
|
+
const p = parseArgs(argv, ["--key-id"], [])
|
|
303
|
+
io.stdout(`${JSON.stringify(trustRootFor(signingKey(io), p.values.get("--key-id") ?? DEFAULT_KEY_ID), null, 2)}\n`)
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const COMMANDS: Record<string, (argv: readonly string[], io: ReleaseIo) => Promise<number>> = { build, manifest, sign, verify, "trust-root": trustRoot }
|
|
308
|
+
|
|
309
|
+
const USAGE = "usage: hubctl-release <build|manifest|sign|verify|trust-root> [options] (see packages/cli/src/bootstrap/release/cli.ts)\n"
|
|
310
|
+
|
|
311
|
+
export async function runRelease(argv: readonly string[], io: ReleaseIo): Promise<number> {
|
|
312
|
+
const [name, ...rest] = argv
|
|
313
|
+
const command = name !== undefined && Object.hasOwn(COMMANDS, name) ? COMMANDS[name] : undefined
|
|
314
|
+
if (!command) {
|
|
315
|
+
io.stderr(USAGE)
|
|
316
|
+
return EXIT_USAGE
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
return await command(rest, io)
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error instanceof Usage) {
|
|
322
|
+
io.stderr(`${error.message}\n${USAGE}`)
|
|
323
|
+
return EXIT_USAGE
|
|
324
|
+
}
|
|
325
|
+
if (error instanceof Failure) {
|
|
326
|
+
io.stderr(`${error.message}\n`)
|
|
327
|
+
return error.code
|
|
328
|
+
}
|
|
329
|
+
io.stderr(`internal error: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
330
|
+
return EXIT_OTHER
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "hub.harness-pin-list",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"pins": [
|
|
5
|
+
{
|
|
6
|
+
"pin": {
|
|
7
|
+
"harness": "opencode",
|
|
8
|
+
"version": "2.2.0",
|
|
9
|
+
"target": "darwin-arm64",
|
|
10
|
+
"url": "https://github.com/anomalyco/opencode/releases/download/v2.2.0/opencode-darwin-arm64.zip",
|
|
11
|
+
"file": "opencode-darwin-arm64.zip",
|
|
12
|
+
"size": 102507526,
|
|
13
|
+
"sha256": "sha256:c71b1cd96541539ad38e2a75377d2f43d2a27567289a14371a182e9a0800db3d",
|
|
14
|
+
"evidence": [],
|
|
15
|
+
"minimumVersion": "2.0.0"
|
|
16
|
+
},
|
|
17
|
+
"review": {
|
|
18
|
+
"by": "huynle",
|
|
19
|
+
"on": "2026-10-30",
|
|
20
|
+
"checked": [
|
|
21
|
+
"upstream-release",
|
|
22
|
+
"digest-redownloaded",
|
|
23
|
+
"evidence-listed"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"pin": {
|
|
29
|
+
"harness": "opencode",
|
|
30
|
+
"version": "2.2.0",
|
|
31
|
+
"target": "darwin-x64",
|
|
32
|
+
"url": "https://github.com/anomalyco/opencode/releases/download/v2.2.0/opencode-darwin-x64-baseline.zip",
|
|
33
|
+
"file": "opencode-darwin-x64-baseline.zip",
|
|
34
|
+
"size": 107678258,
|
|
35
|
+
"sha256": "sha256:bda15562402475e891d39de5938de7ce052a245f9fd926ab72d32f55b1e26ac3",
|
|
36
|
+
"evidence": [],
|
|
37
|
+
"minimumVersion": "2.0.0"
|
|
38
|
+
},
|
|
39
|
+
"review": {
|
|
40
|
+
"by": "huynle",
|
|
41
|
+
"on": "2026-10-30",
|
|
42
|
+
"checked": [
|
|
43
|
+
"upstream-release",
|
|
44
|
+
"digest-redownloaded",
|
|
45
|
+
"evidence-listed"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"pin": {
|
|
51
|
+
"harness": "opencode",
|
|
52
|
+
"version": "2.2.0",
|
|
53
|
+
"target": "linux-x64",
|
|
54
|
+
"url": "https://github.com/anomalyco/opencode/releases/download/v2.2.0/opencode-linux-x64-baseline.tar.gz",
|
|
55
|
+
"file": "opencode-linux-x64-baseline.tar.gz",
|
|
56
|
+
"size": 115841183,
|
|
57
|
+
"sha256": "sha256:6d597970a8cc448f1b9a9f645532584b339dd3ab9583bb5f063f13d83e54768f",
|
|
58
|
+
"evidence": [],
|
|
59
|
+
"minimumVersion": "2.0.0"
|
|
60
|
+
},
|
|
61
|
+
"review": {
|
|
62
|
+
"by": "huynle",
|
|
63
|
+
"on": "2026-10-30",
|
|
64
|
+
"checked": [
|
|
65
|
+
"upstream-release",
|
|
66
|
+
"digest-redownloaded",
|
|
67
|
+
"evidence-listed"
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"pin": {
|
|
73
|
+
"harness": "opencode",
|
|
74
|
+
"version": "2.2.0",
|
|
75
|
+
"target": "linux-arm64",
|
|
76
|
+
"url": "https://github.com/anomalyco/opencode/releases/download/v2.2.0/opencode-linux-arm64.tar.gz",
|
|
77
|
+
"file": "opencode-linux-arm64.tar.gz",
|
|
78
|
+
"size": 114522206,
|
|
79
|
+
"sha256": "sha256:1f1ce0fc07b9245a815ffa9d652dbb80e6b0f07e5bf74ea87d2b1c0339fbac52",
|
|
80
|
+
"evidence": [],
|
|
81
|
+
"minimumVersion": "2.0.0"
|
|
82
|
+
},
|
|
83
|
+
"review": {
|
|
84
|
+
"by": "huynle",
|
|
85
|
+
"on": "2026-10-30",
|
|
86
|
+
"checked": [
|
|
87
|
+
"upstream-release",
|
|
88
|
+
"digest-redownloaded",
|
|
89
|
+
"evidence-listed"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Building and signing the hubctl release manifest (contracts §3.6, I5, decision Q5).
|
|
2
|
+
// - The body lists one hubctl artifact for every supported target (in matrix order) and the reviewed harness
|
|
3
|
+
// pins (pins.ts), so the same inputs always give the same bytes.
|
|
4
|
+
// - The signature is a detached ed25519 signature over manifestPayload (canonical JSON without `signatures`),
|
|
5
|
+
// by the single release key (threshold 1), held as a GitHub Actions secret.
|
|
6
|
+
// - signVerified signs and then verifies with the contract's verifyManifest against the trust root the hubctl
|
|
7
|
+
// binaries embed: a secret that does not match the shipped root never produces a publishable manifest.
|
|
8
|
+
// Only node:crypto and the contracts are used, so the signing job runs with no npm dependencies installed.
|
|
9
|
+
import { createPrivateKey, createPublicKey, sign, type KeyObject } from "node:crypto"
|
|
10
|
+
import {
|
|
11
|
+
Checker,
|
|
12
|
+
SEMVER,
|
|
13
|
+
SUPPORTED_TARGETS,
|
|
14
|
+
TRUST_ROOT_SCHEMA,
|
|
15
|
+
canonicalJSON,
|
|
16
|
+
manifestPayload,
|
|
17
|
+
validateArtifactManifest,
|
|
18
|
+
verifyManifest,
|
|
19
|
+
type ArtifactManifest,
|
|
20
|
+
type HarnessPin,
|
|
21
|
+
type Result,
|
|
22
|
+
type TrustRoot,
|
|
23
|
+
type VerifiedManifest,
|
|
24
|
+
} from "../contracts/index.ts"
|
|
25
|
+
|
|
26
|
+
export type ManifestBody = Omit<ArtifactManifest, "signatures">
|
|
27
|
+
|
|
28
|
+
export interface ManifestInput {
|
|
29
|
+
readonly release: string
|
|
30
|
+
/** The security floor (≤ release). */
|
|
31
|
+
readonly minimumVersion: string
|
|
32
|
+
readonly issuedAt: number
|
|
33
|
+
readonly artifacts: readonly { readonly target: string; readonly file: string; readonly size: number; readonly sha256: string }[]
|
|
34
|
+
/** The manifest's harness pins (verifyPinDownloads / harnessPins). */
|
|
35
|
+
readonly harnesses: readonly HarnessPin[]
|
|
36
|
+
/**
|
|
37
|
+
* The targets this release ships, listed explicitly (a partial release, e.g. one target for testing): each must
|
|
38
|
+
* be supported and have exactly one artifact, and no artifact may be for another target. Default: every
|
|
39
|
+
* supported target.
|
|
40
|
+
*/
|
|
41
|
+
readonly targets?: readonly string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SigningKey {
|
|
45
|
+
readonly keyID: string
|
|
46
|
+
readonly privateKey: KeyObject
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A pre-release (x.y.z-…) goes to `next`, everything else to `stable`. */
|
|
50
|
+
export const channelFor = (release: string): "stable" | "next" => (SEMVER.exec(release)?.[4] ? "next" : "stable")
|
|
51
|
+
|
|
52
|
+
const ORDER = SUPPORTED_TARGETS.map((t) => t.target)
|
|
53
|
+
const rank = (target: string) => {
|
|
54
|
+
const i = ORDER.indexOf(target)
|
|
55
|
+
return i < 0 ? ORDER.length : i
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The unsigned manifest for a release: validated, every supported target (or every listed target) present and
|
|
60
|
+
* nothing else, artifacts in matrix order.
|
|
61
|
+
*/
|
|
62
|
+
export function buildManifestBody(input: ManifestInput): Result<ManifestBody> {
|
|
63
|
+
const body: ManifestBody = {
|
|
64
|
+
schema: "hub.artifact-manifest",
|
|
65
|
+
version: 1,
|
|
66
|
+
product: "hubctl",
|
|
67
|
+
release: input.release,
|
|
68
|
+
channel: channelFor(input.release),
|
|
69
|
+
minimumVersion: input.minimumVersion,
|
|
70
|
+
issuedAt: input.issuedAt,
|
|
71
|
+
artifacts: [...input.artifacts].map(({ target, file, size, sha256 }) => ({ target, file, size, sha256 })).sort((a, b) => rank(a.target) - rank(b.target)),
|
|
72
|
+
harnesses: [...input.harnesses],
|
|
73
|
+
}
|
|
74
|
+
const c = new Checker()
|
|
75
|
+
c.merge("", validateArtifactManifest({ ...body, signatures: [] }))
|
|
76
|
+
const targets = input.targets ?? ORDER
|
|
77
|
+
if (input.targets) {
|
|
78
|
+
if (targets.length === 0) c.add("/artifacts", "invalid_value", "a release lists at least one target")
|
|
79
|
+
if (new Set(targets).size !== targets.length) c.add("/artifacts", "duplicate", "a target is listed twice")
|
|
80
|
+
for (const target of targets) if (!ORDER.includes(target)) c.add("/artifacts", "unsupported_platform", `${target} is not a supported target`)
|
|
81
|
+
for (const a of body.artifacts) if (ORDER.includes(a.target) && !targets.includes(a.target)) c.add("/artifacts", "inconsistent", `the release has a hubctl for ${a.target}, which is not one of the listed targets (${targets.join(", ")})`)
|
|
82
|
+
}
|
|
83
|
+
for (const target of targets) if (!body.artifacts.some((a) => a.target === target)) c.add("/artifacts", "missing_field", `the release has no hubctl for ${target}`)
|
|
84
|
+
return c.result(body)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The release key from the secret's text: PKCS#8 PEM or base64 PKCS#8 DER, ed25519 only. Errors never quote it. */
|
|
88
|
+
export function parseSigningKey(secret: string): KeyObject {
|
|
89
|
+
const text = secret.trim()
|
|
90
|
+
let key: KeyObject
|
|
91
|
+
try {
|
|
92
|
+
key = text.startsWith("-----BEGIN") ? createPrivateKey({ key: text, format: "pem" }) : createPrivateKey({ key: Buffer.from(text, "base64"), format: "der", type: "pkcs8" })
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error("the release signing key is not a PKCS#8 ed25519 private key (PEM or base64 DER)")
|
|
95
|
+
}
|
|
96
|
+
if (key.asymmetricKeyType !== "ed25519") throw new Error(`the release signing key must be ed25519, not ${key.asymmetricKeyType ?? "unknown"}`)
|
|
97
|
+
return key
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Adds the detached ed25519 signature over manifestPayload. */
|
|
101
|
+
export function signManifest(body: ManifestBody, key: SigningKey): ArtifactManifest {
|
|
102
|
+
const value = sign(null, Buffer.from(manifestPayload(body), "utf8"), key.privateKey).toString("base64")
|
|
103
|
+
return { ...body, signatures: [{ keyID: key.keyID, algorithm: "ed25519", value }] }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Signs, then verifies against the shipped trust root; a key the root does not list never yields a manifest. */
|
|
107
|
+
export function signVerified(body: ManifestBody, key: SigningKey, root: TrustRoot): Result<VerifiedManifest> {
|
|
108
|
+
return verifyManifest(signManifest(body, key), root)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The v1 trust root (one active key, threshold 1) for the public half of a release key. */
|
|
112
|
+
export function trustRootFor(privateKey: KeyObject, keyID: string): TrustRoot {
|
|
113
|
+
const publicKey = createPublicKey(privateKey).export({ format: "der", type: "spki" }).toString("base64")
|
|
114
|
+
return { schema: TRUST_ROOT_SCHEMA, version: 1, threshold: 1, keys: [{ keyID, algorithm: "ed25519", publicKey, status: "active" }] }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The bytes published as hubctl-manifest.json. */
|
|
118
|
+
export const manifestJSON = (manifest: ArtifactManifest): string => `${canonicalJSON(manifest)}\n`
|