@pellux/goodvibes-daemon 1.28.19 → 1.28.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -63
- package/README.md +24 -19
- package/bin/launcher-support.js +5 -5
- package/package.json +6 -4
- package/scripts/postinstall.js +8 -8
- package/src/cli/command-catalog.ts +22 -22
- package/src/cli/completion.ts +4 -4
- package/src/cli/help.ts +5 -5
- package/src/cli/index.ts +3 -3
- package/src/cli/parser.ts +2 -2
- package/src/cli/surface-catalog.ts +1 -1
- package/src/cli/types.ts +2 -2
- package/src/cluster/daemon-ws-call.ts +5 -5
- package/src/cluster/raw-reply-route.ts +5 -5
- package/src/config/checkpoint-settings.ts +7 -7
- package/src/config/config-key-guard.ts +22 -0
- package/src/config/run-daemon-config-migration.ts +3 -3
- package/src/config/secret-config.ts +7 -7
- package/src/config/surface.ts +3 -3
- package/src/core/pairing-banner.ts +5 -5
- package/src/daemon/cli.ts +45 -43
- package/src/daemon/config-command.ts +15 -15
- package/src/daemon/handlers/context.ts +1 -1
- package/src/daemon/handlers/contracts.ts +19 -4
- package/src/daemon/handlers/credentials.ts +1 -1
- package/src/daemon/handlers/drafts/draft-store.ts +3 -3
- package/src/daemon/handlers/drafts/register.ts +4 -4
- package/src/daemon/handlers/inbox/aggregator.ts +8 -8
- package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
- package/src/daemon/handlers/inbox/index.ts +7 -7
- package/src/daemon/handlers/inbox/mapping.ts +2 -2
- package/src/daemon/handlers/inbox/poller.ts +5 -5
- package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
- package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
- package/src/daemon/handlers/inbox/providers/email.ts +3 -3
- package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
- package/src/daemon/handlers/index.ts +18 -8
- package/src/daemon/handlers/payments/address-store.ts +54 -0
- package/src/daemon/handlers/payments/budget-store.ts +356 -0
- package/src/daemon/handlers/payments/card-store.ts +486 -0
- package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
- package/src/daemon/handlers/payments/index.ts +38 -0
- package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
- package/src/daemon/handlers/payments/notifier.ts +112 -0
- package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
- package/src/daemon/handlers/payments/register.ts +518 -0
- package/src/daemon/handlers/register.ts +3 -3
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
- package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
- package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
- package/src/daemon/handlers/remote/backends/types.ts +2 -2
- package/src/daemon/handlers/remote/dispatcher.ts +3 -3
- package/src/daemon/handlers/remote/index.ts +1 -1
- package/src/daemon/handlers/remote/peer-registry.ts +62 -13
- package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
- package/src/daemon/handlers/routing/index.ts +1 -1
- package/src/daemon/handlers/routing/route-store.ts +1 -1
- package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
- package/src/daemon/handlers/sqlite-store.ts +9 -9
- package/src/daemon/handlers/triage/index.ts +1 -1
- package/src/daemon/handlers/triage/integration.ts +3 -3
- package/src/daemon/handlers/triage/pipeline.ts +2 -2
- package/src/daemon/handlers/triage/scorer.ts +2 -2
- package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
- package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
- package/src/daemon/handlers/triage/tagger/index.ts +1 -1
- package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
- package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
- package/src/daemon/handlers/triage/types.ts +2 -2
- package/src/daemon/lifecycle.ts +5 -5
- package/src/daemon/local-daemon-state.ts +7 -7
- package/src/daemon/pair-command.ts +14 -14
- package/src/daemon/provision-wake-model.ts +5 -5
- package/src/daemon/send/channels.ts +7 -7
- package/src/daemon/send/command.ts +11 -11
- package/src/daemon/send/composition.ts +5 -5
- package/src/daemon/send/failure-text.ts +6 -6
- package/src/daemon/send/inert-text.ts +18 -18
- package/src/daemon/send/stdin.ts +3 -3
- package/src/daemon/service-commands.ts +32 -32
- package/src/daemon/sessions-command.ts +7 -7
- package/src/daemon/status-command.ts +22 -22
- package/src/daemon/webui-command.ts +14 -14
- package/src/runtime/boot-tasks.ts +1 -1
- package/src/runtime/browser-checkout-seam-holder.ts +55 -0
- package/src/runtime/cluster-composition.ts +9 -9
- package/src/runtime/cluster-group-composition.ts +7 -7
- package/src/runtime/conversation-rewind-port.ts +8 -8
- package/src/runtime/credential-composition.ts +2 -2
- package/src/runtime/daemon-handler-composition.ts +61 -4
- package/src/runtime/device-posture-composition.ts +10 -10
- package/src/runtime/disposal-wiring.ts +8 -8
- package/src/runtime/fleet-needs-input-push.ts +4 -4
- package/src/runtime/fleet-services.ts +1 -1
- package/src/runtime/hosted-session-composition.ts +13 -13
- package/src/runtime/index.ts +1 -1
- package/src/runtime/knowledge-services.ts +2 -2
- package/src/runtime/legacy-daemon-migration.ts +43 -43
- package/src/runtime/legacy-daemon-reconcile.ts +30 -30
- package/src/runtime/mail-composition.ts +6 -6
- package/src/runtime/notification-dispatch.ts +7 -7
- package/src/runtime/payments-composition.ts +187 -0
- package/src/runtime/plugin-composition.ts +7 -7
- package/src/runtime/runtime-services-types.ts +9 -9
- package/src/runtime/services.ts +41 -32
- package/src/runtime/trigger-services.ts +1 -1
- package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
- package/src/runtime/trust/trust-gated-approvals.ts +9 -9
- package/src/runtime/update-check.ts +4 -4
- package/src/runtime/workspace-checkpointing.ts +6 -6
- package/src/testing/daemon-fixture.ts +11 -11
- package/src/testing/hosted-session-failures.ts +4 -4
- package/src/version.ts +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Service-lifecycle subcommands
|
|
2
|
+
* Service-lifecycle subcommands, `goodvibes-daemon install-service |
|
|
3
3
|
* uninstall-service | service-status`.
|
|
4
4
|
*
|
|
5
5
|
* The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
|
|
11
11
|
* bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
|
|
12
|
-
* REAL wired service machinery
|
|
13
|
-
* `/api/service/*` routes via facade-composition.ts
|
|
12
|
+
* REAL wired service machinery, reached in production by the daemon's own HTTP
|
|
13
|
+
* `/api/service/*` routes via facade-composition.ts, is
|
|
14
14
|
* `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
|
|
15
15
|
* systemd/launchd/windows-aware manager with install/uninstall/status/start/
|
|
16
16
|
* stop/restart and a `suggestedCommands` hint list. This module now rewires the
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
* honestly queries systemd via the injected actionRunner) BEFORE the
|
|
39
39
|
* legacy unit is stopped, disabled, or removed. A failed or unhealthy new
|
|
40
40
|
* unit rolls itself back (uninstalled) and never touches the legacy one
|
|
41
|
-
*
|
|
41
|
+
* , a botched takeover must never cost the user their working daemon.
|
|
42
42
|
* - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
|
|
43
43
|
* something is already listening on the configured host:port (Mike's real
|
|
44
44
|
* dev-host case: a manual `nohup`'d daemon with no unit at all), this is
|
|
45
|
-
* an unidentified process, not a managed unit
|
|
45
|
+
* an unidentified process, not a managed unit, there is nothing to stop
|
|
46
46
|
* or disable, and this module will not attempt to kill it. It warns and
|
|
47
47
|
* leaves the decision to the operator.
|
|
48
48
|
* - Every action (legacy stop/disable, unit-file removal, daemon-reload)
|
|
49
49
|
* goes through the SAME injectable `actionRunner`/`legacyUnitFileRemove`
|
|
50
|
-
* seams tests already use
|
|
50
|
+
* seams tests already use, this module never has a code path that bypasses
|
|
51
51
|
* them, so the migration is exercised deterministically via fakes and never
|
|
52
52
|
* touches a real running service in tests.
|
|
53
53
|
*/
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
type ManagedServiceActionRunner,
|
|
68
68
|
} from '../runtime/legacy-daemon-migration.ts';
|
|
69
69
|
|
|
70
|
-
// `resolveInstalledDaemonBinary` lives in the runtime module
|
|
70
|
+
// `resolveInstalledDaemonBinary` lives in the runtime module, re-exported
|
|
71
71
|
// here so this module stays the CLI's stable public surface (and so existing
|
|
72
72
|
// test imports keep working).
|
|
73
73
|
export {
|
|
@@ -83,7 +83,7 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
|
|
|
83
83
|
// literal name `goodvibes-daemon.service`. This module (rewired onto
|
|
84
84
|
// PlatformServiceManager, see the file banner above) manages a DIFFERENT
|
|
85
85
|
// unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
|
|
86
|
-
// unit installed
|
|
86
|
+
// unit installed, this dev machine included, would otherwise see
|
|
87
87
|
// service-status honestly report installed:false/running:false for the
|
|
88
88
|
// tracked name while the legacy unit keeps running untouched underneath it,
|
|
89
89
|
// uninstall-service would silently leave the legacy unit orphaned with no
|
|
@@ -94,13 +94,13 @@ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migrat
|
|
|
94
94
|
// migration engine (`runLegacyDaemonMigration`) both live in
|
|
95
95
|
// `../runtime/legacy-daemon-migration.ts` rather than here, because this
|
|
96
96
|
// daemon's own boot-time reconcile (`../runtime/legacy-daemon-reconcile.ts`)
|
|
97
|
-
// needs them too
|
|
97
|
+
// needs them too, the entrypoint-agnostic `runtime` layer is what both of
|
|
98
98
|
// this repository's consumers can import, so this CLI module is just one of
|
|
99
99
|
// them. The terminal app's own onboarding guided UX solves the same problem
|
|
100
100
|
// independently, under its own architecture constraints.
|
|
101
101
|
//
|
|
102
102
|
// This detection is entirely independent of PlatformServiceManager's own
|
|
103
|
-
// status()
|
|
103
|
+
// status(), it does not rely on (or get invalidated by) the parallel SDK
|
|
104
104
|
// fix that makes status().running itself query systemd honestly for the
|
|
105
105
|
// TRACKED unit name.
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
@@ -125,7 +125,7 @@ export function isDaemonServiceSubcommand(value: string | undefined): value is D
|
|
|
125
125
|
* Exit codes `service-status` reports, so a script never parses prose.
|
|
126
126
|
*
|
|
127
127
|
* 0/3/4 are the three answers the question actually has; 1 stays what it has
|
|
128
|
-
* always been
|
|
128
|
+
* always been, the platform refused the query and the error is printed.
|
|
129
129
|
*/
|
|
130
130
|
export const SERVICE_STATUS_EXIT_RUNNING = 0;
|
|
131
131
|
export const SERVICE_STATUS_EXIT_INSTALLED_NOT_RUNNING = 3;
|
|
@@ -134,10 +134,10 @@ export const SERVICE_STATUS_EXIT_NOT_INSTALLED = 4;
|
|
|
134
134
|
export interface DaemonServiceCliInput {
|
|
135
135
|
readonly subcommand: DaemonServiceSubcommand;
|
|
136
136
|
readonly binaryPath: string;
|
|
137
|
-
/** The GoodVibes tree home (GOODVIBES_HOME-overridable)
|
|
137
|
+
/** The GoodVibes tree home (GOODVIBES_HOME-overridable), config/state root only, never unit-path resolution. See `unitHomeDir`. */
|
|
138
138
|
readonly homeDir: string;
|
|
139
139
|
/**
|
|
140
|
-
* The LOGIN user's home
|
|
140
|
+
* The LOGIN user's home, where the systemd/launchd/Windows unit actually
|
|
141
141
|
* lives, regardless of any GOODVIBES_HOME/GOODVIBES_DAEMON_HOME override.
|
|
142
142
|
* Required (no default to `homeDir`) so a unit-management subcommand can
|
|
143
143
|
* never silently search for its unit under a relocated tree home instead of
|
|
@@ -148,14 +148,14 @@ export interface DaemonServiceCliInput {
|
|
|
148
148
|
readonly port: number;
|
|
149
149
|
/**
|
|
150
150
|
* Whether the invoking CLI line carried an explicit `--hostname`/`--port`
|
|
151
|
-
* flag (not merely whether `host`/`port` above are set
|
|
151
|
+
* flag (not merely whether `host`/`port` above are set, those are always
|
|
152
152
|
* set, resolved from config either way). `install-service`/`migrate-service`
|
|
153
|
-
* refuse when either is true
|
|
153
|
+
* refuse when either is true, see `validateServiceEndpointFlags`. Defaults
|
|
154
154
|
* to false (no flag), matching every subcommand these two don't apply to.
|
|
155
155
|
*/
|
|
156
156
|
readonly hostnameFlagProvided?: boolean | undefined;
|
|
157
157
|
readonly portFlagProvided?: boolean | undefined;
|
|
158
|
-
/** Defaults to `homeDir
|
|
158
|
+
/** Defaults to `homeDir`, overridable so tests can scope both to one tempdir. */
|
|
159
159
|
readonly workingDirectory?: string | undefined;
|
|
160
160
|
/** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
|
|
161
161
|
readonly configManager?: ConfigManager | undefined;
|
|
@@ -166,7 +166,7 @@ export interface DaemonServiceCliInput {
|
|
|
166
166
|
/**
|
|
167
167
|
* `migrate-service` only: explicit consent to actually execute the
|
|
168
168
|
* migration (wired from the CLI's `-y`/`--yes` flag). Without it, the
|
|
169
|
-
* subcommand prints the exact plan and changes nothing
|
|
169
|
+
* subcommand prints the exact plan and changes nothing, never auto-migrate.
|
|
170
170
|
*/
|
|
171
171
|
readonly confirmMigration?: boolean | undefined;
|
|
172
172
|
/**
|
|
@@ -183,7 +183,7 @@ export interface DaemonServiceCliInput {
|
|
|
183
183
|
readonly legacyUnitFileRemove?: ((path: string) => void) | undefined;
|
|
184
184
|
/**
|
|
185
185
|
* `service-status` only: report one JSON document instead of prose. The exit
|
|
186
|
-
* code is the same either way
|
|
186
|
+
* code is the same either way, the codes are the machine-readable answer,
|
|
187
187
|
* and JSON is for when a script wants the fields as well.
|
|
188
188
|
*/
|
|
189
189
|
readonly json?: boolean | undefined;
|
|
@@ -200,7 +200,7 @@ export interface DaemonServiceCliResult {
|
|
|
200
200
|
/**
|
|
201
201
|
* The manager's definition (`ExecStart` command/args, name, description) is
|
|
202
202
|
* built once, in `../runtime/legacy-daemon-migration.ts`, and shared with the
|
|
203
|
-
* onboarding guided UX
|
|
203
|
+
* onboarding guided UX, see that module's doc comment for why.
|
|
204
204
|
*/
|
|
205
205
|
function buildManager(input: DaemonServiceCliInput): PlatformServiceManager {
|
|
206
206
|
return buildManagedDaemonServiceManager({
|
|
@@ -225,12 +225,12 @@ function statusLines(status: ManagedServiceStatus): string[] {
|
|
|
225
225
|
if (status.pid !== undefined) lines.push(`pid: ${status.pid}`);
|
|
226
226
|
if (status.platform !== 'manual' && status.installed && !status.running) {
|
|
227
227
|
// W3 Finding 4: this used to assert "'running' here only reflects
|
|
228
|
-
// processes this tool started directly"
|
|
228
|
+
// processes this tool started directly", true for the pid-file-only
|
|
229
229
|
// check the (currently linked) SDK still uses, but the parallel SDK
|
|
230
230
|
// batch is making status().running query systemd honestly via
|
|
231
231
|
// `is-active`, which would make that specific claim stale. Drop the
|
|
232
232
|
// claim about HOW running was computed and just offer the escape
|
|
233
|
-
// hatch
|
|
233
|
+
// hatch, true and useful under either SDK version.
|
|
234
234
|
lines.push(
|
|
235
235
|
`note: if this looks wrong, verify directly: ${status.suggestedCommands[status.suggestedCommands.length - 1] ?? 'the platform service-status command'}`,
|
|
236
236
|
);
|
|
@@ -264,7 +264,7 @@ export function buildInstallResultLines(status: ManagedServiceStatus): string[]
|
|
|
264
264
|
* Every line names the RESOLVED unit (`status.serviceName`, `status.path`) and
|
|
265
265
|
* says what the platform reported afterwards, rather than asserting the verb
|
|
266
266
|
* worked because it was dispatched without throwing. A verb aimed at a service
|
|
267
|
-
* that is not installed says exactly that
|
|
267
|
+
* that is not installed says exactly that, installing it silently would be a
|
|
268
268
|
* different command than the one that was run.
|
|
269
269
|
*/
|
|
270
270
|
export function buildLifecycleResultLines(
|
|
@@ -364,7 +364,7 @@ function ok(action: 'install' | 'uninstall' | 'status', status: ManagedServiceSt
|
|
|
364
364
|
lines.push(`removed the ${status.platform} service at ${status.path}`);
|
|
365
365
|
if (status.platform === 'systemd') {
|
|
366
366
|
lines.push(
|
|
367
|
-
"note: this removes the unit file but does not run `disable
|
|
367
|
+
"note: this removes the unit file but does not run `disable`; run " +
|
|
368
368
|
"`systemctl --user daemon-reload` to clear any stale enablement symlink.",
|
|
369
369
|
);
|
|
370
370
|
}
|
|
@@ -388,12 +388,12 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
|
|
|
388
388
|
* `install-service` and `migrate-service` refuse an explicit
|
|
389
389
|
* `--hostname`/`--port` flag rather than silently accepting it. Both flags are
|
|
390
390
|
* RUNTIME-ONLY overrides applied to this one invocation's in-memory config
|
|
391
|
-
* (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`)
|
|
391
|
+
* (`applyRuntimeEndpointFlagOverrides` in `src/daemon/cli.ts`), but the unit
|
|
392
392
|
* this subcommand writes carries no endpoint flags at all (see
|
|
393
393
|
* `buildManagedDaemonServiceManager`'s ExecStart doc): the daemon always
|
|
394
394
|
* re-resolves `controlPlane.host`/`port`/`hostMode` from PERSISTED settings at
|
|
395
395
|
* boot. A flag accepted here would print/probe a binding the installed unit
|
|
396
|
-
* will never actually have
|
|
396
|
+
* will never actually have, an honest gap, not a cosmetic one, since the
|
|
397
397
|
* printed value is exactly what an operator would expect the running service
|
|
398
398
|
* to bind to. Refusing with a pointer to the persistent config path is
|
|
399
399
|
* strictly better than silently doing nothing with the flag.
|
|
@@ -408,7 +408,7 @@ export function validateServiceEndpointFlags(
|
|
|
408
408
|
.filter((name): name is string => name !== null)
|
|
409
409
|
.join('/');
|
|
410
410
|
return [
|
|
411
|
-
`${subcommand} refused: ${flagNames} only override this process's runtime config
|
|
411
|
+
`${subcommand} refused: ${flagNames} only override this process's runtime config; the installed unit carries ` +
|
|
412
412
|
'no endpoint flags and re-resolves controlPlane.host/port/hostMode from persisted settings at boot, so the ' +
|
|
413
413
|
'override would never take effect there.',
|
|
414
414
|
`Set the persistent binding instead: goodvibes-daemon config set controlPlane.host / controlPlane.port (or ` +
|
|
@@ -419,7 +419,7 @@ export function validateServiceEndpointFlags(
|
|
|
419
419
|
/**
|
|
420
420
|
* `migrate-service`: the guided, consented takeover of the legacy
|
|
421
421
|
* `goodvibes-daemon.service` unit. Thin wrapper over
|
|
422
|
-
* `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`)
|
|
422
|
+
* `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`), see
|
|
423
423
|
* that module for the design constraints (never auto-migrate,
|
|
424
424
|
* new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
|
|
425
425
|
* every action through an injectable seam) and the onboarding UX consumer.
|
|
@@ -461,7 +461,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
461
461
|
case 'install-service': {
|
|
462
462
|
// W3 Finding 4: refuse rather than risk starting a second daemon
|
|
463
463
|
// alongside an already-installed legacy unit. Refuses whenever the
|
|
464
|
-
// legacy unit is present at all (not just when currently active)
|
|
464
|
+
// legacy unit is present at all (not just when currently active),
|
|
465
465
|
// an installed-but-inactive legacy unit can still be enabled and
|
|
466
466
|
// start competing for the same port later, and "never silently
|
|
467
467
|
// start a second daemon" is the bar here, not "never right now."
|
|
@@ -483,7 +483,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
483
483
|
if (installed.actionError) return failed('install', installed);
|
|
484
484
|
const started = manager.start();
|
|
485
485
|
// start()'s actionError (e.g. a platform this manager can't dispatch
|
|
486
|
-
// actions for) doesn't undo the write
|
|
486
|
+
// actions for) doesn't undo the write, report install as ok, but surface
|
|
487
487
|
// the follow-up problem honestly instead of claiming it is running.
|
|
488
488
|
return started.actionError
|
|
489
489
|
? ok('install', { ...started, running: false }, [`could not start it automatically: ${started.actionError}`])
|
|
@@ -496,7 +496,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
496
496
|
const extra: string[] = [];
|
|
497
497
|
if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
|
|
498
498
|
// This command only ever touches the TRACKED unit (whatever name/path
|
|
499
|
-
// actually resolved
|
|
499
|
+
// actually resolved, not necessarily the SERVICE_NAME constant) above,
|
|
500
500
|
// say so explicitly when an install-script unit also exists, so its
|
|
501
501
|
// continued presence is never a silent surprise.
|
|
502
502
|
if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
|
|
@@ -520,8 +520,8 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
520
520
|
};
|
|
521
521
|
}
|
|
522
522
|
const result = ok('status', status, legacy.present ? [legacyUnitNote(legacy, resolveManagedUnitName(status))] : []);
|
|
523
|
-
// The status verb reports; it does not fail. `ok` is still true
|
|
524
|
-
// question was answered
|
|
523
|
+
// The status verb reports; it does not fail. `ok` is still true, the
|
|
524
|
+
// question was answered, while the exit code carries the answer itself
|
|
525
525
|
// (0 running / 3 installed-not-running / 4 not installed), so a script
|
|
526
526
|
// never has to read the prose above.
|
|
527
527
|
return { ...result, exitCode: serviceStatusExitCode(status) };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sessions-command.ts
|
|
2
|
+
* sessions-command.ts, `goodvibes-daemon sessions list|kill <id>`.
|
|
3
3
|
*
|
|
4
4
|
* The sessions this daemon HOSTS: conversation loops running inside it, which
|
|
5
5
|
* outlive the client that started them. That is the whole reason they need a
|
|
6
|
-
* command
|
|
6
|
+
* command, a terminal's own session dies with the terminal and never needs
|
|
7
7
|
* listing from outside, while a hosted one can be running on a headless box
|
|
8
8
|
* with nothing attached to it at all.
|
|
9
9
|
*
|
|
10
10
|
* The verbs are `sessions.hosted.list` and `sessions.hosted.kill`, and both are
|
|
11
|
-
* declared ws-only in the method catalog
|
|
11
|
+
* declared ws-only in the method catalog, they have no REST binding, so they
|
|
12
12
|
* go through `callDaemonWsVerb` rather than `callDaemonVerb`. Same target, same
|
|
13
13
|
* operator token, same --host/--port/--token convention; only the transport is
|
|
14
14
|
* different. A daemon built without hosted sessions answers "does not know the
|
|
@@ -62,7 +62,7 @@ export interface SessionsCommandFlags extends RemoteCommandFlags {
|
|
|
62
62
|
|
|
63
63
|
export interface RunSessionsCommandInput extends RemoteCommandDeps {
|
|
64
64
|
readonly flags: SessionsCommandFlags;
|
|
65
|
-
/** Positional words after `sessions
|
|
65
|
+
/** Positional words after `sessions`, the subcommand and its argument. */
|
|
66
66
|
readonly args: readonly string[];
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -119,7 +119,7 @@ function renderSession(session: HostedSessionRecord, now: number): string[] {
|
|
|
119
119
|
*
|
|
120
120
|
* Exit 0 when the daemon answered, 1 when it refused or could not be reached,
|
|
121
121
|
* 2 when the command line was wrong. A `kill` with no id is a usage refusal
|
|
122
|
-
* rather than a "kill everything"
|
|
122
|
+
* rather than a "kill everything", there is no shape of this command that ends
|
|
123
123
|
* more than the one session named.
|
|
124
124
|
*/
|
|
125
125
|
export async function runSessionsCommand(input: RunSessionsCommandInput): Promise<DaemonCommandResult> {
|
|
@@ -129,11 +129,11 @@ export async function runSessionsCommand(input: RunSessionsCommandInput): Promis
|
|
|
129
129
|
return refusal('name what to do with the sessions.', flags.json);
|
|
130
130
|
}
|
|
131
131
|
if (!isSessionsSubcommand(subcommand)) {
|
|
132
|
-
return refusal(`'${subcommand}' is not a sessions command
|
|
132
|
+
return refusal(`'${subcommand}' is not a sessions command; try list or kill.`, flags.json);
|
|
133
133
|
}
|
|
134
134
|
const sessionId = args[1];
|
|
135
135
|
if (subcommand === 'kill' && sessionId === undefined) {
|
|
136
|
-
return refusal('kill needs the session to end
|
|
136
|
+
return refusal('kill needs the session to end; run `sessions list` to see them.', flags.json);
|
|
137
137
|
}
|
|
138
138
|
if (args.length > (subcommand === 'kill' ? 2 : 1)) {
|
|
139
139
|
return refusal(`'${args[subcommand === 'kill' ? 2 : 1]}' is one argument too many.`, flags.json);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* status-command.ts
|
|
2
|
+
* status-command.ts, `goodvibes-daemon status` and `goodvibes-daemon update`.
|
|
3
3
|
*
|
|
4
4
|
* The question a headless box's operator asks first: is it up, what version, on
|
|
5
5
|
* what address, is anything unhealthy, and what did it do to itself while I was
|
|
6
|
-
* not looking. Before this, the binary answered none of that
|
|
6
|
+
* not looking. Before this, the binary answered none of that, `status` fell
|
|
7
7
|
* through the parser and started a SECOND daemon in the foreground.
|
|
8
8
|
*
|
|
9
9
|
* WHERE EACH LINE COMES FROM
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* established: `/status` for identity, `/api/health` for the health roll-up and
|
|
14
14
|
* the address it actually bound, `/api/channels/status` for the channels, and
|
|
15
15
|
* `/api/cluster/status` for this machine's place in its group. Hosted sessions
|
|
16
|
-
* are a ws-only verb family, so they go through `callDaemonWsVerb
|
|
16
|
+
* are a ws-only verb family, so they go through `callDaemonWsVerb`, same
|
|
17
17
|
* target, same token, different transport.
|
|
18
18
|
*
|
|
19
|
-
* Everything about the daemon's own HISTORY
|
|
20
|
-
* the version an automatic rollback rejected
|
|
19
|
+
* Everything about the daemon's own HISTORY, uptime, the receipts it wrote,
|
|
20
|
+
* the version an automatic rollback rejected, comes from files on the daemon's
|
|
21
21
|
* host, because no verb reports them. That makes those lines local-only, and
|
|
22
22
|
* they say so for a remote target instead of being guessed at.
|
|
23
23
|
*
|
|
@@ -76,8 +76,8 @@ export interface RemoteCommandDeps {
|
|
|
76
76
|
*
|
|
77
77
|
* It carries a `cluster` block the operator contract does not list, and that
|
|
78
78
|
* block is where this daemon's ROLE in its group comes from. `cluster.uptimeMs`
|
|
79
|
-
* is the coordinator's, not the daemon's
|
|
80
|
-
* up for hours
|
|
79
|
+
* is the coordinator's, not the daemon's, it reads 0 on a daemon that has been
|
|
80
|
+
* up for hours, so the uptime line comes from the lifecycle marker instead.
|
|
81
81
|
*/
|
|
82
82
|
interface ControlStatusPayload {
|
|
83
83
|
readonly status?: string;
|
|
@@ -179,7 +179,7 @@ function optionalLine(label: string, value: string | undefined): string[] {
|
|
|
179
179
|
*
|
|
180
180
|
* `/status` reports `version` from the platform package while the cluster block
|
|
181
181
|
* it carries reports the DAEMON build's version, and against a live daemon
|
|
182
|
-
* those disagreed
|
|
182
|
+
* those disagreed, 1.21.0 against 1.28.0. Printing one of them silently would
|
|
183
183
|
* put a number on this page that is wrong for whichever question the reader had
|
|
184
184
|
* in mind, so both are printed and labelled until the daemon states one.
|
|
185
185
|
*/
|
|
@@ -210,7 +210,7 @@ function localStateLines(state: LocalDaemonState): string[] {
|
|
|
210
210
|
}
|
|
211
211
|
if (state.marker?.rejectedVersion !== undefined) {
|
|
212
212
|
lines.push(
|
|
213
|
-
` rejected: ${state.marker.rejectedVersion} crash looped and was rolled back
|
|
213
|
+
` rejected: ${state.marker.rejectedVersion} crash looped and was rolled back; `
|
|
214
214
|
+ 'the update loop will not install that version again',
|
|
215
215
|
);
|
|
216
216
|
}
|
|
@@ -229,7 +229,7 @@ function localStateLines(state: LocalDaemonState): string[] {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
function healthLines(outcome: DaemonVerbOutcome<HealthPayload>): string[] {
|
|
232
|
-
if (!outcome.ok) return [` health: could not read
|
|
232
|
+
if (!outcome.ok) return [` health: could not read: ${outcome.error}`];
|
|
233
233
|
const health = outcome.data;
|
|
234
234
|
const lines = [` health: ${health.overall ?? 'unknown'}`];
|
|
235
235
|
const binding = health.network?.controlPlane;
|
|
@@ -250,12 +250,12 @@ function healthLines(outcome: DaemonVerbOutcome<HealthPayload>): string[] {
|
|
|
250
250
|
*
|
|
251
251
|
* Only a channel that is switched ON and not healthy is named. Every channel
|
|
252
252
|
* the daemon knows about appears in this payload, and a daemon with one
|
|
253
|
-
* configured channel ships sixteen more in state `disabled
|
|
253
|
+
* configured channel ships sixteen more in state `disabled`, listing those as
|
|
254
254
|
* problems produced a seventeen-line wall under a healthy daemon and buried the
|
|
255
255
|
* one line that meant something.
|
|
256
256
|
*/
|
|
257
257
|
function channelLines(outcome: DaemonVerbOutcome<ChannelsPayload>): string[] {
|
|
258
|
-
if (!outcome.ok) return [` channels: could not read
|
|
258
|
+
if (!outcome.ok) return [` channels: could not read: ${outcome.error}`];
|
|
259
259
|
const channels = outcome.data.channels ?? [];
|
|
260
260
|
if (channels.length === 0) return [' channels: none configured'];
|
|
261
261
|
const on = channels.filter((channel) => channel.enabled !== false);
|
|
@@ -276,7 +276,7 @@ function channelLines(outcome: DaemonVerbOutcome<ChannelsPayload>): string[] {
|
|
|
276
276
|
* Two sources, because neither answers the whole question: `/status` carries
|
|
277
277
|
* the ROLE this node currently holds, and `/api/cluster/status` carries the
|
|
278
278
|
* GROUP it holds that role in. A daemon with sharing switched off says so and
|
|
279
|
-
* stops
|
|
279
|
+
* stops, a role inside no group is not information.
|
|
280
280
|
*/
|
|
281
281
|
function clusterLines(
|
|
282
282
|
identity: ControlStatusPayload,
|
|
@@ -284,18 +284,18 @@ function clusterLines(
|
|
|
284
284
|
): string[] {
|
|
285
285
|
const role = identity.cluster?.role;
|
|
286
286
|
if (identity.cluster?.enabled === false) {
|
|
287
|
-
return [' cluster: off
|
|
287
|
+
return [' cluster: off, this machine handles its own inbound work'];
|
|
288
288
|
}
|
|
289
289
|
if (!outcome.ok) {
|
|
290
290
|
return [
|
|
291
291
|
role === undefined
|
|
292
|
-
? ` cluster: could not read
|
|
293
|
-
: ` cluster: ${role} (the group view could not be read
|
|
292
|
+
? ` cluster: could not read: ${outcome.error}`
|
|
293
|
+
: ` cluster: ${role} (the group view could not be read: ${outcome.error})`,
|
|
294
294
|
];
|
|
295
295
|
}
|
|
296
296
|
const cluster = outcome.data;
|
|
297
297
|
if (cluster.membership === 'no-group') {
|
|
298
|
-
return [' cluster: in no group yet
|
|
298
|
+
return [' cluster: in no group yet, `goodvibes-daemon cluster create` starts one'];
|
|
299
299
|
}
|
|
300
300
|
const group = cluster.groupName ?? cluster.groupId ?? 'its group';
|
|
301
301
|
const members = cluster.memberCount === undefined ? '' : ` of ${cluster.memberCount}`;
|
|
@@ -303,7 +303,7 @@ function clusterLines(
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
function hostedSessionLines(outcome: DaemonVerbOutcome<HostedSessionsPayload>): string[] {
|
|
306
|
-
if (!outcome.ok) return [` sessions: could not read
|
|
306
|
+
if (!outcome.ok) return [` sessions: could not read: ${outcome.error}`];
|
|
307
307
|
const sessions = outcome.data.sessions ?? [];
|
|
308
308
|
return [` sessions: ${sessions.length} hosted by this daemon`];
|
|
309
309
|
}
|
|
@@ -317,7 +317,7 @@ export interface RunStatusCommandInput extends RemoteCommandDeps {
|
|
|
317
317
|
*
|
|
318
318
|
* Exit 0 when the daemon answered its identity call, 1 when it could not be
|
|
319
319
|
* reached. Every other sub-question that fails is one line inside a successful
|
|
320
|
-
* report
|
|
320
|
+
* report, a daemon with a broken channel is up, and saying otherwise would be
|
|
321
321
|
* the kind of wrong that makes an operator distrust the whole page.
|
|
322
322
|
*/
|
|
323
323
|
export async function runStatusCommand(input: RunStatusCommandInput): Promise<DaemonCommandResult> {
|
|
@@ -407,8 +407,8 @@ export interface RunUpdateCommandInput extends RemoteCommandDeps {
|
|
|
407
407
|
* rejected, and whether a rollback is in force.
|
|
408
408
|
*
|
|
409
409
|
* --check is honest about a gap. The daemon runs the whole self-update loop
|
|
410
|
-
* itself
|
|
411
|
-
* binary
|
|
410
|
+
* itself, it checks hourly, swaps at an idle moment and keeps the outgoing
|
|
411
|
+
* binary, but the control plane publishes NO verb to trigger that check early:
|
|
412
412
|
* the operator contract this build was written against has no update method of
|
|
413
413
|
* any kind (no `update.*`, no `admin.update`, nothing under `control.` that
|
|
414
414
|
* checks). Rather than invent a verb this daemon does not answer, --check says
|
|
@@ -439,7 +439,7 @@ export async function runUpdateCommand(input: RunUpdateCommandInput): Promise<Da
|
|
|
439
439
|
'',
|
|
440
440
|
'update --check: this daemon publishes no verb to trigger an update check early.',
|
|
441
441
|
' It checks once an hour on its own and swaps only at an idle moment.',
|
|
442
|
-
' To make it check now, restart it
|
|
442
|
+
' To make it check now, restart it; it checks on the way up:',
|
|
443
443
|
' goodvibes-daemon restart-service',
|
|
444
444
|
]
|
|
445
445
|
: [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// webui-command.ts
|
|
2
|
+
// webui-command.ts, `goodvibes-daemon webui enable|disable|status`.
|
|
3
3
|
//
|
|
4
4
|
// WHAT THIS IS FOR
|
|
5
5
|
//
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// serve such a directory: with `controlPlane.webui.serve` on, its own HTTP
|
|
9
9
|
// router answers `/` from `controlPlane.webui.bundleDir` and falls back to
|
|
10
10
|
// index.html for app routes. So installing the web UI is two config writes and
|
|
11
|
-
// a directory on disk
|
|
11
|
+
// a directory on disk, and this is the command that makes those writes, so the
|
|
12
12
|
// curl installer does not have to know the key names, the file format, or which
|
|
13
13
|
// of the three settings tiers a daemon-owned key belongs in. Same reason
|
|
14
14
|
// `provision-wake-model` exists: the installer runs the binary it just placed
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
// WHERE IT IS SERVED, AND WHY THE URL SAYS WHAT IT SAYS
|
|
18
18
|
//
|
|
19
19
|
// The bundle is served BY THE CONTROL-PLANE LISTENER, same origin as the API
|
|
20
|
-
// (that is the whole point
|
|
20
|
+
// (that is the whole point, a same-origin bundle makes the browser's
|
|
21
21
|
// same-origin policy a non-issue and needs no CORS allowlist). So the URL that
|
|
22
22
|
// opens the web UI is the control-plane origin: `http://<host>:<controlPlane.port>`,
|
|
23
23
|
// not `web.port`. `web.port` is the surface's DECLARED endpoint, used for links
|
|
24
|
-
// and for `tailscale serve`; nothing binds it. This command therefore reports
|
|
25
|
-
// and, when it is still sitting on the shipped placeholder, writes
|
|
24
|
+
// and for `tailscale serve`; nothing binds it. This command therefore reports,
|
|
25
|
+
// and, when it is still sitting on the shipped placeholder, writes,
|
|
26
26
|
// `web.publicBaseUrl` as the origin that actually answers, so the printed URL,
|
|
27
27
|
// the pairing deep link and the running server all agree.
|
|
28
28
|
//
|
|
@@ -41,7 +41,7 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
41
41
|
import { resolveRuntimeEndpointBinding } from '@pellux/goodvibes-terminal-shell';
|
|
42
42
|
import { probeStableHostInputs, stableUrlHostForBindHost, type StableHostInputs } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
43
43
|
|
|
44
|
-
/** The shipped `web.publicBaseUrl` placeholder
|
|
44
|
+
/** The shipped `web.publicBaseUrl` placeholder, a port nothing binds. */
|
|
45
45
|
const SHIPPED_PUBLIC_BASE_URL = 'http://127.0.0.1:3423';
|
|
46
46
|
|
|
47
47
|
export interface WebuiCommandResult {
|
|
@@ -175,13 +175,13 @@ function postureLines(origin: string, loopback: boolean): string[] {
|
|
|
175
175
|
];
|
|
176
176
|
}
|
|
177
177
|
return [
|
|
178
|
-
` reachable from your network at ${origin}
|
|
178
|
+
` reachable from your network at ${origin}; the control-plane listener is bound to all interfaces.`,
|
|
179
179
|
' To take it back to this machine only: goodvibes-daemon webui enable --loopback',
|
|
180
180
|
];
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
|
-
* Run the command and report it. Never throws for an ordinary refusal
|
|
184
|
+
* Run the command and report it. Never throws for an ordinary refusal, a bad
|
|
185
185
|
* argument, a missing bundle, an unwritable settings file all come back as an
|
|
186
186
|
* exit code and lines, because the caller is often an installer reading both.
|
|
187
187
|
*/
|
|
@@ -212,14 +212,14 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
|
|
|
212
212
|
try {
|
|
213
213
|
config.set('controlPlane.webui.serve', false);
|
|
214
214
|
} catch (error) {
|
|
215
|
-
return { exitCode: 1, lines: [`webui: could not write settings
|
|
215
|
+
return { exitCode: 1, lines: [`webui: could not write settings: ${message(error)}`] };
|
|
216
216
|
}
|
|
217
217
|
const kept = readString(config, 'controlPlane.webui.bundleDir');
|
|
218
218
|
return {
|
|
219
219
|
exitCode: 0,
|
|
220
220
|
lines: [
|
|
221
221
|
'web UI: no longer served by the daemon.',
|
|
222
|
-
...(kept ? [` the bundle is left on disk at ${kept}
|
|
222
|
+
...(kept ? [` the bundle is left on disk at ${kept}; 'goodvibes-daemon webui enable' serves it again`] : []),
|
|
223
223
|
' Restart the daemon for this to take effect on a running process.',
|
|
224
224
|
],
|
|
225
225
|
};
|
|
@@ -242,7 +242,7 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
|
|
|
242
242
|
return {
|
|
243
243
|
exitCode: 1,
|
|
244
244
|
lines: [
|
|
245
|
-
`webui: refusing to serve ${bundleDir}
|
|
245
|
+
`webui: refusing to serve ${bundleDir}: ${problem}.`,
|
|
246
246
|
' Nothing was changed.',
|
|
247
247
|
],
|
|
248
248
|
};
|
|
@@ -260,7 +260,7 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
|
|
|
260
260
|
config.set('web.hostMode', 'local');
|
|
261
261
|
}
|
|
262
262
|
} catch (error) {
|
|
263
|
-
return { exitCode: 1, lines: [`webui: could not write settings
|
|
263
|
+
return { exitCode: 1, lines: [`webui: could not write settings: ${message(error)}`] };
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
const serving = servingOrigin(config, probe);
|
|
@@ -284,7 +284,7 @@ export function runWebuiCommand(argv: readonly string[], deps: WebuiCommandDeps)
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
if (!serving.recognized) {
|
|
287
|
-
lines.push(' note: controlPlane.hostMode is not one of local|network|custom
|
|
287
|
+
lines.push(' note: controlPlane.hostMode is not one of local|network|custom; the daemon cannot bind until that is corrected.');
|
|
288
288
|
}
|
|
289
289
|
lines.push(...postureLines(serving.origin, serving.loopback));
|
|
290
290
|
lines.push(' Restart the daemon for this to take effect on a running process.');
|
|
@@ -324,7 +324,7 @@ function renderStatus(
|
|
|
324
324
|
} else {
|
|
325
325
|
const absoluteDir = deps.absolute(directory);
|
|
326
326
|
const problem = describeBundleProblem(absoluteDir, deps.directoryExists, deps.fileExists);
|
|
327
|
-
lines.push(problem ? ` bundle (${source}): ${absoluteDir}
|
|
327
|
+
lines.push(problem ? ` bundle (${source}): ${absoluteDir} (UNUSABLE: ${problem})` : ` bundle (${source}): ${absoluteDir}`);
|
|
328
328
|
}
|
|
329
329
|
const currentPublic = readString(config, 'web.publicBaseUrl');
|
|
330
330
|
if (currentPublic && currentPublic !== serving.origin) {
|
|
@@ -98,7 +98,7 @@ export async function runDaemonBootTasks(services: RuntimeServices): Promise<voi
|
|
|
98
98
|
|
|
99
99
|
// Load the plugins this host can serve. The manager was constructed by the
|
|
100
100
|
// graph and never initialised, so it could list a plugin directory and never
|
|
101
|
-
// load anything out of it
|
|
101
|
+
// load anything out of it, `enable` persisted a flag that turned nothing on.
|
|
102
102
|
// Both hosts read the same directories; each takes the registrations it can
|
|
103
103
|
// serve (plugin-composition.ts). Best-effort like everything else here: a
|
|
104
104
|
// plugin that will not load is not a reason for the daemon not to start.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-checkout-seam-holder.ts, holding a seam that arrives after its
|
|
3
|
+
* first reader is constructed.
|
|
4
|
+
*
|
|
5
|
+
* ── Why a holder, and not a constructor argument ──────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The SDK hands out a `BrowserCheckoutSeam` through `onBrowserCheckout`, a
|
|
8
|
+
* callback `composeDaemonBrowser` invokes once, synchronously, at the moment
|
|
9
|
+
* IT composes the browser. In this daemon that composition runs inside
|
|
10
|
+
* `attachWsOnlyGatewayVerbHandlers` (services.ts), which is called AFTER
|
|
11
|
+
* `createDaemonHandlerComposition` builds the payments handlers, because the
|
|
12
|
+
* ws-only verb groups need managers (disposal scope, workspace checkpoint
|
|
13
|
+
* manager, ...) that do not exist yet at the point payments is composed.
|
|
14
|
+
* Reordering the two would ripple across every other verb group that call
|
|
15
|
+
* builds, for one capability's benefit.
|
|
16
|
+
*
|
|
17
|
+
* So the payments composition cannot receive the seam as a constructor
|
|
18
|
+
* argument; it receives a GETTER, closed over this holder, and reads it at
|
|
19
|
+
* CALL time rather than at registration time. By the time any real invocation
|
|
20
|
+
* reaches `payments.checkout.begin`, the daemon has finished booting and
|
|
21
|
+
* `onBrowserCheckout` has already fired (or the composition is one where the
|
|
22
|
+
* browser was never buildable at all, `composeDaemonBrowser` returned null,
|
|
23
|
+
* homeDirectory absent, and the getter honestly keeps returning undefined
|
|
24
|
+
* forever, which the checkout handler already refuses on cleanly).
|
|
25
|
+
*/
|
|
26
|
+
import type { BrowserCheckoutSeam } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
27
|
+
|
|
28
|
+
export interface BrowserCheckoutSeamHolder {
|
|
29
|
+
readonly get: () => BrowserCheckoutSeam | undefined;
|
|
30
|
+
readonly set: (seam: BrowserCheckoutSeam) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Forgets the held seam. Call this from the browser's own disposal path
|
|
33
|
+
* (services.ts, registered on the SAME disposal scope the browser sessions
|
|
34
|
+
* teardown runs on), so a daemon that has started shutting its browser down
|
|
35
|
+
* cannot hand a checkout call a seam whose `driverFor`/`cardFieldGuard` point
|
|
36
|
+
* at an engine that is being (or has been) torn down. After this, `get()`
|
|
37
|
+
* returns `undefined` again, and `payments.checkout.begin`/`.fillCard` fall
|
|
38
|
+
* back to their ordinary "checkout is not available right now" 409 refusal,
|
|
39
|
+
* see checkout-handlers.ts, rather than reaching into a disposed engine.
|
|
40
|
+
*/
|
|
41
|
+
readonly clear: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createBrowserCheckoutSeamHolder(): BrowserCheckoutSeamHolder {
|
|
45
|
+
let seam: BrowserCheckoutSeam | undefined;
|
|
46
|
+
return {
|
|
47
|
+
get: () => seam,
|
|
48
|
+
set: (received) => {
|
|
49
|
+
seam = received;
|
|
50
|
+
},
|
|
51
|
+
clear: () => {
|
|
52
|
+
seam = undefined;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|