@umbra-privacy/qos-ceremony 0.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +15 -15
  2. package/dist-cli/cli.js +134 -69
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -11,7 +11,7 @@ at it.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npm i -g @umbra-privacy/qos-ceremony # provides the `ceremony` command
14
+ npm i -g @umbra-privacy/qos-ceremony # provides the `qos-ceremony` command
15
15
  ```
16
16
 
17
17
  ## Use
@@ -21,25 +21,25 @@ export CEREMONY_API=https://<coordinator>/ \
21
21
  CEREMONY_NAMESPACE=<service> CEREMONY_NONCE=<nonce> \
22
22
  CEREMONY_ALIAS=p1 CEREMONY_SECRET=/path/to/p1.secret
23
23
 
24
- ceremony keygen # once, offline — generate your key; give the pubkey to an admin
25
- ceremony share # shareSet: derive + upload your (encrypted) genesis share
26
- ceremony approve --yes # manifestSet: review + sign the manifest (verify the pivot hash!)
27
- ceremony provision # shareSet: re-encrypt your share to the enclave's ephemeral key
28
- ceremony status # read-only phase + blockers
24
+ qos-ceremony keygen # once, offline — generate your key; give the pubkey to an admin
25
+ qos-ceremony share # shareSet: derive + upload your (encrypted) genesis share
26
+ qos-ceremony approve --yes # manifestSet: review + sign the manifest (verify the pivot hash!)
27
+ qos-ceremony provision # shareSet: re-encrypt your share to the enclave's ephemeral key
28
+ qos-ceremony status # read-only phase + blockers
29
29
  ```
30
30
 
31
- `shift-traffic` (ops, needs AWS creds): `ceremony shift-traffic --service arith --stack <s> --weights '{"<old>":0,"<new>":100}'`.
31
+ `shift-traffic` (ops, needs AWS creds): `qos-ceremony shift-traffic --service arith --stack <s> --weights '{"<old>":0,"<new>":100}'`.
32
32
 
33
33
  ## Commands
34
34
 
35
- | Command | Set | What |
36
- |---|---|---|
37
- | `keygen` | any | generate seed (local) + print pubkey to register |
38
- | `share` | shareSet | derive quorum-key share from genesis; upload (ciphertext) |
39
- | `approve` | manifestSet | review + sign the manifest |
40
- | `provision` | shareSet | re-encrypt share to the enclave ephemeral key; post it |
41
- | `status` | — | read-only phase + blockers |
42
- | `shift-traffic` | ops | blue-green weighted ALB cutover across a service's colors |
35
+ | Command | Set | What |
36
+ | --------------- | ----------- | --------------------------------------------------------- |
37
+ | `keygen` | any | generate seed (local) + print pubkey to register |
38
+ | `share` | shareSet | derive quorum-key share from genesis; upload (ciphertext) |
39
+ | `approve` | manifestSet | review + sign the manifest |
40
+ | `provision` | shareSet | re-encrypt share to the enclave ephemeral key; post it |
41
+ | `status` | — | read-only phase + blockers |
42
+ | `shift-traffic` | ops | blue-green weighted ALB cutover across a service's colors |
43
43
 
44
44
  Flags mirror env: `--api --namespace --nonce --alias --secret-path --yubikey
45
45
  --pcr3-preimage-path --qos-release-dir --qos-client`.
package/dist-cli/cli.js CHANGED
@@ -32720,11 +32720,14 @@ var QosClient = class {
32720
32720
 
32721
32721
  // ../qos-core/src/state.ts
32722
32722
  function allMembers(sets) {
32723
- return Array.from(/* @__PURE__ */ new Set([...sets.manifest.members, ...sets.share.members, ...sets.patch.members]));
32723
+ return Array.from(
32724
+ /* @__PURE__ */ new Set([...sets.manifest.members, ...sets.share.members, ...sets.patch.members])
32725
+ );
32724
32726
  }
32725
32727
  function blockers(s) {
32726
32728
  const out = [];
32727
- if (s.dr_blocker) out.push(`DR: ${s.dr_blocker.reason} (since ${s.dr_blocker.since}) \u2014 share set must re-post`);
32729
+ if (s.dr_blocker)
32730
+ out.push(`DR: ${s.dr_blocker.reason} (since ${s.dr_blocker.since}) \u2014 share set must re-post`);
32728
32731
  switch (s.phase) {
32729
32732
  case "Init":
32730
32733
  case "CollectingPubkeys": {
@@ -32735,7 +32738,8 @@ function blockers(s) {
32735
32738
  }
32736
32739
  case "GenesisBooted": {
32737
32740
  const need = s.sets.share.members.filter((m3) => !s.shares_derived.includes(m3));
32738
- if (need.length) out.push(`waiting for shareSet members to derive+upload shares: ${need.join(", ")}`);
32741
+ if (need.length)
32742
+ out.push(`waiting for shareSet members to derive+upload shares: ${need.join(", ")}`);
32739
32743
  else out.push("all shares derived \u2014 coordinator will generate the manifest on next advance");
32740
32744
  break;
32741
32745
  }
@@ -32743,16 +32747,24 @@ function blockers(s) {
32743
32747
  case "CollectingApprovals": {
32744
32748
  const have = s.approvals.length;
32745
32749
  const need = s.sets.manifest.threshold - have;
32746
- if (need > 0) out.push(`waiting for ${need} more manifestSet approval(s) (have ${have}/${s.sets.manifest.threshold})`);
32750
+ if (need > 0)
32751
+ out.push(
32752
+ `waiting for ${need} more manifestSet approval(s) (have ${have}/${s.sets.manifest.threshold})`
32753
+ );
32747
32754
  else if (s.mode === "forward")
32748
- out.push(`approval threshold met \u2014 coordinator will key-forward Q from ${s.source_cluster} on next advance`);
32755
+ out.push(
32756
+ `approval threshold met \u2014 coordinator will key-forward Q from ${s.source_cluster} on next advance`
32757
+ );
32749
32758
  else out.push("approval threshold met \u2014 coordinator will boot-standard on next advance");
32750
32759
  break;
32751
32760
  }
32752
32761
  case "Booted":
32753
32762
  case "CollectingShares": {
32754
32763
  const need = s.sets.share.threshold - s.shares_posted;
32755
- if (need > 0) out.push(`waiting for ${need} more provisioned share(s) (have ${s.shares_posted}/${s.sets.share.threshold})`);
32764
+ if (need > 0)
32765
+ out.push(
32766
+ `waiting for ${need} more provisioned share(s) (have ${s.shares_posted}/${s.sets.share.threshold})`
32767
+ );
32756
32768
  else out.push("share threshold met \u2014 enclave reconstructing quorum key");
32757
32769
  break;
32758
32770
  }
@@ -32859,7 +32871,14 @@ var CeremonyApi = class {
32859
32871
  const wd = new Workdir("ceremony-sign-");
32860
32872
  try {
32861
32873
  const ts = Date.now();
32862
- const canonical = buildSignedPayload({ alias: signer.alias, namespace: ns, nonce, route, ts, artifact });
32874
+ const canonical = buildSignedPayload({
32875
+ alias: signer.alias,
32876
+ namespace: ns,
32877
+ nonce,
32878
+ route,
32879
+ ts,
32880
+ artifact
32881
+ });
32863
32882
  const payloadPath = wd.writeText("payload.json", canonical);
32864
32883
  const sigPath = wd.path("payload.sig");
32865
32884
  if (signer.secret.yubikey) {
@@ -32879,15 +32898,36 @@ var CeremonyApi = class {
32879
32898
  }
32880
32899
  }
32881
32900
  async putPubkey(ns, nonce, alias, pubkeyHex, signer) {
32882
- const r5 = await this.signedRequest("POST", ns, nonce, "pubkey", { pubkey: pubkeyHex }, { alias, ...signer });
32901
+ const r5 = await this.signedRequest(
32902
+ "POST",
32903
+ ns,
32904
+ nonce,
32905
+ "pubkey",
32906
+ { pubkey: pubkeyHex },
32907
+ { alias, ...signer }
32908
+ );
32883
32909
  if (!r5.ok) throw new Error(`putPubkey: ${r5.status} ${await r5.text()}`);
32884
32910
  }
32885
32911
  async putShare(ns, nonce, alias, shareB64, signer) {
32886
- const r5 = await this.signedRequest("PUT", ns, nonce, "share", { share: shareB64 }, { alias, ...signer });
32912
+ const r5 = await this.signedRequest(
32913
+ "PUT",
32914
+ ns,
32915
+ nonce,
32916
+ "share",
32917
+ { share: shareB64 },
32918
+ { alias, ...signer }
32919
+ );
32887
32920
  if (!r5.ok) throw new Error(`putShare: ${r5.status} ${await r5.text()}`);
32888
32921
  }
32889
32922
  async postApproval(ns, nonce, alias, approvalB64, signer) {
32890
- const r5 = await this.signedRequest("POST", ns, nonce, "approval", { approval: approvalB64 }, { alias, ...signer });
32923
+ const r5 = await this.signedRequest(
32924
+ "POST",
32925
+ ns,
32926
+ nonce,
32927
+ "approval",
32928
+ { approval: approvalB64 },
32929
+ { alias, ...signer }
32930
+ );
32891
32931
  if (!r5.ok) throw new Error(`postApproval: ${r5.status} ${await r5.text()}`);
32892
32932
  }
32893
32933
  async postEphShare(ns, nonce, alias, ephShareB64, approvalB64, signer) {
@@ -32946,13 +32986,9 @@ async function keygen(o3) {
32946
32986
  const pubPath = `${secretPath}.pub`;
32947
32987
  qos.generateFileKey(secretPath, pubPath);
32948
32988
  const pubHex = (0, import_node_fs3.readFileSync)(pubPath, "utf8").trim();
32949
- console.log(
32950
- `keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`
32951
- );
32989
+ console.log(`keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`);
32952
32990
  console.log(`keygen: pubkey written to ${pubPath}`);
32953
- console.log(
32954
- "\nRegister this operator (admin, IAM-gated) so it can join ceremonies:"
32955
- );
32991
+ console.log("\nRegister this operator (admin, IAM-gated) so it can join ceremonies:");
32956
32992
  console.log(
32957
32993
  " aws lambda invoke --function-name qos-coordinator-admin-<stack> --cli-binary-format raw-in-base64-out \\"
32958
32994
  );
@@ -32974,7 +33010,12 @@ function resolve(o3) {
32974
33010
  const nonce = Number(nonceStr);
32975
33011
  if (!Number.isInteger(nonce)) throw new Error(`--nonce must be an integer, got ${nonceStr}`);
32976
33012
  const alias = req(o3.alias ?? process.env.CEREMONY_ALIAS, "--alias");
32977
- const secret = o3.yubikey ? { yubikey: true } : { secretPath: req(o3.secretPath ?? process.env.CEREMONY_SECRET, "--secret-path / CEREMONY_SECRET") };
33013
+ const secret = o3.yubikey ? { yubikey: true } : {
33014
+ secretPath: req(
33015
+ o3.secretPath ?? process.env.CEREMONY_SECRET,
33016
+ "--secret-path / CEREMONY_SECRET"
33017
+ )
33018
+ };
32978
33019
  return {
32979
33020
  api,
32980
33021
  qos,
@@ -33000,7 +33041,8 @@ async function buildSetDirs(ctx, wd, sets) {
33000
33041
  const dir = wd.dir(name);
33001
33042
  for (const alias of set.members) {
33002
33043
  const hex = members[alias];
33003
- if (!hex) throw new Error(`operator '${alias}' has no registered pubkey (admin register-operator)`);
33044
+ if (!hex)
33045
+ throw new Error(`operator '${alias}' has no registered pubkey (admin register-operator)`);
33004
33046
  (0, import_node_fs4.writeFileSync)(`${dir}/${alias}.pub`, hex);
33005
33047
  }
33006
33048
  (0, import_node_fs4.writeFileSync)(`${dir}/quorum_threshold`, `${set.threshold}`);
@@ -33016,7 +33058,12 @@ async function pullGenesisDir(ctx, wd) {
33016
33058
  const dir = wd.dir("namespace");
33017
33059
  await pullArtifact(ctx, wd, "genesis/genesis_output", "namespace/genesis_output");
33018
33060
  await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "namespace/quorum_key.pub").catch(() => "");
33019
- await pullArtifact(ctx, wd, "genesis/genesis_attestation_doc", "namespace/genesis_attestation_doc").catch(() => "");
33061
+ await pullArtifact(
33062
+ ctx,
33063
+ wd,
33064
+ "genesis/genesis_attestation_doc",
33065
+ "namespace/genesis_attestation_doc"
33066
+ ).catch(() => "");
33020
33067
  return dir;
33021
33068
  }
33022
33069
  async function fetchState(ctx) {
@@ -33039,8 +33086,13 @@ async function share(o3) {
33039
33086
  secret: ctx.secret
33040
33087
  });
33041
33088
  const shareB64 = wd.readBytes(`${ctx.alias}.share`).toString("base64");
33042
- await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, { secret: ctx.secret, qos: ctx.qos });
33043
- console.log(`share: derived + uploaded encrypted share for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`);
33089
+ await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
33090
+ secret: ctx.secret,
33091
+ qos: ctx.qos
33092
+ });
33093
+ console.log(
33094
+ `share: derived + uploaded encrypted share for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`
33095
+ );
33044
33096
  } finally {
33045
33097
  wd.cleanup();
33046
33098
  }
@@ -33053,29 +33105,10 @@ async function approve(o3) {
33053
33105
  const state2 = await fetchState(ctx);
33054
33106
  const wd = new Workdir("ceremony-approve-");
33055
33107
  try {
33056
- const manifestPath = await pullArtifact(
33057
- ctx,
33058
- wd,
33059
- "manifest/manifest",
33060
- "manifest"
33061
- );
33062
- const pivotHashPath = await pullArtifact(
33063
- ctx,
33064
- wd,
33065
- "manifest/pivot.hash",
33066
- "pivot.hash"
33067
- );
33068
- const quorumKeyPath = await pullArtifact(
33069
- ctx,
33070
- wd,
33071
- "genesis/quorum_key.pub",
33072
- "quorum_key.pub"
33073
- );
33074
- const { manifestSetDir, shareSetDir, patchSetDir } = await buildSetDirs(
33075
- ctx,
33076
- wd,
33077
- state2.sets
33078
- );
33108
+ const manifestPath = await pullArtifact(ctx, wd, "manifest/manifest", "manifest");
33109
+ const pivotHashPath = await pullArtifact(ctx, wd, "manifest/pivot.hash", "pivot.hash");
33110
+ const quorumKeyPath = await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "quorum_key.pub");
33111
+ const { manifestSetDir, shareSetDir, patchSetDir } = await buildSetDirs(ctx, wd, state2.sets);
33079
33112
  const approvalsDir = wd.dir("approvals");
33080
33113
  const json = ctx.qos.display(manifestPath, "manifest");
33081
33114
  console.log("\n===== MANIFEST UNDER REVIEW =====");
@@ -33113,13 +33146,10 @@ async function approve(o3) {
33113
33146
  const nsFlat = state2.namespace.replace(/\//g, "-");
33114
33147
  const approvalFile = `${ctx.alias}-${nsFlat}-${state2.nonce}.approval`;
33115
33148
  const approvalB64 = wd.readBytes(`approvals/${approvalFile}`).toString("base64");
33116
- await ctx.api.postApproval(
33117
- ctx.namespace,
33118
- ctx.nonce,
33119
- ctx.alias,
33120
- approvalB64,
33121
- { secret: ctx.secret, qos: ctx.qos }
33122
- );
33149
+ await ctx.api.postApproval(ctx.namespace, ctx.nonce, ctx.alias, approvalB64, {
33150
+ secret: ctx.secret,
33151
+ qos: ctx.qos
33152
+ });
33123
33153
  console.log(`approve: uploaded approval '${approvalFile}'`);
33124
33154
  } finally {
33125
33155
  wd.cleanup();
@@ -33133,8 +33163,18 @@ async function provision(o3) {
33133
33163
  const wd = new Workdir("ceremony-provision-");
33134
33164
  try {
33135
33165
  const gen = state2.boot_generation ?? 0;
33136
- const attestationDocPath = await pullArtifact(ctx, wd, `provision/g${gen}/attestation_doc`, "attestation_doc");
33137
- const manifestEnvelopePath = await pullArtifact(ctx, wd, "manifest/manifest_envelope", "manifest_envelope");
33166
+ const attestationDocPath = await pullArtifact(
33167
+ ctx,
33168
+ wd,
33169
+ `provision/g${gen}/attestation_doc`,
33170
+ "attestation_doc"
33171
+ );
33172
+ const manifestEnvelopePath = await pullArtifact(
33173
+ ctx,
33174
+ wd,
33175
+ "manifest/manifest_envelope",
33176
+ "manifest_envelope"
33177
+ );
33138
33178
  const sharePath = await pullArtifact(ctx, wd, `shares/${ctx.alias}.share`, `${ctx.alias}.share`);
33139
33179
  const { manifestSetDir } = await buildSetDirs(ctx, wd, state2.sets);
33140
33180
  const ephWrappedSharePath = wd.path(`${ctx.alias}.eph_share`);
@@ -33169,7 +33209,8 @@ async function status(o3) {
33169
33209
  const apiUrl = o3.api ?? process.env.CEREMONY_API;
33170
33210
  const namespace = o3.namespace ?? process.env.CEREMONY_NAMESPACE;
33171
33211
  const nonceStr = o3.nonce ?? process.env.CEREMONY_NONCE;
33172
- if (!apiUrl || !namespace || !nonceStr) throw new Error("status requires --api --namespace --nonce");
33212
+ if (!apiUrl || !namespace || !nonceStr)
33213
+ throw new Error("status requires --api --namespace --nonce");
33173
33214
  const api = new CeremonyApi(apiUrl);
33174
33215
  const s = await api.getState(namespace, Number(nonceStr));
33175
33216
  console.log(`namespace : ${s.namespace}`);
@@ -33190,7 +33231,9 @@ var import_client_elastic_load_balancing_v2 = __toESM(require_dist_cjs16());
33190
33231
  function buildWeightedForward(tgs, weights) {
33191
33232
  for (const k5 of Object.keys(weights)) {
33192
33233
  if (!tgs.some((t) => t.nonce === Number(k5))) {
33193
- throw new Error(`shift-traffic: weight given for nonce ${k5} but no matching target group was found`);
33234
+ throw new Error(
33235
+ `shift-traffic: weight given for nonce ${k5} but no matching target group was found`
33236
+ );
33194
33237
  }
33195
33238
  }
33196
33239
  if (tgs.length === 0) throw new Error("shift-traffic: no target groups to forward to");
@@ -33223,16 +33266,21 @@ async function discoverColorTargetGroups(elbv2, service, stack, role) {
33223
33266
  const tags = Object.fromEntries((desc.Tags ?? []).map((t) => [t.Key, t.Value]));
33224
33267
  if (tags.TeeService === service && tags.Stack === stack && tags.Role === role && tags.Nonce) {
33225
33268
  const src = batch.find((b5) => b5.arn === desc.ResourceArn);
33226
- if (src) matched.push({ nonce: Number(tags.Nonce), arn: desc.ResourceArn, lbArn: src.lbArn });
33269
+ if (src)
33270
+ matched.push({ nonce: Number(tags.Nonce), arn: desc.ResourceArn, lbArn: src.lbArn });
33227
33271
  }
33228
33272
  }
33229
33273
  }
33230
33274
  if (matched.length === 0) {
33231
- throw new Error(`shift-traffic: no ${role} target groups found for service '${service}' stack '${stack}'`);
33275
+ throw new Error(
33276
+ `shift-traffic: no ${role} target groups found for service '${service}' stack '${stack}'`
33277
+ );
33232
33278
  }
33233
33279
  const lbArn = matched[0].lbArn;
33234
33280
  if (!matched.every((m3) => m3.lbArn === lbArn)) {
33235
- throw new Error(`shift-traffic: ${role} target groups span multiple load balancers \u2014 refusing to guess`);
33281
+ throw new Error(
33282
+ `shift-traffic: ${role} target groups span multiple load balancers \u2014 refusing to guess`
33283
+ );
33236
33284
  }
33237
33285
  return { lbArn, tgs: matched };
33238
33286
  }
@@ -33250,7 +33298,11 @@ async function shiftTraffic(opts) {
33250
33298
  const role = opts.listener ?? "app";
33251
33299
  const elbv2 = opts.client ?? new import_client_elastic_load_balancing_v2.ElasticLoadBalancingV2Client(opts.region ? { region: opts.region } : {});
33252
33300
  const { lbArn, tgs } = await discoverColorTargetGroups(elbv2, opts.service, opts.stack, role);
33253
- const listenerArn = await findListenerForTargetGroups(elbv2, lbArn, new Set(tgs.map((t) => t.arn)));
33301
+ const listenerArn = await findListenerForTargetGroups(
33302
+ elbv2,
33303
+ lbArn,
33304
+ new Set(tgs.map((t) => t.arn))
33305
+ );
33254
33306
  const actions = buildWeightedForward(tgs, opts.weights);
33255
33307
  await elbv2.send(new import_client_elastic_load_balancing_v2.ModifyListenerCommand({ ListenerArn: listenerArn, DefaultActions: actions }));
33256
33308
  return { listenerArn, actions };
@@ -33264,7 +33316,13 @@ async function shiftTrafficCommand(o3) {
33264
33316
  const weights = {};
33265
33317
  for (const [k5, v] of Object.entries(parsed)) weights[Number(k5)] = v;
33266
33318
  const listener = o3.listener ?? "app";
33267
- const { listenerArn, actions } = await shiftTraffic({ service, stack, region: o3.region, listener, weights });
33319
+ const { listenerArn, actions } = await shiftTraffic({
33320
+ service,
33321
+ stack,
33322
+ region: o3.region,
33323
+ listener,
33324
+ weights
33325
+ });
33268
33326
  const summary = (actions[0].ForwardConfig?.TargetGroups ?? []).map((t) => `${t.TargetGroupArn?.split("/").slice(-2, -1)[0] ?? t.TargetGroupArn}=${t.Weight}`).join(" ");
33269
33327
  console.log(`shift-traffic: ${service}/${stack} ${listener} listener \u2192 ${summary}`);
33270
33328
  console.log(` listener: ${listenerArn}`);
@@ -33272,9 +33330,14 @@ async function shiftTrafficCommand(o3) {
33272
33330
 
33273
33331
  // src/cli.ts
33274
33332
  var program2 = new Command();
33275
- program2.name("ceremony").description("QOS TEE operator ceremony CLI \u2014 your private key never leaves this device; all crypto via qos_client");
33333
+ program2.name("qos-ceremony").description(
33334
+ "QOS TEE operator ceremony CLI \u2014 your private key never leaves this device; all crypto via qos_client"
33335
+ );
33276
33336
  function withCommon(cmd) {
33277
- return cmd.option("--api <url>", "coordinator API base URL (env CEREMONY_API)").option("--namespace <ns>", "ceremony namespace, e.g. arith (env CEREMONY_NAMESPACE)").option("--nonce <n>", "manifest nonce (env CEREMONY_NONCE)").option("--alias <alias>", "operator alias (env CEREMONY_ALIAS)").option("--secret-path <path>", "operator master-seed file \u2014 stays local (env CEREMONY_SECRET)").option("--yubikey", "use a YubiKey PIV key instead of a seed file").option("--pcr3-preimage-path <path>", "PCR3 preimage (IAM role ARN) file").option("--qos-release-dir <dir>", "QOS release dir (aws-x86_64.pcrs)").option("--qos-client <bin>", "qos_client binary path (env QOS_CLIENT_BIN; else embedded/PATH)").option("--unsafe-eph-path-override <path>", "[dev/mock only] read enclave ephemeral pubkey from this file");
33337
+ return cmd.option("--api <url>", "coordinator API base URL (env CEREMONY_API)").option("--namespace <ns>", "ceremony namespace, e.g. arith (env CEREMONY_NAMESPACE)").option("--nonce <n>", "manifest nonce (env CEREMONY_NONCE)").option("--alias <alias>", "operator alias (env CEREMONY_ALIAS)").option("--secret-path <path>", "operator master-seed file \u2014 stays local (env CEREMONY_SECRET)").option("--yubikey", "use a YubiKey PIV key instead of a seed file").option("--pcr3-preimage-path <path>", "PCR3 preimage (IAM role ARN) file").option("--qos-release-dir <dir>", "QOS release dir (aws-x86_64.pcrs)").option("--qos-client <bin>", "qos_client binary path (env QOS_CLIENT_BIN; else embedded/PATH)").option(
33338
+ "--unsafe-eph-path-override <path>",
33339
+ "[dev/mock only] read enclave ephemeral pubkey from this file"
33340
+ );
33278
33341
  }
33279
33342
  function run(fn) {
33280
33343
  return (o3) => fn(o3).catch((e5) => {
@@ -33282,12 +33345,12 @@ function run(fn) {
33282
33345
  process.exit(1);
33283
33346
  });
33284
33347
  }
33285
- withCommon(program2.command("keygen").description("generate operator keypair; print pubkey to register (seed stays local)")).action(
33286
- run(keygen)
33287
- );
33288
- withCommon(program2.command("share").description("derive quorum-key share from genesis output; upload (encrypted)")).action(
33289
- run(share)
33290
- );
33348
+ withCommon(
33349
+ program2.command("keygen").description("generate operator keypair; print pubkey to register (seed stays local)")
33350
+ ).action(run(keygen));
33351
+ withCommon(
33352
+ program2.command("share").description("derive quorum-key share from genesis output; upload (encrypted)")
33353
+ ).action(run(share));
33291
33354
  withCommon(
33292
33355
  program2.command("approve").description("review + sign the manifest (manifestSet)").option("--yes", "skip interactive confirm")
33293
33356
  ).action(run(approve));
@@ -33295,7 +33358,9 @@ withCommon(
33295
33358
  program2.command("provision").description("re-encrypt share to the enclave ephemeral key; post it (shareSet)")
33296
33359
  ).action(run(provision));
33297
33360
  program2.command("status").description("show ceremony phase + blockers (read-only)").option("--api <url>", "coordinator API base URL (env CEREMONY_API)").option("--namespace <ns>", "ceremony namespace (env CEREMONY_NAMESPACE)").option("--nonce <n>", "manifest nonce (env CEREMONY_NONCE)").action(run(status));
33298
- program2.command("shift-traffic").description("[ops] blue-green weighted traffic cutover across a service's colors (needs AWS creds)").option("--service <name>", "service name (env CEREMONY_SERVICE)").option("--stack <stack>", "stack (env CEREMONY_STACK)").option("--region <r>", "AWS region").option("--listener <app|control>", "which listener carries the split (default app)").option("--weights <json>", `nonce->weight map, e.g. '{"2026070601":0,"2026071501":100}'`).action(run(shiftTrafficCommand));
33361
+ program2.command("shift-traffic").description(
33362
+ "[ops] blue-green weighted traffic cutover across a service's colors (needs AWS creds)"
33363
+ ).option("--service <name>", "service name (env CEREMONY_SERVICE)").option("--stack <stack>", "stack (env CEREMONY_STACK)").option("--region <r>", "AWS region").option("--listener <app|control>", "which listener carries the split (default app)").option("--weights <json>", `nonce->weight map, e.g. '{"2026070601":0,"2026071501":100}'`).action(run(shiftTrafficCommand));
33299
33364
  program2.parseAsync(process.argv).catch((e5) => {
33300
33365
  console.error(e5);
33301
33366
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umbra-privacy/qos-ceremony",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "QOS TEE operator ceremony CLI — keygen/share/approve/provision + shift-traffic over the coordinator API. All crypto runs in qos_client (embedded for common platforms); your private key never leaves the device. Ships NO coordinator code.",
5
5
  "keywords": [
6
6
  "qos",
@@ -17,7 +17,7 @@
17
17
  "directory": "tools/operator-cli"
18
18
  },
19
19
  "bin": {
20
- "ceremony": "dist-cli/cli.js"
20
+ "qos-ceremony": "dist-cli/cli.js"
21
21
  },
22
22
  "main": "dist-cli/cli.js",
23
23
  "files": [