instabox 0.6.3 → 0.7.0

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.
@@ -1,7 +1,7 @@
1
1
  // What a directly copied configuration file becomes: the local file with this
2
2
  // machine's HOME rewritten to the box's, lines that look like credentials
3
3
  // removed and macOS-only lines commented out. ~/.gitconfig also gets a few
4
- // changes so git works on a box that has GH_TOKEN and no SSH key.
4
+ // changes so git works on a box that has no signing or SSH key.
5
5
  //
6
6
  // Used by plan.mjs (for sizes) and push.mjs (for the upload). Nothing here
7
7
  // prints a file's content.
@@ -40,25 +40,11 @@ export function scrubText(text, { path, home }) {
40
40
  return { text: out.join('\n'), secrets, macos };
41
41
  }
42
42
 
43
- // The credential helper `gh auth setup-git` writes: git over HTTPS uses GH_TOKEN through gh.
44
- export const GH_HELPER = [
45
- '',
46
- `# ${TAG}: git over HTTPS to GitHub uses an explicitly selected env GH_TOKEN through gh`,
47
- '[credential "https://github.com"]',
48
- '\thelper =',
49
- '\thelper = !gh auth git-credential',
50
- '[credential "https://gist.github.com"]',
51
- '\thelper =',
52
- '\thelper = !gh auth git-credential',
53
- '',
54
- ].join('\n');
55
-
56
- // ~/.gitconfig for the box. ghToken: GH_TOKEN is explicitly selected for env delivery, so the gh helper is added.
57
- // Changes, besides scrubText's:
43
+ // ~/.gitconfig for the box. Changes, besides scrubText's:
58
44
  // - commit/tag signing is turned off: the signing key (GPG, SSH, 1Password) is not on the box;
59
45
  // - url.<ssh>.insteadOf rewrites are commented out: the box has no SSH key, and they would
60
46
  // send HTTPS remotes over SSH.
61
- export function gitconfigText(text, { home, ghToken }) {
47
+ export function gitconfigText(text, { home }) {
62
48
  const r = scrubText(text, { path: '.gitconfig', home });
63
49
  let section = '';
64
50
  let signing = 0;
@@ -77,13 +63,11 @@ export function gitconfigText(text, { home, ghToken }) {
77
63
  }
78
64
  return line;
79
65
  });
80
- let out = lines.join('\n');
81
- if (ghToken) out = out.replace(/\n*$/, '\n') + GH_HELPER;
82
- return { text: out, secrets: r.secrets, macos: r.macos, signing, insteadOf };
66
+ return { text: lines.join('\n'), secrets: r.secrets, macos: r.macos, signing, insteadOf };
83
67
  }
84
68
 
85
69
  // The content of one file of a file item, or {error}. f: {path, src, transform}.
86
- export function fileContent(f, { home, ghToken = false } = {}) {
70
+ export function fileContent(f, { home } = {}) {
87
71
  let buf;
88
72
  try {
89
73
  buf = readFileSync(f.src);
@@ -93,7 +77,7 @@ export function fileContent(f, { home, ghToken = false } = {}) {
93
77
  if (buf.includes(0)) return { error: `${f.src}: a binary file (config copying supports UTF-8 text only)` };
94
78
  const text = buf.toString('utf8');
95
79
  if (!Buffer.from(text).equals(buf)) return { error: `${f.src}: invalid UTF-8 text` };
96
- if (f.transform === 'gitconfig') return gitconfigText(text, { home, ghToken });
80
+ if (f.transform === 'gitconfig') return gitconfigText(text, { home });
97
81
  if (f.transform === 'raw') {
98
82
  if (SECRET_VALUE.test(text) || text.split('\n').some((line) => SECRET_KEY_LINE.test(line)))
99
83
  return { error: `${f.src}: possible credential in structured config; review it separately` };
@@ -3,7 +3,6 @@
3
3
  // The one rule every function here keeps: a credential's value is never
4
4
  // returned to the agent, stored in a file or printed. Code that looks at a
5
5
  // secret returns names, places and sizes only.
6
- import { isIP } from 'node:net';
7
6
  import { spawnSync } from 'node:child_process';
8
7
  import { lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
9
8
  import { fileURLToPath } from 'node:url';
@@ -38,50 +37,55 @@ export const SECRET_VALUE = new RegExp(
38
37
  export const SECRET_KEY_LINE =
39
38
  /(?:^|[\s{,;:])["']?[\w.-]*(?:token|password|passwd|secret|api[_-]?key|apikey|_auth|authorization|extraheader|private[_-]?key)[\w.-]*["']?\s*[=:]\s*["']?(?![0-9.]+\b|true\b|false\b)[^\s"',}]/i;
40
39
 
41
- // Model-provider and developer-service tokens: moved by default when found. Any other
42
- // credential-looking variable exported in a shell file is listed unchecked.
43
- export const WELL_KNOWN = new Set([
44
- 'GH_TOKEN',
45
- 'GITHUB_TOKEN',
46
- 'CLAUDE_CODE_OAUTH_TOKEN',
47
- 'ANTHROPIC_API_KEY',
48
- 'OPENAI_API_KEY',
49
- 'GEMINI_API_KEY',
50
- 'GOOGLE_API_KEY',
51
- 'GOOGLE_GENERATIVE_AI_API_KEY',
52
- 'OPENROUTER_API_KEY',
53
- 'GROQ_API_KEY',
54
- 'MISTRAL_API_KEY',
55
- 'DEEPSEEK_API_KEY',
56
- 'XAI_API_KEY',
57
- 'TOGETHER_API_KEY',
58
- 'FIREWORKS_API_KEY',
59
- 'PERPLEXITY_API_KEY',
60
- 'COHERE_API_KEY',
61
- 'VOYAGE_API_KEY',
62
- 'REPLICATE_API_TOKEN',
63
- 'ELEVENLABS_API_KEY',
64
- 'TAVILY_API_KEY',
65
- 'EXA_API_KEY',
66
- 'FIRECRAWL_API_KEY',
67
- 'BRAVE_API_KEY',
68
- 'PINECONE_API_KEY',
69
- 'HF_TOKEN',
70
- 'HUGGING_FACE_HUB_TOKEN',
71
- 'NPM_TOKEN',
72
- 'VERCEL_TOKEN',
73
- 'NETLIFY_AUTH_TOKEN',
74
- 'CLOUDFLARE_API_TOKEN',
75
- 'SUPABASE_ACCESS_TOKEN',
76
- 'FLY_API_TOKEN',
77
- 'RAILWAY_TOKEN',
78
- 'RENDER_API_KEY',
79
- 'NEON_API_KEY',
80
- 'SENTRY_AUTH_TOKEN',
81
- 'LINEAR_API_KEY',
82
- 'GITLAB_TOKEN',
83
- 'DIGITALOCEAN_ACCESS_TOKEN',
84
- ]);
40
+ // The tokens this skill saves, each with the HTTPS API hosts its usual client sends it to in a
41
+ // request header. The box gets a placeholder and the outbound proxy fills in the token only for
42
+ // these hosts. A token whose client signs with it, sends it in a body or URL, or talks to hosts
43
+ // that cannot be named exactly is left out: the scan lists it as a manual step.
44
+ // FORMAT is the scan and approved-selection format. 4: proxy-only Secrets, hosts from
45
+ // TOKEN_HOSTS. Older files approved env delivery or other hosts; they are refused, never
46
+ // reinterpreted, so an approval cannot widen a credential's reach.
47
+ export const FORMAT = 4;
48
+
49
+ export const TOKEN_HOSTS = new Map(
50
+ Object.entries({
51
+ GH_TOKEN: ['api.github.com'],
52
+ GITHUB_TOKEN: ['api.github.com'],
53
+ ANTHROPIC_API_KEY: ['api.anthropic.com'],
54
+ OPENAI_API_KEY: ['api.openai.com'],
55
+ GEMINI_API_KEY: ['generativelanguage.googleapis.com'],
56
+ GOOGLE_API_KEY: ['generativelanguage.googleapis.com'],
57
+ GOOGLE_GENERATIVE_AI_API_KEY: ['generativelanguage.googleapis.com'],
58
+ OPENROUTER_API_KEY: ['openrouter.ai'],
59
+ GROQ_API_KEY: ['api.groq.com'],
60
+ MISTRAL_API_KEY: ['api.mistral.ai'],
61
+ DEEPSEEK_API_KEY: ['api.deepseek.com'],
62
+ XAI_API_KEY: ['api.x.ai'],
63
+ TOGETHER_API_KEY: ['api.together.ai', 'api.together.xyz'],
64
+ FIREWORKS_API_KEY: ['api.fireworks.ai'],
65
+ PERPLEXITY_API_KEY: ['api.perplexity.ai'],
66
+ COHERE_API_KEY: ['api.cohere.ai', 'api.cohere.com'],
67
+ VOYAGE_API_KEY: ['api.voyageai.com'],
68
+ REPLICATE_API_TOKEN: ['api.replicate.com'],
69
+ ELEVENLABS_API_KEY: ['api.elevenlabs.io'],
70
+ TAVILY_API_KEY: ['api.tavily.com'],
71
+ EXA_API_KEY: ['api.exa.ai'],
72
+ FIRECRAWL_API_KEY: ['api.firecrawl.dev'],
73
+ BRAVE_API_KEY: ['api.search.brave.com'],
74
+ HF_TOKEN: ['huggingface.co', 'router.huggingface.co'],
75
+ HUGGING_FACE_HUB_TOKEN: ['huggingface.co', 'router.huggingface.co'],
76
+ NPM_TOKEN: ['registry.npmjs.org'],
77
+ VERCEL_TOKEN: ['api.vercel.com'],
78
+ NETLIFY_AUTH_TOKEN: ['api.netlify.com'],
79
+ CLOUDFLARE_API_TOKEN: ['api.cloudflare.com'],
80
+ SUPABASE_ACCESS_TOKEN: ['api.supabase.com'],
81
+ RENDER_API_KEY: ['api.render.com'],
82
+ NEON_API_KEY: ['console.neon.tech'],
83
+ SENTRY_AUTH_TOKEN: ['sentry.io'],
84
+ LINEAR_API_KEY: ['api.linear.app'],
85
+ GITLAB_TOKEN: ['gitlab.com'],
86
+ DIGITALOCEAN_ACCESS_TOKEN: ['api.digitalocean.com'],
87
+ }),
88
+ );
85
89
 
86
90
  // Reserved Secret variable names (internal/secrets/validate.go).
87
91
  const RESERVED = new Set([
@@ -206,28 +210,6 @@ export function configPathOK(p) {
206
210
  return p.split('/').every((c) => c !== '' && c !== '.' && !c.startsWith('..'));
207
211
  }
208
212
 
209
- export function deliveryPolicy(delivery, hosts = []) {
210
- if (!['env', 'proxy'].includes(delivery) || !Array.isArray(hosts)) throw new Error('delivery must be env or proxy');
211
- if (delivery === 'env' && hosts.length) throw new Error('env delivery cannot enforce hosts; use -');
212
- if (delivery === 'proxy' && (!hosts.length || hosts.length > 32))
213
- throw new Error('proxy delivery requires 1-32 exact hosts');
214
- const normalized = hosts.map((name) => {
215
- if (typeof name !== 'string') throw new Error('hosts must be exact public DNS names');
216
- const h = name.toLowerCase().replace(/\.$/, '');
217
- if (
218
- h.length > 253 ||
219
- !h.includes('.') ||
220
- isIP(h) ||
221
- /(?:^|\.)(?:local|localhost)$/.test(h) ||
222
- h.split('.').some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
223
- ) {
224
- throw new Error('hosts must be exact public DNS names without a scheme, port, path, IP address or wildcard');
225
- }
226
- return h;
227
- });
228
- return { delivery, hosts: [...new Set(normalized)].sort() };
229
- }
230
-
231
213
  export function validID(s) {
232
214
  return typeof s === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(s);
233
215
  }
@@ -237,7 +219,6 @@ export function secretIdentity(s) {
237
219
  name: s.name,
238
220
  orgId: s.orgId,
239
221
  version: s.version,
240
- delivery: s.delivery,
241
222
  hosts: [...(s.hosts || [])].sort(),
242
223
  };
243
224
  }
@@ -11,7 +11,8 @@ import {
11
11
  FILE_MAX_VALUE,
12
12
  FILE_MAX_TOTAL,
13
13
  configPathOK,
14
- deliveryPolicy,
14
+ FORMAT,
15
+ TOKEN_HOSTS,
15
16
  human,
16
17
  isMain,
17
18
  sameSecret,
@@ -60,30 +61,29 @@ export class PlanError extends Error {
60
61
  }
61
62
 
62
63
  function checkContext(scan, secrets, orgId) {
63
- if (scan.version !== 3 || !Array.isArray(scan.items))
64
+ if (
65
+ scan.version !== FORMAT ||
66
+ !Array.isArray(scan.items) ||
67
+ scan.items.some((it) => it.kind === 'env' && !TOKEN_HOSTS.has(it.name))
68
+ )
64
69
  throw new PlanError(['scan format changed; run scan.mjs again']);
65
70
  if (!validID(orgId)) throw new PlanError(['an explicit org ID is required']);
66
71
  if (!Array.isArray(secrets) || secrets.some((s) => s.orgId !== orgId || !s.id || !Number.isSafeInteger(s.version)))
67
72
  throw new PlanError(['Secret metadata must belong to the selected org and include IDs and versions']);
68
73
  }
69
74
 
70
- export function writePlan(
71
- scan,
72
- { secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [], delivery = 'env', hosts = [] } = {},
73
- ) {
75
+ export function writePlan(scan, { secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [] } = {}) {
74
76
  checkContext(scan, secrets, orgId);
75
77
  if (sandboxes.some((id) => !validID(id) || !id.startsWith('m_')))
76
78
  throw new PlanError(['sandboxes must use stable m_ IDs']);
77
- const policy = deliveryPolicy(delivery, hosts);
78
79
  const out = [
79
80
  '# instabox basic setup: plan',
80
81
  '',
81
82
  'Check `[x]` the items to save or copy. Do not change IDs or delete lines.',
82
- 'Tokens become encrypted org Secrets. Each Delivery line explicitly chooses env (real values) or proxy (HTTPS-header placeholders with allowed hosts).',
83
- 'This run adds env grants only to the sandbox IDs below; an empty list adds no grants. Existing grants remain in force.',
84
- 'Proxy Secrets also become available to other org sandboxes whose proxy selection is all, including future sandboxes.',
83
+ 'Tokens become encrypted org Secrets. A box sees a placeholder; the outbound proxy puts the token into HTTPS headers sent to the hosts listed for each token, and nowhere else.',
84
+ 'This run selects them only on the sandbox IDs below; an empty list changes no selection. Existing selections remain in force.',
85
+ 'Saved Secrets also become available to other org sandboxes whose selection is all, including future sandboxes.',
85
86
  'Replacing an existing Secret affects its other authorized sandboxes. Config files are copied once to the named running sandboxes and may overwrite those paths; they are not Secrets.',
86
- 'Env values can be copied into files, processes or snapshots; removing a selection cannot recover those copies.',
87
87
  '',
88
88
  `Org: \`${orgId}\``,
89
89
  `Sandboxes: \`${[...new Set(sandboxes)].join(',') || '-'}\``,
@@ -104,7 +104,7 @@ export function writePlan(
104
104
  const checked = it.checked && !other && (it.kind === 'env' || sandboxes.length > 0);
105
105
  let description =
106
106
  it.kind === 'env'
107
- ? `${it.name} from ${it.from}`
107
+ ? `${it.name} from ${it.from}, for ${TOKEN_HOSTS.get(it.name).join(', ')}`
108
108
  : `~/${it.path}: ${it.files.length} file(s), ${human(it.bytes)}`;
109
109
  if (it.reason) description += `; off by default: ${it.reason}`;
110
110
  if (it.note) description += `; ${it.note}`;
@@ -112,12 +112,6 @@ export function writePlan(
112
112
  else if (ex.length) description += '; replaces the unchanged Secret from your previous run';
113
113
  if (it.kind === 'file' && !sandboxes.length) description += '; select running sandbox IDs before checking this';
114
114
  out.push(`- [${checked ? 'x' : ' '}] \`${it.id}\` ${description}`);
115
- if (it.kind === 'env') {
116
- // Preserve an existing mode as a starting point; never silently change
117
- // a proxy credential into a real-value env credential on a repeat run.
118
- const p = ex.length ? deliveryPolicy(ex[0].item.delivery, ex[0].item.hosts || []) : policy;
119
- out.push(`Delivery \`${it.id}\`: \`${p.delivery}\` hosts \`${p.hosts.join(',') || '-'}\``);
120
- }
121
115
  }
122
116
  }
123
117
  out.push('', '## Manual setup', '', 'These entries have no checkbox; no credentials are read for them.', '');
@@ -129,8 +123,7 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
129
123
  checkContext(scan, secrets, orgId);
130
124
  const errors = [];
131
125
  const byId = new Map(scan.items.map((it) => [it.id, it]));
132
- const states = new Map(),
133
- policies = new Map();
126
+ const states = new Map();
134
127
  let planOrg, sandboxes, digest;
135
128
  for (const [index, raw] of text.split('\n').entries()) {
136
129
  const line = raw.replace(/\r$/, ''),
@@ -155,20 +148,6 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
155
148
  }
156
149
  continue;
157
150
  }
158
- if (/^Delivery\b/.test(line)) {
159
- const m = line.match(/^Delivery `([^`]+)`: `([^`]+)` hosts `([^`]*)`$/);
160
- if (!m || byId.get(m[1])?.kind !== 'env') {
161
- errors.push(`line ${n}: invalid Delivery line`);
162
- continue;
163
- }
164
- if (policies.has(m[1])) errors.push(`line ${n}: duplicate delivery for ${m[1]}`);
165
- try {
166
- policies.set(m[1], deliveryPolicy(m[2], m[3] === '-' ? [] : m[3].split(',').map((h) => h.trim())));
167
- } catch (e) {
168
- errors.push(`line ${n}: ${e.message}`);
169
- }
170
- continue;
171
- }
172
151
  if (/^Selector:/.test(line)) errors.push(`line ${n}: legacy selectors are retired`);
173
152
  const d = line.match(/^<!-- plan ([0-9a-f]+):/);
174
153
  if (d) {
@@ -195,27 +174,21 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
195
174
  errors.push('different scan or org Secret state; scan, list Secrets and write a new plan');
196
175
  for (const it of scan.items) {
197
176
  if (!states.has(it.id)) errors.push(`\`${it.id}\` is missing from the plan`);
198
- if (it.kind === 'env' && !policies.has(it.id))
199
- errors.push(`\`${it.id}\` needs an explicit delivery mode and hosts line`);
200
177
  }
201
178
  if (errors.length) throw new PlanError(errors);
202
179
 
203
180
  const picked = scan.items.filter((it) => states.get(it.id));
204
- const ghToken = picked.some(
205
- (it) => it.kind === 'env' && it.name === 'GH_TOKEN' && policies.get(it.id).delivery === 'env',
206
- );
207
181
  const items = picked.map((it) => {
208
182
  const item = { ...it };
209
183
  delete item.checked;
210
184
  if (it.kind === 'env') {
211
- Object.assign(item, policies.get(it.id));
212
185
  item.expected = secrets.find((s) => s.name === it.name);
213
186
  item.expected = item.expected ? secretIdentity(item.expected) : null;
214
187
  } else {
215
188
  if (!sandboxes.length) errors.push(`\`${it.id}\`: config copying requires explicit running sandbox IDs`);
216
189
  item.files = it.files.map((f) => {
217
190
  if (!configPathOK(f.path)) errors.push(`\`${it.id}\`: invalid config path`);
218
- const c = fileContent(f, { home: scan.home, ghToken });
191
+ const c = fileContent(f, { home: scan.home });
219
192
  if (c.error) errors.push(`\`${it.id}\`: ${c.error}`);
220
193
  const bytes = c.error ? 0 : Buffer.byteLength(c.text);
221
194
  if (bytes > FILE_MAX_VALUE) errors.push(`\`${it.id}\`: file exceeds the config-copy limit of 64 KiB`);
@@ -249,11 +222,10 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
249
222
  if (fileBytes > FILE_MAX_TOTAL)
250
223
  throw new PlanError(['config copies exceed 512 KiB; uncheck files or transfer them separately'], 3);
251
224
  return {
252
- version: 3,
225
+ version: FORMAT,
253
226
  home: scan.home,
254
227
  orgId,
255
228
  sandboxes,
256
- ghToken,
257
229
  items,
258
230
  manual: scan.manual,
259
231
  skipped: scan.items.filter((it) => !states.get(it.id)).map((it) => it.id),
@@ -285,14 +257,11 @@ function main() {
285
257
  const secrets = JSON.parse(readFileSync(secretsFile, 'utf8')).items;
286
258
  if (!Array.isArray(secrets)) throw new Error('metadata must be instabox secrets ls --json output');
287
259
  if (command === 'write') {
288
- const sandboxes = [],
289
- hosts = [];
260
+ const sandboxes = [];
290
261
  let value;
291
262
  while ((value = opt('--sandbox')) !== undefined) sandboxes.push(value);
292
- while ((value = opt('--host')) !== undefined) hosts.push(value);
293
- const delivery = opt('--delivery') || 'env';
294
263
  if (args.length) throw new Error('unknown write arguments');
295
- process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes, delivery, hosts }));
264
+ process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes }));
296
265
  } else if (command === 'parse') {
297
266
  const limitFile = opt('--limits');
298
267
  const limits = limitFile ? JSON.parse(readFileSync(limitFile, 'utf8')) : {};
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // Save reviewed org Secrets, select them for named sandboxes, and copy ordinary
3
- // config files separately. Credential values are held only in memory/stdin.
2
+ // Save reviewed org Secrets for their registry hosts, select them for named sandboxes, and copy
3
+ // ordinary config files separately. Credential values are held only in memory/stdin.
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
@@ -13,7 +13,8 @@ import {
13
13
  FILE_MAX_VALUE,
14
14
  FILE_MAX_TOTAL,
15
15
  configPathOK,
16
- deliveryPolicy,
16
+ FORMAT,
17
+ TOKEN_HOSTS,
17
18
  human,
18
19
  isMain,
19
20
  run,
@@ -107,8 +108,8 @@ function validSource(it, home) {
107
108
  }
108
109
 
109
110
  function checkSelection(sel) {
110
- if (sel.version !== 3 || Object.hasOwn(sel, 'selector'))
111
- throw new Error('legacy selection format is retired; scan and approve a new org Secret plan');
111
+ if (sel.version !== FORMAT || Object.hasOwn(sel, 'selector'))
112
+ throw new Error('this selection was approved in an older format; scan and approve a new org Secret plan');
112
113
  if (
113
114
  !validID(sel.orgId) ||
114
115
  !Array.isArray(sel.sandboxes) ||
@@ -124,12 +125,15 @@ function checkSelection(sel) {
124
125
  if (ids.has(it.id)) throw new Error('duplicate selection ID');
125
126
  ids.add(it.id);
126
127
  if (it.kind === 'env') {
127
- if (!secretNameOK(it.name) || names.has(it.name) || !validSource(it, sel.home) || !Object.hasOwn(it, 'expected'))
128
+ if (
129
+ !TOKEN_HOSTS.has(it.name) ||
130
+ !secretNameOK(it.name) ||
131
+ names.has(it.name) ||
132
+ !validSource(it, sel.home) ||
133
+ !Object.hasOwn(it, 'expected')
134
+ )
128
135
  throw new Error('invalid Secret selection or source');
129
136
  names.add(it.name);
130
- const policy = deliveryPolicy(it.delivery, it.hosts);
131
- if (JSON.stringify(policy.hosts) !== JSON.stringify(it.hosts))
132
- throw new Error('hosts must use the normalized policy from the approved plan');
133
137
  } else if (it.kind === 'file') {
134
138
  if (!sel.sandboxes.length || !Array.isArray(it.files))
135
139
  throw new Error('config copying needs explicit sandbox IDs');
@@ -191,7 +195,7 @@ export function push(
191
195
  if (sel.items.some((it) => it.kind === 'file') && box.state !== 'running')
192
196
  throw new Error(`${id}: start the sandbox before copying config files`);
193
197
  const selection = json(['secrets', 'show', id]);
194
- if (!Array.isArray(selection.envSecrets) || (selection.secrets !== 'all' && !Array.isArray(selection.secrets)))
198
+ if (selection.secrets !== 'all' && !Array.isArray(selection.secrets))
195
199
  throw new Error('sandbox returned invalid Secret selections');
196
200
  return box;
197
201
  });
@@ -211,7 +215,6 @@ export function push(
211
215
 
212
216
  const tokens = [],
213
217
  files = [];
214
- const ghToken = sel.items.some((it) => it.kind === 'env' && it.name === 'GH_TOKEN' && it.delivery === 'env');
215
218
  const rcNames = new Map(),
216
219
  rcValues = new Map();
217
220
  for (const it of sel.items)
@@ -229,13 +232,13 @@ export function push(
229
232
  typeof value !== 'string' ||
230
233
  value.includes('\0') ||
231
234
  Buffer.byteLength(value) > SECRET_MAX_VALUE ||
232
- (it.delivery === 'proxy' && /[\x00-\x08\x0a-\x1f\x7f]/.test(value))
235
+ /[\x00-\x08\x0a-\x1f\x7f]/.test(value)
233
236
  )
234
- return fail(`${it.name}: could not prepare a valid ${it.delivery} value; nothing written`);
235
- tokens.push({ item: it, value });
237
+ return fail(`${it.name}: could not prepare a valid header value; nothing written`);
238
+ tokens.push({ item: it, hosts: TOKEN_HOSTS.get(it.name), value });
236
239
  } else {
237
240
  for (const f of it.files) {
238
- const got = fileContent(f, { home: sel.home, ghToken });
241
+ const got = fileContent(f, { home: sel.home });
239
242
  if (got.error || Buffer.byteLength(got.text) > FILE_MAX_VALUE)
240
243
  return fail(`~/${f.path}: config could not be prepared; nothing written`);
241
244
  files.push({ path: f.path, text: got.text });
@@ -267,11 +270,11 @@ export function push(
267
270
 
268
271
  const manifest = readManifest(manifestPath);
269
272
  let ok = true;
270
- for (const { item, value } of tokens) {
273
+ for (const { item, hosts, value } of tokens) {
271
274
  const r = {
272
275
  name: item.name,
273
276
  kind: 'secret',
274
- delivery: item.delivery,
277
+ hosts,
275
278
  bytes: Buffer.byteLength(value),
276
279
  status: dryRun ? 'would save' : 'failed',
277
280
  };
@@ -284,9 +287,7 @@ export function push(
284
287
  item.name,
285
288
  '--org',
286
289
  sel.orgId,
287
- '--delivery',
288
- item.delivery,
289
- ...item.hosts.flatMap((h) => ['--host', h]),
290
+ ...hosts.flatMap((h) => ['--host', h]),
290
291
  '--json',
291
292
  ];
292
293
  const response = run(args, { env, input: value + '\n' });
@@ -298,8 +299,7 @@ export function push(
298
299
  saved.orgId !== sel.orgId ||
299
300
  !saved.id ||
300
301
  !Number.isSafeInteger(saved.version) ||
301
- saved.delivery !== item.delivery ||
302
- JSON.stringify(deliveryPolicy(saved.delivery, saved.hosts || []).hosts) !== JSON.stringify(item.hosts)
302
+ JSON.stringify(unique(saved.hosts || [])) !== JSON.stringify(unique(hosts))
303
303
  )
304
304
  throw new Error();
305
305
  r.status = 'saved';
@@ -310,7 +310,7 @@ export function push(
310
310
  }
311
311
  }
312
312
  log(
313
- `${r.status} ${item.name} (${item.delivery}${item.hosts.length ? ', ' + item.hosts.join(',') : ''}, ${r.bytes} bytes) in ${sel.orgId}`,
313
+ `${r.status} ${item.name} (for ${hosts.join(',')}, ${r.bytes} bytes) in ${sel.orgId}`,
314
314
  );
315
315
  }
316
316
  if (!dryRun && tokens.length) saveManifest(manifestPath, manifest);
@@ -333,17 +333,14 @@ export function push(
333
333
  try {
334
334
  if (tokens.length) {
335
335
  const current = json(['secrets', 'show', box.id]);
336
- const envNames = tokens.filter((t) => t.item.delivery === 'env').map((t) => t.item.name);
337
- const proxyNames = tokens.filter((t) => t.item.delivery === 'proxy').map((t) => t.item.name);
338
- const flags = [];
339
- if (envNames.length) flags.push('--env-secrets', JSON.stringify(unique([...current.envSecrets, ...envNames])));
340
- if (proxyNames.length && current.secrets !== 'all')
341
- flags.push('--secrets', JSON.stringify(unique([...current.secrets, ...proxyNames])));
342
- if (flags.length) json(['secrets', 'select', box.id, ...flags]);
336
+ if (current.secrets !== 'all') {
337
+ const names = unique([...current.secrets, ...tokens.map((t) => t.item.name)]);
338
+ json(['secrets', 'select', box.id, '--secrets', JSON.stringify(names)]);
339
+ }
343
340
  if (box.state === 'running') {
344
341
  const refreshed = json(['secrets', 'push', box.id]);
345
342
  if (refreshed.guest?.state !== 'applied')
346
- throw new Error('guest delivery is pending or failed; inspect secrets show and retry');
343
+ throw new Error('the box has not applied the Secret selection; inspect secrets show and retry');
347
344
  }
348
345
  }
349
346
  if (files.length) {
@@ -377,7 +374,7 @@ export function push(
377
374
  }
378
375
  result.status = 'configured';
379
376
  log(
380
- `configured ${box.id}${box.state === 'running' ? '; existing application processes may need a restart' : '; Secret delivery waits for the next start'}`,
377
+ `configured ${box.id}${box.state === 'running' ? '; existing application processes may need a restart' : '; the Secret selection applies at the next start'}`,
381
378
  );
382
379
  } catch (e) {
383
380
  ok = false;