@ours.network/install 1.2.1-nightly.6 → 1.2.1-nightly.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -295,3 +295,22 @@ deletion is required; repeat the original setup with the updated installer.
295
295
  The repair runs isolated verification containers without state mounts or network
296
296
  access. Other image verification failures stop setup without replacing the image.
297
297
  An interrupted repair can be retried, including after the image tag was replaced.
298
+
299
+ ### Remote clients over HTTPS
300
+
301
+ A client may select an `https://` origin backed by a TLS reverse proxy. Keep the
302
+ daemon listener private and configure the proxy separately with a certificate
303
+ trusted by the client and matching the hostname. Node uses its normal trust
304
+ store; a private CA may be supplied through `NODE_EXTRA_CA_CERTS` before starting
305
+ the client. Certificate verification must remain enabled.
306
+
307
+ The existing issued client credential works over either transport; changing the
308
+ URL scheme does not require a new credential. Keep the server's master on the
309
+ server. Forward `x-ours-api-token` and the `x-ours-*` session headers unchanged,
310
+ and support streamed request/response bodies and long polling. Client requests
311
+ refuse redirects, including HTTPS-to-HTTP redirects: configure the final HTTPS
312
+ origin directly. UUID/capability checks still precede credential-bearing calls.
313
+
314
+ This adds client HTTPS support, not an HTTPS daemon listener, certificate
315
+ provisioning or automatic reverse-proxy configuration. Existing local HTTP and
316
+ SSH-tunnel profiles continue to work.
@@ -4285,9 +4285,9 @@
4285
4285
  }
4286
4286
  },
4287
4287
  "node_modules/lru-cache": {
4288
- "version": "11.5.2",
4289
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
4290
- "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
4288
+ "version": "11.5.3",
4289
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
4290
+ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
4291
4291
  "license": "BlueOak-1.0.0",
4292
4292
  "engines": {
4293
4293
  "node": "20 || >=22"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "channel": "nightly",
4
- "installerVersion": "1.2.1-nightly.6",
4
+ "installerVersion": "1.2.1-nightly.8",
5
5
  "packages": {
6
6
  "@ours.network/sdk": {
7
7
  "version": "3.8.1-nightly.3",
@@ -2,7 +2,7 @@
2
2
  "release": {
3
3
  "schema": 1,
4
4
  "channel": "nightly",
5
- "installerVersion": "1.2.1-nightly.6",
5
+ "installerVersion": "1.2.1-nightly.8",
6
6
  "packages": {
7
7
  "@ours.network/sdk": {
8
8
  "version": "3.8.1-nightly.3",
package/lib/effects.mjs CHANGED
@@ -1155,8 +1155,8 @@ export function networkEffects(effects) {
1155
1155
  },
1156
1156
  async discoverClientProfile(endpoint, credentialPath) {
1157
1157
  const url = new URL(endpoint);
1158
- if (url.protocol !== 'http:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
1159
- throw new Error('Client endpoint must be an HTTP origin');
1158
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
1159
+ throw new Error('Client endpoint must be an HTTP or HTTPS origin');
1160
1160
  const response = await fetch(`${url.origin}/selection`, { redirect: 'error', signal: AbortSignal.timeout(5000) });
1161
1161
  if (!response.ok) throw new Error(`Daemon selection answered HTTP ${response.status}`);
1162
1162
  const selection = await response.json();
@@ -86,6 +86,41 @@ async function perform(effects, dryRun, label, thunk) {
86
86
 
87
87
  const reason = (error) => (error instanceof Error ? error.message : String(error));
88
88
 
89
+ function clientDiagnostic(error) {
90
+ return reason(error)
91
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
92
+ .replace(/\bBearer\s+[^\s,;]+/gi, 'Bearer [redacted]')
93
+ .replace(/((?:api[_-]?key|api[_-]?token|access[_-]?token|refresh[_-]?token|token|password|secret|authorization|credential)\s*["']?\s*[:=]\s*["']?)[^\s"',;]+/gi, '$1[redacted]')
94
+ .replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[redacted]@')
95
+ .replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 700);
96
+ }
97
+
98
+ function clientRetry(effects, imported, integrations) {
99
+ effects.out(info(`Saved server connection: ${imported.configPath}.`));
100
+ if (integrations.includes('fleet') && !imported.settings.fleetSettingsPath) {
101
+ effects.out(info('Saved profile and settings retained. Run ours-install, choose Connect to an existing server, and reuse the saved profile to finish the interactive Fleet configuration.'));
102
+ return;
103
+ }
104
+ const args = ['client', 'install', '--config', imported.configPath, '--integrations', integrations.join(','), '--sources', imported.settings.sourcesPath];
105
+ if (imported.settings.fleetSettingsPath) args.push('--fleet-settings', imported.settings.fleetSettingsPath);
106
+ const command = `ours-install client install ${args.slice(2).map((value, i) => i % 2 === 0 ? value : shellQuote(value)).join(' ')}`;
107
+ effects.out(info(`Saved profile and settings retained; re-run ${command}`));
108
+ }
109
+
110
+ function explainClientFailure(effects, name, row) {
111
+ const label = { codex: 'Codex', 'claude-code': 'Claude Code', fleet: 'Fleet' }[name];
112
+ const command = name === 'claude-code' ? 'claude' : name;
113
+ if (row?.note === 'not installed') {
114
+ effects.out(warn(`${label}: executable not found on PATH. Install ${label} or make its executable available in this shell, then check ${command} --version before retrying.`));
115
+ } else if (row?.manual) {
116
+ effects.out(warn(`${label}: cannot register the plugin automatically — ${clientDiagnostic(row.note)}. Use the real executable (check ${command} --version), or register the displayed local marketplace manually.`));
117
+ } else {
118
+ effects.out(warn(`${label}: ${row?.failedStep ?? 'integration setup'} failed. ${clientDiagnostic(row?.detail ?? row?.note ?? 'The integration did not report successful completion. Review the messages above.')} Fix the reported command error before retrying.`));
119
+ if (row?.failedCommand) effects.out(info(`Failed command: ${row.failedCommand.map(shellQuote).join(' ')}`));
120
+ }
121
+ }
122
+
123
+
89
124
  function semverMajor(version) {
90
125
  const match = /^(?:[~^<>= ]*)(\d+)\./.exec(String(version ?? '').trim());
91
126
  return match ? Number(match[1]) : null;
@@ -193,11 +228,11 @@ async function prepareIncompatibleUpgrade(args, effects, target, cliPkg, mismatc
193
228
  * rather than one harness. So a failure here is one honest line plus whatever
194
229
  * the caller wants to say about retrying, and the walk continues.
195
230
  */
196
- async function attempt(effects, dryRun, label, thunk) {
231
+ async function attempt(effects, dryRun, label, thunk, formatReason = reason) {
197
232
  try {
198
233
  return { ok: true, ...(await perform(effects, dryRun, label, thunk)) };
199
234
  } catch (error) {
200
- effects.out(warn(`${label} — did not complete: ${reason(error)}`));
235
+ effects.out(warn(`${label} — did not complete: ${formatReason(error)}`));
201
236
  return { ok: false, error };
202
237
  }
203
238
  }
@@ -817,6 +852,9 @@ export async function runIdentityPhase(args, effects, { target, mcpReady }) {
817
852
  export async function runHarnessPhase(args, effects, { target, isDefaultStateDir, exactSuite = null }) {
818
853
  effects.out(heading('Harness plugins'));
819
854
  const detected = (await effects.detectHarnesses()).filter(h => !args.clientIntegrations || args.clientIntegrations.includes(h.name));
855
+ for (const name of args.clientIntegrations ?? []) {
856
+ if (name !== 'fleet' && !detected.some(h => h.name === name)) detected.push({ name, status: 'absent' });
857
+ }
820
858
  for (const h of detected) {
821
859
  if (h.status === 'ok') effects.out(ok(`'${h.command ?? h.name}' → ${h.detail ?? 'real program'} (its plugin can be installed)`));
822
860
  else if (h.status === 'alias') effects.out(warn(`'${h.command ?? h.name}' → ${h.detail} (I won't call it — manual steps below)`));
@@ -826,7 +864,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
826
864
  if (detected.every((h) => h.status === 'absent')) {
827
865
  effects.out(info('No Claude Code, Codex or Hermes found — install one and re-run to wire it up.'));
828
866
  effects.out(info('Your daemon is unaffected; nothing else in this run depends on a harness.'));
829
- return [];
867
+ return args.clientIntegrations ? detected.map(h => ({ key: h.name, state: 'skipped', note: 'not installed' })) : [];
830
868
  }
831
869
 
832
870
  const plans = planHarnessPlugins({
@@ -851,7 +889,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
851
889
  if (plan.action === 'manual' && exactSuite?.localPackages?.[plan.name]) {
852
890
  const root = await effects.prepareClientMarketplace(plan.name, exactSuite.localPackages[plan.name]);
853
891
  effects.out(warn(`${plan.label} requires manual registration of exact local marketplace ${root}; ${target.managed ? 'the saved client default is retained' : `keep OURS_CONFIG=${target.configPath}`}.`));
854
- rows.push({ ...row, state: 'failed', note: 'native executable unavailable' });
892
+ rows.push({ ...row, state: 'failed', note: plan.reason, manual: true });
855
893
  continue;
856
894
  }
857
895
  if (plan.action === 'manual') {
@@ -873,7 +911,7 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
873
911
  effects.out(warn(`${plan.label} — ${plan.reason}; install it yourself with:`));
874
912
  for (const step of manual) effects.out(info(` ${step}`));
875
913
  if (plan.envLine) effects.out(info(`Before later native launches, set: ${plan.envLine}`));
876
- rows.push({ ...row, state: 'skipped', note: plan.reason });
914
+ rows.push({ ...row, state: 'skipped', note: plan.reason, manual: true });
877
915
  continue;
878
916
  }
879
917
 
@@ -884,13 +922,16 @@ export async function runHarnessPhase(args, effects, { target, isDefaultStateDir
884
922
  const steps = plan.name === 'codex'
885
923
  ? [['codex', 'plugin', 'marketplace', 'add', registration], ['codex', 'plugin', 'add', 'ours@ours-codex-marketplace']]
886
924
  : [['claude', 'plugin', 'marketplace', 'add', registration], ['claude', 'plugin', await effects.hasClaudePlugin() ? 'update' : 'install', 'ours@ours.network']];
887
- let failed = false;
925
+ let failure = null;
888
926
  for (const step of steps) {
889
- const outcome = await attempt(effects, false, step.join(' '), () => effects.run(step[0], step.slice(1), { env: profileEnv(target) }));
890
- if (!outcome.ok) { failed = true; break; }
927
+ const outcome = await attempt(effects, false, step.join(' '), () => effects.run(step[0], step.slice(1), { env: profileEnv(target) }), clientDiagnostic);
928
+ if (!outcome.ok) {
929
+ failure = { failedCommand: step, failedStep: step[2] === 'marketplace' ? `Register ${plan.label} marketplace` : `Install ${plan.label} plugin`, detail: clientDiagnostic(outcome.error) };
930
+ break;
931
+ }
891
932
  }
892
933
  effects.out(info(target.managed ? `Native ${plan.name} launches use the saved client default.` : `Native ${plan.name} launches must retain OURS_CONFIG=${target.configPath}.`));
893
- rows.push({ ...row, state: failed ? 'failed' : 'installed' });
934
+ rows.push({ ...row, state: failure ? 'failed' : 'installed', ...(failure ?? {}) });
894
935
  continue;
895
936
  }
896
937
 
@@ -1387,7 +1428,7 @@ export async function runClientCommand(command, effects) {
1387
1428
  let profile;
1388
1429
  if (configPath) profile = validateHostProfile(effects.readProfile(configPath));
1389
1430
  else if (!command.preset && effects.interactive) {
1390
- const endpoint = await effects.askLine('Server HTTP endpoint: ', 'http://127.0.0.1:3050');
1431
+ const endpoint = await effects.askLine('Server HTTP or HTTPS endpoint: ', 'http://127.0.0.1:3050');
1391
1432
  const credentialPath = await effects.askLine('Private issued-token file: ', '');
1392
1433
  if (!credentialPath) throw new InstallUsageError('Client setup requires an issued-token file');
1393
1434
  profile = await effects.discoverClientProfile(endpoint, credentialPath);
@@ -1428,29 +1469,36 @@ export async function runClientCommand(command, effects) {
1428
1469
  }
1429
1470
  effects.out(progress(0, 4, 'Client configuration', 'Prepare the selected integrations and private connection profile.'));
1430
1471
  const imported = effects.importClientProfile({ profile, sourcesPath, sources: resolvedSources, integrations, fleetSettingsPath, refresh: !!command.preset });
1472
+ let phase = 'Validate saved server connection';
1431
1473
  try {
1432
1474
  await effects.verifyHostProfile(imported.configPath);
1433
1475
  effects.out(progress(1, 4, 'Client packages', 'Acquire and verify the selected client package versions.'));
1476
+ phase = 'Acquire client packages';
1434
1477
  const exactSuite = await effects.acquireClientPackages(imported.configPath, imported.settings.sourcesPath, integrations, { refresh: !!command.preset });
1435
1478
  const args = { assumeYes: true, dryRun: false, channel: 'latest', clientIntegrations: integrations,
1436
1479
  acquiredFleet: exactSuite.fleetBin, fleetSettingsPath: imported.settings.fleetSettingsPath };
1437
1480
  const target = { mode: 'host-profile', managed: true, configPath: imported.configPath, profile: imported.profile, endpoint: imported.profile.endpoint };
1438
1481
  effects.out(progress(2, 4, 'Client integrations', 'Register the selected agent integrations.'));
1482
+ phase = 'Register client integrations';
1439
1483
  const summary = await runHarnessPhase(args, effects, { target, isDefaultStateDir: false, exactSuite });
1440
1484
  if (integrations.includes('fleet')) {
1441
1485
  effects.out(progress(3, 4, 'Fleet configuration', 'Apply prepared settings or open the selected Fleet wizard.'));
1486
+ phase = 'Configure Fleet';
1442
1487
  summary.push(await runFleetPhase(args, effects, { target, isDefaultStateDir: false }));
1443
1488
  }
1444
1489
  const incomplete = integrations.filter(name => !summary.some(row => row.key === name && row.state === 'installed'));
1445
1490
  if (effects.env.OURS_CONFIG && resolve(effects.env.OURS_CONFIG) !== imported.configPath)
1446
1491
  effects.out(warn('This shell has an explicit OURS_CONFIG override. Unset it for new clients to use the saved default; installer did not edit your shell.'));
1447
- effects.out(incomplete.length
1448
- ? warn(`Client setup incomplete (${incomplete.join(', ')}); saved profile and settings retained. Re-run ours-install client install.`)
1449
- : ok(`Client setup complete. New clients discover ${imported.configPath}; no OURS_CONFIG export is required.`));
1492
+ if (incomplete.length) {
1493
+ effects.out(warn(`Client setup incomplete (${incomplete.join(', ')}). The selected client integrations need attention:`));
1494
+ for (const name of incomplete) explainClientFailure(effects, name, summary.find(row => row.key === name));
1495
+ clientRetry(effects, imported, integrations);
1496
+ } else effects.out(ok(`Client setup complete. New clients discover ${imported.configPath}; no OURS_CONFIG export is required.`));
1450
1497
  if (!incomplete.length) effects.out(progress(4, 4, 'Client setup complete', 'All selected integrations are configured.'));
1451
1498
  return incomplete.length ? EXIT_REFUSED : EXIT_OK;
1452
1499
  } catch (error) {
1453
- effects.out(warn(`Client setup incomplete: ${reason(error)}. Saved profile and settings retained; re-run ours-install client install.`));
1500
+ effects.out(warn(`Client setup incomplete: ${phase} failed — ${clientDiagnostic(error)}. Fix this error before retrying.`));
1501
+ clientRetry(effects, imported, integrations);
1454
1502
  return EXIT_REFUSED;
1455
1503
  }
1456
1504
  }
package/lib/target.mjs CHANGED
@@ -85,15 +85,15 @@ export function validateHostProfile(value) {
85
85
  const mixed = LEGACY_PROFILE_KEYS.filter((key) => Object.hasOwn(value, key));
86
86
  if (mixed.length) throw profileError(`legacy selection keys cannot be mixed with a host profile (${mixed.join(', ')}).`);
87
87
  const { endpoint, expectedInstanceId, credentialPath } = value;
88
- if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP origin.');
88
+ if (typeof endpoint !== 'string' || endpoint.trim() !== endpoint || endpoint === '') throw profileError('endpoint must be a non-empty HTTP or HTTPS origin.');
89
89
  if (typeof expectedInstanceId !== 'string' || !PROFILE_UUID.test(expectedInstanceId)) throw profileError('expectedInstanceId must be a lowercase UUID.');
90
90
  if (typeof credentialPath !== 'string' || credentialPath === '' || !credentialPath.startsWith('/') || resolve(credentialPath) !== credentialPath) {
91
91
  throw profileError('credentialPath must be a normalized absolute path.');
92
92
  }
93
93
  let url;
94
- try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP origin.'); }
95
- if (url.protocol !== 'http:' || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
96
- throw profileError('endpoint must be an HTTP origin without credentials, path, query, or fragment.');
94
+ try { url = new URL(endpoint); } catch { throw profileError('endpoint must be an HTTP or HTTPS origin.'); }
95
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
96
+ throw profileError('endpoint must be an HTTP or HTTPS origin without credentials, path, query, or fragment.');
97
97
  }
98
98
  return { endpoint: url.origin, expectedInstanceId, credentialPath };
99
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "1.2.1-nightly.6",
3
+ "version": "1.2.1-nightly.8",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet initialization, harness plugins, Human identity, progress UI, and guided next steps.",
6
6
  "type": "module",