@umbra-privacy/qos-ceremony 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +15 -15
  2. package/dist-cli/cli.js +145 -70
  3. package/package.json +3 -3
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) {
@@ -32926,9 +32966,19 @@ function resolveQosBin(explicit) {
32926
32966
  if (explicit) return explicit;
32927
32967
  if (process.env.QOS_CLIENT_BIN) return process.env.QOS_CLIENT_BIN;
32928
32968
  const embedded = (0, import_node_path2.join)(__dirname, "bin", `qos_client-${process.platform}-${process.arch}`);
32929
- if ((0, import_node_fs2.existsSync)(embedded)) return embedded;
32969
+ if ((0, import_node_fs2.existsSync)(embedded)) {
32970
+ ensureExecutable(embedded);
32971
+ return embedded;
32972
+ }
32930
32973
  return void 0;
32931
32974
  }
32975
+ function ensureExecutable(path) {
32976
+ try {
32977
+ const mode = (0, import_node_fs2.statSync)(path).mode;
32978
+ if ((mode & 73) !== 73) (0, import_node_fs2.chmodSync)(path, mode | 493);
32979
+ } catch {
32980
+ }
32981
+ }
32932
32982
 
32933
32983
  // src/commands/keygen.ts
32934
32984
  async function keygen(o3) {
@@ -32946,13 +32996,9 @@ async function keygen(o3) {
32946
32996
  const pubPath = `${secretPath}.pub`;
32947
32997
  qos.generateFileKey(secretPath, pubPath);
32948
32998
  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
- );
32999
+ console.log(`keygen: seed written to ${secretPath} (keep it safe \u2014 it never leaves this device)`);
32952
33000
  console.log(`keygen: pubkey written to ${pubPath}`);
32953
- console.log(
32954
- "\nRegister this operator (admin, IAM-gated) so it can join ceremonies:"
32955
- );
33001
+ console.log("\nRegister this operator (admin, IAM-gated) so it can join ceremonies:");
32956
33002
  console.log(
32957
33003
  " aws lambda invoke --function-name qos-coordinator-admin-<stack> --cli-binary-format raw-in-base64-out \\"
32958
33004
  );
@@ -32974,7 +33020,12 @@ function resolve(o3) {
32974
33020
  const nonce = Number(nonceStr);
32975
33021
  if (!Number.isInteger(nonce)) throw new Error(`--nonce must be an integer, got ${nonceStr}`);
32976
33022
  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") };
33023
+ const secret = o3.yubikey ? { yubikey: true } : {
33024
+ secretPath: req(
33025
+ o3.secretPath ?? process.env.CEREMONY_SECRET,
33026
+ "--secret-path / CEREMONY_SECRET"
33027
+ )
33028
+ };
32978
33029
  return {
32979
33030
  api,
32980
33031
  qos,
@@ -33000,7 +33051,8 @@ async function buildSetDirs(ctx, wd, sets) {
33000
33051
  const dir = wd.dir(name);
33001
33052
  for (const alias of set.members) {
33002
33053
  const hex = members[alias];
33003
- if (!hex) throw new Error(`operator '${alias}' has no registered pubkey (admin register-operator)`);
33054
+ if (!hex)
33055
+ throw new Error(`operator '${alias}' has no registered pubkey (admin register-operator)`);
33004
33056
  (0, import_node_fs4.writeFileSync)(`${dir}/${alias}.pub`, hex);
33005
33057
  }
33006
33058
  (0, import_node_fs4.writeFileSync)(`${dir}/quorum_threshold`, `${set.threshold}`);
@@ -33016,7 +33068,12 @@ async function pullGenesisDir(ctx, wd) {
33016
33068
  const dir = wd.dir("namespace");
33017
33069
  await pullArtifact(ctx, wd, "genesis/genesis_output", "namespace/genesis_output");
33018
33070
  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(() => "");
33071
+ await pullArtifact(
33072
+ ctx,
33073
+ wd,
33074
+ "genesis/genesis_attestation_doc",
33075
+ "namespace/genesis_attestation_doc"
33076
+ ).catch(() => "");
33020
33077
  return dir;
33021
33078
  }
33022
33079
  async function fetchState(ctx) {
@@ -33039,8 +33096,13 @@ async function share(o3) {
33039
33096
  secret: ctx.secret
33040
33097
  });
33041
33098
  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})`);
33099
+ await ctx.api.putShare(ctx.namespace, ctx.nonce, ctx.alias, shareB64, {
33100
+ secret: ctx.secret,
33101
+ qos: ctx.qos
33102
+ });
33103
+ console.log(
33104
+ `share: derived + uploaded encrypted share for '${ctx.alias}' (${ctx.namespace}/${ctx.nonce})`
33105
+ );
33044
33106
  } finally {
33045
33107
  wd.cleanup();
33046
33108
  }
@@ -33053,29 +33115,10 @@ async function approve(o3) {
33053
33115
  const state2 = await fetchState(ctx);
33054
33116
  const wd = new Workdir("ceremony-approve-");
33055
33117
  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
- );
33118
+ const manifestPath = await pullArtifact(ctx, wd, "manifest/manifest", "manifest");
33119
+ const pivotHashPath = await pullArtifact(ctx, wd, "manifest/pivot.hash", "pivot.hash");
33120
+ const quorumKeyPath = await pullArtifact(ctx, wd, "genesis/quorum_key.pub", "quorum_key.pub");
33121
+ const { manifestSetDir, shareSetDir, patchSetDir } = await buildSetDirs(ctx, wd, state2.sets);
33079
33122
  const approvalsDir = wd.dir("approvals");
33080
33123
  const json = ctx.qos.display(manifestPath, "manifest");
33081
33124
  console.log("\n===== MANIFEST UNDER REVIEW =====");
@@ -33113,13 +33156,10 @@ async function approve(o3) {
33113
33156
  const nsFlat = state2.namespace.replace(/\//g, "-");
33114
33157
  const approvalFile = `${ctx.alias}-${nsFlat}-${state2.nonce}.approval`;
33115
33158
  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
- );
33159
+ await ctx.api.postApproval(ctx.namespace, ctx.nonce, ctx.alias, approvalB64, {
33160
+ secret: ctx.secret,
33161
+ qos: ctx.qos
33162
+ });
33123
33163
  console.log(`approve: uploaded approval '${approvalFile}'`);
33124
33164
  } finally {
33125
33165
  wd.cleanup();
@@ -33133,8 +33173,18 @@ async function provision(o3) {
33133
33173
  const wd = new Workdir("ceremony-provision-");
33134
33174
  try {
33135
33175
  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");
33176
+ const attestationDocPath = await pullArtifact(
33177
+ ctx,
33178
+ wd,
33179
+ `provision/g${gen}/attestation_doc`,
33180
+ "attestation_doc"
33181
+ );
33182
+ const manifestEnvelopePath = await pullArtifact(
33183
+ ctx,
33184
+ wd,
33185
+ "manifest/manifest_envelope",
33186
+ "manifest_envelope"
33187
+ );
33138
33188
  const sharePath = await pullArtifact(ctx, wd, `shares/${ctx.alias}.share`, `${ctx.alias}.share`);
33139
33189
  const { manifestSetDir } = await buildSetDirs(ctx, wd, state2.sets);
33140
33190
  const ephWrappedSharePath = wd.path(`${ctx.alias}.eph_share`);
@@ -33169,7 +33219,8 @@ async function status(o3) {
33169
33219
  const apiUrl = o3.api ?? process.env.CEREMONY_API;
33170
33220
  const namespace = o3.namespace ?? process.env.CEREMONY_NAMESPACE;
33171
33221
  const nonceStr = o3.nonce ?? process.env.CEREMONY_NONCE;
33172
- if (!apiUrl || !namespace || !nonceStr) throw new Error("status requires --api --namespace --nonce");
33222
+ if (!apiUrl || !namespace || !nonceStr)
33223
+ throw new Error("status requires --api --namespace --nonce");
33173
33224
  const api = new CeremonyApi(apiUrl);
33174
33225
  const s = await api.getState(namespace, Number(nonceStr));
33175
33226
  console.log(`namespace : ${s.namespace}`);
@@ -33190,7 +33241,9 @@ var import_client_elastic_load_balancing_v2 = __toESM(require_dist_cjs16());
33190
33241
  function buildWeightedForward(tgs, weights) {
33191
33242
  for (const k5 of Object.keys(weights)) {
33192
33243
  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`);
33244
+ throw new Error(
33245
+ `shift-traffic: weight given for nonce ${k5} but no matching target group was found`
33246
+ );
33194
33247
  }
33195
33248
  }
33196
33249
  if (tgs.length === 0) throw new Error("shift-traffic: no target groups to forward to");
@@ -33223,16 +33276,21 @@ async function discoverColorTargetGroups(elbv2, service, stack, role) {
33223
33276
  const tags = Object.fromEntries((desc.Tags ?? []).map((t) => [t.Key, t.Value]));
33224
33277
  if (tags.TeeService === service && tags.Stack === stack && tags.Role === role && tags.Nonce) {
33225
33278
  const src = batch.find((b5) => b5.arn === desc.ResourceArn);
33226
- if (src) matched.push({ nonce: Number(tags.Nonce), arn: desc.ResourceArn, lbArn: src.lbArn });
33279
+ if (src)
33280
+ matched.push({ nonce: Number(tags.Nonce), arn: desc.ResourceArn, lbArn: src.lbArn });
33227
33281
  }
33228
33282
  }
33229
33283
  }
33230
33284
  if (matched.length === 0) {
33231
- throw new Error(`shift-traffic: no ${role} target groups found for service '${service}' stack '${stack}'`);
33285
+ throw new Error(
33286
+ `shift-traffic: no ${role} target groups found for service '${service}' stack '${stack}'`
33287
+ );
33232
33288
  }
33233
33289
  const lbArn = matched[0].lbArn;
33234
33290
  if (!matched.every((m3) => m3.lbArn === lbArn)) {
33235
- throw new Error(`shift-traffic: ${role} target groups span multiple load balancers \u2014 refusing to guess`);
33291
+ throw new Error(
33292
+ `shift-traffic: ${role} target groups span multiple load balancers \u2014 refusing to guess`
33293
+ );
33236
33294
  }
33237
33295
  return { lbArn, tgs: matched };
33238
33296
  }
@@ -33250,7 +33308,11 @@ async function shiftTraffic(opts) {
33250
33308
  const role = opts.listener ?? "app";
33251
33309
  const elbv2 = opts.client ?? new import_client_elastic_load_balancing_v2.ElasticLoadBalancingV2Client(opts.region ? { region: opts.region } : {});
33252
33310
  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)));
33311
+ const listenerArn = await findListenerForTargetGroups(
33312
+ elbv2,
33313
+ lbArn,
33314
+ new Set(tgs.map((t) => t.arn))
33315
+ );
33254
33316
  const actions = buildWeightedForward(tgs, opts.weights);
33255
33317
  await elbv2.send(new import_client_elastic_load_balancing_v2.ModifyListenerCommand({ ListenerArn: listenerArn, DefaultActions: actions }));
33256
33318
  return { listenerArn, actions };
@@ -33264,7 +33326,13 @@ async function shiftTrafficCommand(o3) {
33264
33326
  const weights = {};
33265
33327
  for (const [k5, v] of Object.entries(parsed)) weights[Number(k5)] = v;
33266
33328
  const listener = o3.listener ?? "app";
33267
- const { listenerArn, actions } = await shiftTraffic({ service, stack, region: o3.region, listener, weights });
33329
+ const { listenerArn, actions } = await shiftTraffic({
33330
+ service,
33331
+ stack,
33332
+ region: o3.region,
33333
+ listener,
33334
+ weights
33335
+ });
33268
33336
  const summary = (actions[0].ForwardConfig?.TargetGroups ?? []).map((t) => `${t.TargetGroupArn?.split("/").slice(-2, -1)[0] ?? t.TargetGroupArn}=${t.Weight}`).join(" ");
33269
33337
  console.log(`shift-traffic: ${service}/${stack} ${listener} listener \u2192 ${summary}`);
33270
33338
  console.log(` listener: ${listenerArn}`);
@@ -33272,9 +33340,14 @@ async function shiftTrafficCommand(o3) {
33272
33340
 
33273
33341
  // src/cli.ts
33274
33342
  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");
33343
+ program2.name("qos-ceremony").description(
33344
+ "QOS TEE operator ceremony CLI \u2014 your private key never leaves this device; all crypto via qos_client"
33345
+ );
33276
33346
  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");
33347
+ 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(
33348
+ "--unsafe-eph-path-override <path>",
33349
+ "[dev/mock only] read enclave ephemeral pubkey from this file"
33350
+ );
33278
33351
  }
33279
33352
  function run(fn) {
33280
33353
  return (o3) => fn(o3).catch((e5) => {
@@ -33282,12 +33355,12 @@ function run(fn) {
33282
33355
  process.exit(1);
33283
33356
  });
33284
33357
  }
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
- );
33358
+ withCommon(
33359
+ program2.command("keygen").description("generate operator keypair; print pubkey to register (seed stays local)")
33360
+ ).action(run(keygen));
33361
+ withCommon(
33362
+ program2.command("share").description("derive quorum-key share from genesis output; upload (encrypted)")
33363
+ ).action(run(share));
33291
33364
  withCommon(
33292
33365
  program2.command("approve").description("review + sign the manifest (manifestSet)").option("--yes", "skip interactive confirm")
33293
33366
  ).action(run(approve));
@@ -33295,7 +33368,9 @@ withCommon(
33295
33368
  program2.command("provision").description("re-encrypt share to the enclave ephemeral key; post it (shareSet)")
33296
33369
  ).action(run(provision));
33297
33370
  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));
33371
+ program2.command("shift-traffic").description(
33372
+ "[ops] blue-green weighted traffic cutover across a service's colors (needs AWS creds)"
33373
+ ).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
33374
  program2.parseAsync(process.argv).catch((e5) => {
33300
33375
  console.error(e5);
33301
33376
  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.2",
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": [
@@ -44,6 +44,6 @@
44
44
  "typecheck": "tsc -p tsconfig.json --noEmit",
45
45
  "test": "vitest run",
46
46
  "bundle:cli": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist-cli/cli.js && chmod +x dist-cli/cli.js",
47
- "embed:qos": "mkdir -p dist-cli/bin && ( [ -f ../../../qos/target/release/qos_client ] && cp -L ../../../qos/target/release/qos_client dist-cli/bin/qos_client-darwin-arm64 || true ) && ( [ -f ../ceremony/dist-lambda/qos_client ] && cp -L ../ceremony/dist-lambda/qos_client dist-cli/bin/qos_client-linux-x64 || true ) && ls -la dist-cli/bin"
47
+ "embed:qos": "mkdir -p dist-cli/bin && ( [ -f ../../../qos/target/release/qos_client ] && cp -L ../../../qos/target/release/qos_client dist-cli/bin/qos_client-darwin-arm64 || true ) && ( [ -f ../ceremony/dist-lambda/qos_client ] && cp -L ../ceremony/dist-lambda/qos_client dist-cli/bin/qos_client-linux-x64 || true ) && ( chmod +x dist-cli/bin/* 2>/dev/null || true ) && ls -la dist-cli/bin"
48
48
  }
49
49
  }