@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,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pairing-banner.ts
|
|
2
|
+
* pairing-banner.ts, the pairing block, in one place.
|
|
3
3
|
*
|
|
4
4
|
* A daemon prints this once, as it finishes starting. That was the ONLY way to
|
|
5
5
|
* see it: scroll it off the screen, start the daemon as a service where nothing
|
|
6
6
|
* reads stdout, or come back to the box tomorrow, and there was no way to get
|
|
7
7
|
* the link back short of restarting the daemon. `goodvibes-daemon pair` prints
|
|
8
|
-
* it again
|
|
8
|
+
* it again, and prints exactly THIS, because both callers render from here.
|
|
9
9
|
*
|
|
10
10
|
* It reprints the daemon's existing shared token rather than minting a new one.
|
|
11
11
|
* Minting is a different act with a different consequence (a per-device token,
|
|
12
12
|
* one more record in the token store, a link the boot banner's QR no longer
|
|
13
|
-
* matches), and `pair` is not the command for it
|
|
13
|
+
* matches), and `pair` is not the command for it, `pairing.handoff.create` is.
|
|
14
14
|
*/
|
|
15
15
|
import {
|
|
16
16
|
buildPairingHandoffLink,
|
|
@@ -28,7 +28,7 @@ export interface PairingBannerInput {
|
|
|
28
28
|
readonly version: string;
|
|
29
29
|
/** The web-app origin the deep link opens. */
|
|
30
30
|
readonly origin: string;
|
|
31
|
-
/** The token the link carries
|
|
31
|
+
/** The token the link carries, the daemon's existing shared companion token. */
|
|
32
32
|
readonly token: string;
|
|
33
33
|
readonly offers: readonly PairingHandoffOfferKind[];
|
|
34
34
|
/** False to print the link and the offers without the QR block. */
|
|
@@ -69,7 +69,7 @@ export function renderPairingBanner(input: PairingBannerInput): PairingBanner {
|
|
|
69
69
|
capabilities,
|
|
70
70
|
notice,
|
|
71
71
|
lines: [
|
|
72
|
-
`GoodVibes daemon ${input.version}
|
|
72
|
+
`GoodVibes daemon ${input.version}: scan to pair a device (opens the web app signed in):`,
|
|
73
73
|
'',
|
|
74
74
|
` ${input.origin}`,
|
|
75
75
|
'',
|
package/src/daemon/cli.ts
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
readControlPlaneBinding,
|
|
8
8
|
resolveDaemonEnabled,
|
|
9
9
|
} from '@pellux/goodvibes-sdk/platform/config';
|
|
10
|
-
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
11
10
|
import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../config/surface.ts';
|
|
11
|
+
import { isKnownConfigKey } from '../config/config-key-guard.ts';
|
|
12
12
|
import { formatProviderModel, getModelIdFromProviderModel, getProviderIdFromModel } from '@pellux/goodvibes-sdk/platform/providers';
|
|
13
13
|
import { resolveGoodVibesHomeOwnership } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
14
|
import { RuntimeEventBus, GlobalNetworkTransportInstaller, configureRuntimeEventBusDefaults, runtimeEventBusOptionsFrom } from '@/runtime/index.ts';
|
|
@@ -72,15 +72,15 @@ import { renderPairingBanner } from '../core/pairing-banner.ts';
|
|
|
72
72
|
import { readOperatorTokenFile } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
73
73
|
type DaemonCliOwnership = {
|
|
74
74
|
readonly workingDirectory: string;
|
|
75
|
-
/** The GoodVibes tree root
|
|
75
|
+
/** The GoodVibes tree root, settings, workspace, and discovery all hang off this. */
|
|
76
76
|
readonly homeDirectory: string;
|
|
77
77
|
/** The daemon's OWN identity home (auth users, operator tokens, daemon settings). */
|
|
78
78
|
readonly daemonHomeDirectory: string;
|
|
79
|
-
/** True when GOODVIBES_HOME or GOODVIBES_DAEMON_HOME named an override
|
|
79
|
+
/** True when GOODVIBES_HOME or GOODVIBES_DAEMON_HOME named an override, see goodvibes-home.ts. */
|
|
80
80
|
readonly isOverridden: boolean;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
// CLI flag parsing delegated to shared module
|
|
83
|
+
// CLI flag parsing delegated to shared module, see src/cli-flags.ts
|
|
84
84
|
|
|
85
85
|
type DaemonCliTokens = {
|
|
86
86
|
readonly daemonToken: string | undefined;
|
|
@@ -90,11 +90,11 @@ type DaemonCliTokens = {
|
|
|
90
90
|
/**
|
|
91
91
|
* Two different directories that used to be one.
|
|
92
92
|
*
|
|
93
|
-
* `GOODVIBES_DAEMON_HOME` names the DAEMON's home
|
|
93
|
+
* `GOODVIBES_DAEMON_HOME` names the DAEMON's home, the identity directory
|
|
94
94
|
* holding auth-users.json, operator-tokens.json, and daemon-settings.json. That
|
|
95
95
|
* is what the name says and what the SDK's `resolveDaemonHomeDir()` has always
|
|
96
96
|
* meant by it. This function used to read it as the GoodVibes tree ROOT, so
|
|
97
|
-
* setting it relocated settings, workspace, and every discovery root as well
|
|
97
|
+
* setting it relocated settings, workspace, and every discovery root as well,
|
|
98
98
|
* far more than the daemon's own state.
|
|
99
99
|
*
|
|
100
100
|
* `GOODVIBES_HOME` is the variable for relocating the tree root. It is what a
|
|
@@ -104,7 +104,7 @@ type DaemonCliTokens = {
|
|
|
104
104
|
function resolveDaemonCliOwnership(): DaemonCliOwnership {
|
|
105
105
|
// Both roots come from the SDK's platform/config goodvibes-home, which the CLIENT entry
|
|
106
106
|
// point also uses. They were resolved independently, and the client's copy
|
|
107
|
-
// simply did not read GOODVIBES_HOME
|
|
107
|
+
// simply did not read GOODVIBES_HOME, so a redirected client wrote into the
|
|
108
108
|
// real tree while the daemon honoured the redirect.
|
|
109
109
|
const { homeDirectory, daemonHomeDirectory, isOverridden } = resolveGoodVibesHomeOwnership();
|
|
110
110
|
return {
|
|
@@ -130,7 +130,7 @@ async function main(): Promise<void> {
|
|
|
130
130
|
// Before the parser, because the subcommand has its own flag vocabulary
|
|
131
131
|
// (--group, --key, --host, --port, --token) that the daemon parser would
|
|
132
132
|
// reject as unknown. Before the runtime, because this command talks to a
|
|
133
|
-
// daemon that is ALREADY RUNNING
|
|
133
|
+
// daemon that is ALREADY RUNNING, composing a second runtime here would
|
|
134
134
|
// build a competing set of state on a machine that already has one.
|
|
135
135
|
//
|
|
136
136
|
// See the terminal shell's cluster-remote-daemon-target for the
|
|
@@ -156,7 +156,7 @@ async function main(): Promise<void> {
|
|
|
156
156
|
});
|
|
157
157
|
// Every write below is immediately followed by process.exit, so all of them
|
|
158
158
|
// go to the descriptor synchronously rather than through a stream that can
|
|
159
|
-
// still be in flight when the process stops existing
|
|
159
|
+
// still be in flight when the process stops existing, see
|
|
160
160
|
// fatal-boot-report.ts. (The OSC 52 clipboard sequence is unterminated
|
|
161
161
|
// either way; the line terminator this adds after it is inert.)
|
|
162
162
|
if (result.rawOutput) writeExitingStdoutLine(`\u001b${result.rawOutput}`);
|
|
@@ -183,7 +183,7 @@ async function main(): Promise<void> {
|
|
|
183
183
|
if (rawArgs[0] === 'send') {
|
|
184
184
|
const ownership = resolveDaemonCliOwnership();
|
|
185
185
|
// Named before the delivery runs, because the flushActivityLogSync() below
|
|
186
|
-
// is what preserves this send's OUTBOUND_HTTP record
|
|
186
|
+
// is what preserves this send's OUTBOUND_HTTP record, and a logger with no
|
|
187
187
|
// destination has nothing to flush, so that record was never written.
|
|
188
188
|
configureActivityLogger(join(ownership.workingDirectory, '.goodvibes', 'logs'));
|
|
189
189
|
runDaemonConfigMigration(ownership.homeDirectory);
|
|
@@ -214,7 +214,7 @@ async function main(): Promise<void> {
|
|
|
214
214
|
// `provision-wake-model …` is intercepted here for the same two reasons, plus
|
|
215
215
|
// its own: the curl installer runs it on the binary it has just placed, before
|
|
216
216
|
// any daemon exists, and it must reach the SDK's pinned manifest rather than
|
|
217
|
-
// have install.sh carry a second copy of the pins. It composes no runtime
|
|
217
|
+
// have install.sh carry a second copy of the pins. It composes no runtime, it
|
|
218
218
|
// resolves a home directory, derives the managed voice root the running daemon
|
|
219
219
|
// uses, and calls one SDK function. See provision-wake-model.ts for why it
|
|
220
220
|
// exits 0 on a failed download.
|
|
@@ -236,7 +236,7 @@ async function main(): Promise<void> {
|
|
|
236
236
|
// placed, right after unpacking the web UI bundle, so that the key names, the
|
|
237
237
|
// settings-tier routing and the "which listener actually serves this" question
|
|
238
238
|
// all stay in one implementation instead of being copied into shell. It
|
|
239
|
-
// composes no runtime
|
|
239
|
+
// composes no runtime, it resolves a home directory, opens the same config
|
|
240
240
|
// manager the daemon boots with, and writes two or three keys. See
|
|
241
241
|
// webui-command.ts.
|
|
242
242
|
if (rawArgs[0] === 'webui') {
|
|
@@ -323,7 +323,7 @@ async function main(): Promise<void> {
|
|
|
323
323
|
process.exit(0);
|
|
324
324
|
}
|
|
325
325
|
// `cluster`, `send`, `webui` and `provision-wake-model` are handled at the top
|
|
326
|
-
// of this function, off the raw argument list, for the reasons stated there
|
|
326
|
+
// of this function, off the raw argument list, for the reasons stated there,
|
|
327
327
|
// which means they only work as the FIRST word. Reaching one here means a
|
|
328
328
|
// global flag was written in front of it. Refusing says so; falling through
|
|
329
329
|
// would start a daemon in the foreground, which is the exact behaviour this
|
|
@@ -360,8 +360,8 @@ async function main(): Promise<void> {
|
|
|
360
360
|
// Give the shared logger a destination before anything else runs. It never
|
|
361
361
|
// had one in this entrypoint: `logger` only writes to a file once
|
|
362
362
|
// `configureActivityLogger()` has named one, so every logger.info/warn/error
|
|
363
|
-
// in this daemon
|
|
364
|
-
// and every flushActivityLogSync() call
|
|
363
|
+
// in this daemon, including the fatal handler at the bottom of this file
|
|
364
|
+
// and every flushActivityLogSync() call, went nowhere at all. That is why
|
|
365
365
|
// a crash-looping daemon left an empty activity log next to a silent
|
|
366
366
|
// console. The terminal app's own src/cli/entrypoint.ts configures its
|
|
367
367
|
// logger the same way at the same point in startup.
|
|
@@ -479,23 +479,23 @@ async function main(): Promise<void> {
|
|
|
479
479
|
|
|
480
480
|
// Service lifecycle: install / uninstall / start / stop / restart / status /
|
|
481
481
|
// migrate manage this host's service definition for the shared daemon. They
|
|
482
|
-
// run BEFORE the daemon boots
|
|
482
|
+
// run BEFORE the daemon boots, no runtime/services are constructed, and
|
|
483
483
|
// exit with the honest result code.
|
|
484
484
|
const serviceSubcommand = cli.command;
|
|
485
485
|
if (isDaemonServiceSubcommand(serviceSubcommand)) {
|
|
486
486
|
// The host/port baked into the unit's ExecStart (and displayed) come from
|
|
487
|
-
// the SAME hostMode-aware resolution the SDK bind path uses
|
|
487
|
+
// the SAME hostMode-aware resolution the SDK bind path uses, never from
|
|
488
488
|
// the GOODVIBES_DAEMON_HOST env var, which nothing in the bind path reads
|
|
489
489
|
// (the --hostname flag already lands in config via
|
|
490
490
|
// applyRuntimeEndpointFlagOverrides above, so it is covered here).
|
|
491
491
|
const binding = resolveRuntimeEndpointBinding(config, 'controlPlane');
|
|
492
492
|
if (!binding.recognized) {
|
|
493
|
-
// The SDK bind path has no default case for an unrecognized hostMode
|
|
493
|
+
// The SDK bind path has no default case for an unrecognized hostMode,
|
|
494
494
|
// a daemon launched with this config throws before binding. Say so
|
|
495
495
|
// instead of presenting the fallback values as a real binding.
|
|
496
496
|
console.warn(
|
|
497
497
|
`[goodvibes-daemon] warning: controlPlane.hostMode '${binding.hostMode}' is not a recognized mode ` +
|
|
498
|
-
"(local|network|custom)
|
|
498
|
+
"(local|network|custom); the daemon will fail to start until it is corrected.",
|
|
499
499
|
);
|
|
500
500
|
}
|
|
501
501
|
const binaryPath = resolveInstalledDaemonBinary({ moduleUrl: import.meta.url });
|
|
@@ -504,12 +504,12 @@ async function main(): Promise<void> {
|
|
|
504
504
|
binaryPath,
|
|
505
505
|
homeDir: homeDirectory,
|
|
506
506
|
// Unit-file paths resolve from the LOGIN home, never the
|
|
507
|
-
// GOODVIBES_HOME-overridable tree home above
|
|
507
|
+
// GOODVIBES_HOME-overridable tree home above, see
|
|
508
508
|
// BuildManagedDaemonServiceManagerParams.unitHomeDir's doc.
|
|
509
509
|
unitHomeDir: homedir(),
|
|
510
510
|
host: binding.host,
|
|
511
511
|
port: binding.port,
|
|
512
|
-
// migrate-service only: never auto-migrate
|
|
512
|
+
// migrate-service only: never auto-migrate, requires the same explicit
|
|
513
513
|
// consent as any other non-interactive destructive confirmation.
|
|
514
514
|
confirmMigration: cliFlags.yes,
|
|
515
515
|
// service-status only.
|
|
@@ -521,7 +521,7 @@ async function main(): Promise<void> {
|
|
|
521
521
|
portFlagProvided: cliFlags.port !== undefined,
|
|
522
522
|
});
|
|
523
523
|
// These print the unit path, the follow-up commands and the honest result,
|
|
524
|
-
// then exit immediately
|
|
524
|
+
// then exit immediately, exactly the race a stream write loses. Descriptor
|
|
525
525
|
// writes instead.
|
|
526
526
|
for (const line of result.lines) {
|
|
527
527
|
writeExitingStdoutLine(line);
|
|
@@ -529,14 +529,14 @@ async function main(): Promise<void> {
|
|
|
529
529
|
process.exit(result.exitCode);
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
// Honest startup identity, printed for EVERY launch shape
|
|
532
|
+
// Honest startup identity, printed for EVERY launch shape, including a bare
|
|
533
533
|
// (no-arg) systemd launch, and BEFORE any runtime construction so it still
|
|
534
534
|
// reaches the journal when a broken config makes the daemon throw during
|
|
535
535
|
// composition. It states the resolved version (never a placeholder) and the
|
|
536
536
|
// home/host/port the daemon will actually bind: the binding comes from the
|
|
537
537
|
// SAME hostMode-aware resolution the SDK bind path uses (resolveHostBinding:
|
|
538
538
|
// 'local' forces 127.0.0.1, 'network' forces 0.0.0.0, port 0/non-numeric
|
|
539
|
-
// falls back to the default)
|
|
539
|
+
// falls back to the default), never from controlPlane.host alone, and never
|
|
540
540
|
// from the GOODVIBES_DAEMON_HOST env var, which the bind path does not read.
|
|
541
541
|
const bannerBinding = resolveRuntimeEndpointBinding(config, 'controlPlane');
|
|
542
542
|
// eslint-disable-next-line no-console
|
|
@@ -547,14 +547,14 @@ async function main(): Promise<void> {
|
|
|
547
547
|
// serving). Warn here so the journaled crash is explained.
|
|
548
548
|
console.warn(
|
|
549
549
|
`[goodvibes-daemon] warning: controlPlane.hostMode '${bannerBinding.hostMode}' is not a recognized mode ` +
|
|
550
|
-
"(local|network|custom)
|
|
550
|
+
"(local|network|custom): the daemon cannot bind until it is corrected; the host/port above are fallback values, not a real binding.",
|
|
551
551
|
);
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
// Boot-time reconciliation. The banner above states the real bind; this
|
|
555
555
|
// compares it to the URL clients are actually handed. Those are produced by
|
|
556
556
|
// two different resolvers, and when they disagree the daemon is advertising an
|
|
557
|
-
// address it does not answer on
|
|
557
|
+
// address it does not answer on, the state that produced two different click
|
|
558
558
|
// hosts from one daemon. The comparison is the SDK's own
|
|
559
559
|
// describeDerivedBindMismatch, which already knows the two cases that are NOT
|
|
560
560
|
// drift: a wildcard bind reported as 0.0.0.0 against a loopback dial target is
|
|
@@ -564,7 +564,9 @@ async function main(): Promise<void> {
|
|
|
564
564
|
if (bannerBinding.recognized) {
|
|
565
565
|
const mismatch = describeDerivedBindMismatch(
|
|
566
566
|
{ host: bannerBinding.host, port: bannerBinding.port },
|
|
567
|
-
readControlPlaneBinding((key) =>
|
|
567
|
+
readControlPlaneBinding((key) =>
|
|
568
|
+
isKnownConfigKey(key, config.getSchema()) ? config.get(key) : undefined,
|
|
569
|
+
),
|
|
568
570
|
);
|
|
569
571
|
if (mismatch) {
|
|
570
572
|
console.warn(`[goodvibes-daemon] warning: ${mismatch}`);
|
|
@@ -601,7 +603,7 @@ async function main(): Promise<void> {
|
|
|
601
603
|
daemonHomeDirectory,
|
|
602
604
|
// This daemon observes externally-launched coding-agent sessions on the
|
|
603
605
|
// host read-only (fleet visibility + steer; never counted, never
|
|
604
|
-
// stopped). Daemon-side only
|
|
606
|
+
// stopped). Daemon-side only, the interactive process reads this snapshot
|
|
605
607
|
// rather than double-detecting. Mirrors the SDK daemon cli.
|
|
606
608
|
observeExternalAgents: true,
|
|
607
609
|
// Opt into the REAL host power seam (Linux logind: systemd-inhibit children
|
|
@@ -613,7 +615,7 @@ async function main(): Promise<void> {
|
|
|
613
615
|
// The wake-word model ships with the installation, and this is the retry: at
|
|
614
616
|
// every daemon start, sweep the managed wake tree and fetch whatever the
|
|
615
617
|
// install could not (an offline install, a killed download, a changed pin).
|
|
616
|
-
// It never blocks startup and never fails it
|
|
618
|
+
// It never blocks startup and never fails it, see the SDK's
|
|
617
619
|
// voice/wake/install-provision.ts. The webui reads the model bytes from THIS
|
|
618
620
|
// process, so a provisioned daemon is what makes the browser path work too.
|
|
619
621
|
provisionWakeModelsAtBoot: true,
|
|
@@ -669,7 +671,7 @@ async function main(): Promise<void> {
|
|
|
669
671
|
// against a REMOTE daemon behaves exactly like one run on that machine.
|
|
670
672
|
clusterGroupVerbs: runtimeServices.clusterGroup.verbs,
|
|
671
673
|
// Daemon-hosted sessions. Stating this is what turns `sessions.hosted.*`
|
|
672
|
-
// on, and what it states is where a hosted run's asks are gated
|
|
674
|
+
// on, and what it states is where a hosted run's asks are gated, this
|
|
673
675
|
// daemon's per-workspace trust decision. See
|
|
674
676
|
// runtime/hosted-session-composition.ts.
|
|
675
677
|
hostedSessions: createHostedSessionOptions(runtimeServices),
|
|
@@ -733,7 +735,7 @@ async function main(): Promise<void> {
|
|
|
733
735
|
// product's rather than the platform's: the legacy memory fold, provider
|
|
734
736
|
// watching, and the notifier/integration attachments. They run AFTER
|
|
735
737
|
// daemon.start(), because the fold has to follow the facade's memoryStore
|
|
736
|
-
// init(), and none of them may keep the daemon from serving
|
|
738
|
+
// init(), and none of them may keep the daemon from serving, see
|
|
737
739
|
// runtime/boot-tasks.ts.
|
|
738
740
|
await runDaemonBootTasks(runtimeServices);
|
|
739
741
|
|
|
@@ -756,11 +758,11 @@ async function main(): Promise<void> {
|
|
|
756
758
|
// crash timeout.
|
|
757
759
|
//
|
|
758
760
|
// This process built the runtime graph and handed it to DaemonServer, so by
|
|
759
|
-
// the SDK's ownership rule the facade leaves it alone
|
|
761
|
+
// the SDK's ownership rule the facade leaves it alone, nothing else stops
|
|
760
762
|
// these pollers. Without dispose() the config watch, fleet tick, memory
|
|
761
763
|
// governor, watcher registry and six more kept ticking until process exit.
|
|
762
764
|
// The handler surfaces (inbox store + its poll timers, catalog handlers) are
|
|
763
|
-
// the FIRST thing dispose() unwinds
|
|
765
|
+
// the FIRST thing dispose() unwinds, they are on the disposal owner list
|
|
764
766
|
// now rather than sequenced by hand here, which is what makes every other
|
|
765
767
|
// shutdown path stop them too instead of only this one.
|
|
766
768
|
const stop = Promise.allSettled([listener.stop(), daemon.stop()])
|
|
@@ -768,7 +770,7 @@ async function main(): Promise<void> {
|
|
|
768
770
|
.then(() => 'done' as const);
|
|
769
771
|
const result = await Promise.race([stop, timeout]);
|
|
770
772
|
if (result === 'timeout') {
|
|
771
|
-
logger.warn('shutdown deadline exceeded
|
|
773
|
+
logger.warn('shutdown deadline exceeded; forcing exit');
|
|
772
774
|
// A forced exit is exactly the case where the log matters most and is
|
|
773
775
|
// least likely to have drained on its own.
|
|
774
776
|
flushActivityLogSync();
|
|
@@ -795,16 +797,16 @@ async function main(): Promise<void> {
|
|
|
795
797
|
|
|
796
798
|
// Cheap unattended reconcile: if this (canonical) daemon unit is confirmed
|
|
797
799
|
// serving AND a redundant installer-managed goodvibes-daemon.service (the
|
|
798
|
-
// retired unit name) sits enabled-but-NOT-running beside it
|
|
799
|
-
// production-incident state
|
|
800
|
+
// retired unit name) sits enabled-but-NOT-running beside it, the exact
|
|
801
|
+
// production-incident state, auto-disable and remove it, printing a
|
|
800
802
|
// receipt. A RUNNING legacy daemon, a hand-written unit, or an unanswered
|
|
801
803
|
// configured endpoint all refuse with a notice instead. Best-effort: never
|
|
802
804
|
// let this block or crash daemon boot (per-call systemctl timeouts plus one
|
|
803
805
|
// cumulative pass deadline). The unit search root is the LOGIN user's home
|
|
804
806
|
// (where systemd user units live), never the daemon data home
|
|
805
807
|
// (GOODVIBES_DAEMON_HOME); the tracked name honors service.serviceName. The
|
|
806
|
-
// endpoint requirement uses the CLIENT view of the config
|
|
807
|
-
// settings.json with none of this process's runtime flag overrides
|
|
808
|
+
// endpoint requirement uses the CLIENT view of the config, a fresh read of
|
|
809
|
+
// settings.json with none of this process's runtime flag overrides, because
|
|
808
810
|
// that is what clients resolve when they look for the daemon.
|
|
809
811
|
try {
|
|
810
812
|
runDaemonConfigMigration(homeDirectory);
|
|
@@ -827,8 +829,8 @@ async function main(): Promise<void> {
|
|
|
827
829
|
}
|
|
828
830
|
}
|
|
829
831
|
if (reconcile.reason !== 'no-legacy-unit') {
|
|
830
|
-
// Breadcrumb for EVERY outcome where a legacy unit file exists
|
|
831
|
-
// including guard refusals
|
|
832
|
+
// Breadcrumb for EVERY outcome where a legacy unit file exists,
|
|
833
|
+
// including guard refusals, so a persisting two-unit state is never
|
|
832
834
|
// silent about why nothing was reconciled.
|
|
833
835
|
logger.info('daemon: legacy-unit reconcile', { action: reconcile.action, reason: reconcile.reason });
|
|
834
836
|
}
|
|
@@ -837,7 +839,7 @@ async function main(): Promise<void> {
|
|
|
837
839
|
}
|
|
838
840
|
|
|
839
841
|
// Print a device-pairing QR to stdout. The QR encodes the canonical
|
|
840
|
-
// `#pair=<token>` deep link the web app consumes
|
|
842
|
+
// `#pair=<token>` deep link the web app consumes, a camera scan opens it
|
|
841
843
|
// already signed in. No raw JSON connection blob is printed. This is also the
|
|
842
844
|
// one place web.publicBaseUrl is frozen from the stable-name resolution (never
|
|
843
845
|
// clobbering a user-set value), so the printed origin survives a DHCP change.
|
|
@@ -849,7 +851,7 @@ async function main(): Promise<void> {
|
|
|
849
851
|
stepUpAvailable: true,
|
|
850
852
|
});
|
|
851
853
|
// Rendered by core/pairing-banner.ts, which `goodvibes-daemon pair` also
|
|
852
|
-
// calls
|
|
854
|
+
// calls, so the block printed here and the one printed on demand are the
|
|
853
855
|
// same block, from the same token, and cannot drift.
|
|
854
856
|
const banner = renderPairingBanner({
|
|
855
857
|
version: VERSION,
|
|
@@ -870,7 +872,7 @@ void main().catch((error) => {
|
|
|
870
872
|
//
|
|
871
873
|
// Doing it the other way round is what shipped mute. This handler used to
|
|
872
874
|
// call logger.error and flushActivityLogSync and nothing else, and the
|
|
873
|
-
// logger had no destination this early in boot
|
|
875
|
+
// logger had no destination this early in boot, so the released 1.27.0
|
|
874
876
|
// binary crash-looped 77 times with exit 1, zero bytes on stdout, zero bytes
|
|
875
877
|
// on stderr and an empty activity log. See fatal-boot-report.ts.
|
|
876
878
|
reportFatalBootFailure(error);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* config-command.ts
|
|
2
|
+
* config-command.ts, `goodvibes-daemon config list|get|set|unset`.
|
|
3
3
|
*
|
|
4
4
|
* A headless daemon's settings had no command at all: the only ways to change
|
|
5
5
|
* one were to hand-edit a JSON file whose path and tier you had to already
|
|
6
6
|
* know, or to attach a client. `webui enable` proved the pattern for a
|
|
7
|
-
* settings-writing command that the installer and a person can both run
|
|
7
|
+
* settings-writing command that the installer and a person can both run, open
|
|
8
8
|
* the same ConfigManager the daemon boots with, write through `set()`, print an
|
|
9
|
-
* honest receipt
|
|
9
|
+
* honest receipt, and this is that pattern generalised to every key.
|
|
10
10
|
*
|
|
11
11
|
* WHICH FILE A WRITE LANDS IN is not decided here. `ConfigManager.set()` routes
|
|
12
12
|
* a daemon-owned key to the daemon's own settings store and a shared key to the
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
* cli-redaction
|
|
18
18
|
* first, so a token, a password, an API key or a card number reads as
|
|
19
19
|
* `<redacted>` and a `config list` pasted into a bug report carries no
|
|
20
|
-
* credential. Writes are NOT redacted
|
|
20
|
+
* credential. Writes are NOT redacted, `config set` stores the real value; it
|
|
21
21
|
* is the OUTPUT that is cleaned. A `goodvibes://secrets/...` reference is left
|
|
22
22
|
* visible on purpose: it is a pointer, not a secret, and hiding it would make
|
|
23
23
|
* the indirection impossible to verify.
|
|
24
24
|
*/
|
|
25
|
-
import type { ConfigManager,
|
|
25
|
+
import type { ConfigManager, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
|
|
26
26
|
import { ConfigError } from '@pellux/goodvibes-sdk/platform/config';
|
|
27
27
|
import {
|
|
28
28
|
REDACTED_VALUE,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
parseConfigValueText,
|
|
31
31
|
redactConfig,
|
|
32
32
|
} from '@pellux/goodvibes-terminal-shell';
|
|
33
|
+
import { isKnownConfigKey } from '../config/config-key-guard.ts';
|
|
33
34
|
|
|
34
35
|
export const CONFIG_SUBCOMMANDS = ['list', 'get', 'set', 'unset'] as const;
|
|
35
36
|
export type ConfigSubcommand = (typeof CONFIG_SUBCOMMANDS)[number];
|
|
@@ -107,8 +108,9 @@ function message(error: unknown): string {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function readValue(deps: ConfigCommandDeps, key: string): unknown {
|
|
111
|
+
if (!isKnownConfigKey(key, deps.configManager.getSchema())) return undefined;
|
|
110
112
|
try {
|
|
111
|
-
return deps.configManager.get(key
|
|
113
|
+
return deps.configManager.get(key);
|
|
112
114
|
} catch {
|
|
113
115
|
return undefined;
|
|
114
116
|
}
|
|
@@ -181,19 +183,18 @@ function getResult(deps: ConfigCommandDeps, key: string): ConfigCommandResult {
|
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
function setResult(deps: ConfigCommandDeps, key: string, rawValue: string): ConfigCommandResult {
|
|
184
|
-
|
|
185
|
-
if (!setting) {
|
|
186
|
+
if (!isKnownConfigKey(key, deps.configManager.getSchema())) {
|
|
186
187
|
return failure(`'${key}' is not a settings key. Run \`${deps.binary ?? 'goodvibes-daemon'} config list\` to see them.`, deps);
|
|
187
188
|
}
|
|
188
189
|
const value = parseConfigValueText(rawValue);
|
|
189
190
|
try {
|
|
190
|
-
deps.configManager.set(key
|
|
191
|
+
deps.configManager.set(key, value as never);
|
|
191
192
|
} catch (error) {
|
|
192
193
|
// A schema refusal is the common case (wrong type, value outside an enum),
|
|
193
194
|
// and its message already names what was wrong. Passing it through beats
|
|
194
195
|
// rewording it into something vaguer.
|
|
195
196
|
return failure(
|
|
196
|
-
error instanceof ConfigError ? error.message : `could not write ${key}
|
|
197
|
+
error instanceof ConfigError ? error.message : `could not write ${key}: ${message(error)}`,
|
|
197
198
|
deps,
|
|
198
199
|
);
|
|
199
200
|
}
|
|
@@ -219,14 +220,13 @@ function setResult(deps: ConfigCommandDeps, key: string, rawValue: string): Conf
|
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
function unsetResult(deps: ConfigCommandDeps, key: string): ConfigCommandResult {
|
|
222
|
-
|
|
223
|
-
if (!setting) {
|
|
223
|
+
if (!isKnownConfigKey(key, deps.configManager.getSchema())) {
|
|
224
224
|
return failure(`'${key}' is not a settings key. Run \`${deps.binary ?? 'goodvibes-daemon'} config list\` to see them.`, deps);
|
|
225
225
|
}
|
|
226
226
|
try {
|
|
227
|
-
deps.configManager.reset(key
|
|
227
|
+
deps.configManager.reset(key);
|
|
228
228
|
} catch (error) {
|
|
229
|
-
return failure(`could not reset ${key}
|
|
229
|
+
return failure(`could not reset ${key}: ${message(error)}`, deps);
|
|
230
230
|
}
|
|
231
231
|
const stored = readValue(deps, key);
|
|
232
232
|
if (deps.json) {
|
|
@@ -254,7 +254,7 @@ export function runConfigCommand(args: readonly string[], deps: ConfigCommandDep
|
|
|
254
254
|
const subcommand = args[0];
|
|
255
255
|
if (subcommand === undefined) return refusal('name what to do with the settings.', deps);
|
|
256
256
|
if (!isConfigSubcommand(subcommand)) {
|
|
257
|
-
return refusal(`'${subcommand}' is not a config command
|
|
257
|
+
return refusal(`'${subcommand}' is not a config command; try ${CONFIG_SUBCOMMANDS.join(', ')}.`, deps);
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
if (subcommand === 'list') {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* former OperatorContext. It carries the SDK gateway catalog (handlers attach
|
|
4
4
|
* to it), the daemon credential store, a read-only slice of the config manager,
|
|
5
5
|
* resolved directories, and a logger. No SDK descriptor or schema is declared
|
|
6
|
-
* here
|
|
6
|
+
* here, the catalog type is re-exported through the contracts seam.
|
|
7
7
|
*/
|
|
8
8
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
9
9
|
import type { GatewayMethodCatalog } from './contracts.ts';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Every other module under `src/daemon/handlers/` imports SDK contract
|
|
5
5
|
* identifiers from HERE and nowhere else. Concentrating the SDK imports in one
|
|
6
6
|
* concrete-submodule module keeps the rest of the layer free of barrel cycles
|
|
7
|
-
* and guarantees the host NEVER re-declares an SDK id, descriptor, or schema
|
|
7
|
+
* and guarantees the host NEVER re-declares an SDK id, descriptor, or schema,
|
|
8
8
|
* it only attaches handlers to the descriptors the SDK already registered.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -17,6 +17,21 @@ export type {
|
|
|
17
17
|
GatewayMethodHandler,
|
|
18
18
|
} from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
19
19
|
|
|
20
|
+
// The SDK's own `payments.*` route module: the registrar and the service seam
|
|
21
|
+
// it dispatches to. Exported by the SDK barrel as of 2.0.18 (CHANGELOG). Three
|
|
22
|
+
// of the seven verbs attach through this unchanged; see
|
|
23
|
+
// daemon/handlers/payments/register.ts and checkout-handlers.ts for why the
|
|
24
|
+
// other four (`cards.create`, `purchases.list`, `checkout.begin`,
|
|
25
|
+
// `checkout.fillCard`) stay local wrappers instead.
|
|
26
|
+
export { registerPaymentsGatewayMethods } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
27
|
+
export type { PaymentsGatewayService, PaymentPurchaseView } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
28
|
+
|
|
29
|
+
// The browser-checkout seam a daemon composition receives through
|
|
30
|
+
// `onBrowserCheckout` (sdk 2.0.19, platform/control-plane's
|
|
31
|
+
// `composeDaemonBrowser`). See daemon/handlers/payments/register.ts for how
|
|
32
|
+
// the checkout pair reads it.
|
|
33
|
+
export type { BrowserCheckoutSeam } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
34
|
+
|
|
20
35
|
// Channel domain types reused in handler signatures (read-only SDK interfaces; never re-declared).
|
|
21
36
|
export type {
|
|
22
37
|
ChannelIdentity,
|
|
@@ -31,13 +46,13 @@ export type {
|
|
|
31
46
|
* `remote.peers.*` HTTP routes can dispatch to it.
|
|
32
47
|
*
|
|
33
48
|
* Both were declared here, by hand, as a verbatim structural mirror of the
|
|
34
|
-
* daemon-sdk's own declarations
|
|
35
|
-
* signature
|
|
49
|
+
* daemon-sdk's own declarations, seventeen methods copied signature for
|
|
50
|
+
* signature, for one reason: neither carried the `export` keyword upstream,
|
|
36
51
|
* so neither could be imported. Both do now, and a mirror that can drift out of
|
|
37
52
|
* agreement with the interface it must satisfy is worse than no mirror at all.
|
|
38
53
|
*
|
|
39
54
|
* They are re-exported under the same names so every implementer in this
|
|
40
55
|
* product keeps naming them the way it already does. The SDK ships no
|
|
41
|
-
* docker/ssh/cloud backend
|
|
56
|
+
* docker/ssh/cloud backend, the host still owns the implementation.
|
|
42
57
|
*/
|
|
43
58
|
export type { DistributedRuntimeRouteService, RemotePeerAuth } from '@pellux/goodvibes-daemon-sdk/remote-routes';
|
|
@@ -20,7 +20,7 @@ export interface DaemonCredentialStore {
|
|
|
20
20
|
* Store a daemon-owned credential. scope defaults 'daemon'.
|
|
21
21
|
*
|
|
22
22
|
* This is the DAEMON's own credential store: every caller is daemon code, and
|
|
23
|
-
* every value written through it is one the daemon itself has to read back
|
|
23
|
+
* every value written through it is one the daemon itself has to read back,
|
|
24
24
|
* including its own generated draft encryption key (createAtRestCipher below,
|
|
25
25
|
* which calls put() with no scope at all). Defaulting to 'user' filed those
|
|
26
26
|
* in a surface-scoped tier, so the daemon's own material lived somewhere it
|
|
@@ -8,14 +8,14 @@ import { HandlerSqliteStore } from '../sqlite-store.ts';
|
|
|
8
8
|
// Mirrors the agent's local draft store (channels/drafts.json) server-side so
|
|
9
9
|
// drafts are visible across surfaces. The agent's local file remains the source
|
|
10
10
|
// of truth; this store is a sync mirror. Conflict resolution (most-recent
|
|
11
|
-
// updatedAt wins) is the integrator's concern
|
|
11
|
+
// updatedAt wins) is the integrator's concern, this store performs a plain
|
|
12
12
|
// upsert and records the supplied/derived updatedAt.
|
|
13
13
|
//
|
|
14
14
|
// SECURITY POSTURE:
|
|
15
15
|
// - The plaintext message body is NEVER persisted. It is encrypted at rest
|
|
16
16
|
// via the daemon at-rest cipher (AES-256-GCM) as `bodyEnc`.
|
|
17
17
|
// - The webhook URL is encrypted at rest (`webhookEnc`) and is ALWAYS
|
|
18
|
-
// redacted ('[redacted]') in every list/get response
|
|
18
|
+
// redacted ('[redacted]') in every list/get response, the raw URL never
|
|
19
19
|
// leaves the store.
|
|
20
20
|
// - `messageDigest` (sha256First(body, 12)) is computed at save time and
|
|
21
21
|
// stored alongside, so reads never need to decrypt the body. The required
|
|
@@ -170,7 +170,7 @@ function parseTags(raw: string | null): string[] | undefined {
|
|
|
170
170
|
return tags.length > 0 ? tags : undefined;
|
|
171
171
|
}
|
|
172
172
|
} catch {
|
|
173
|
-
// Corrupt tags column
|
|
173
|
+
// Corrupt tags column, treat as absent rather than throwing on read.
|
|
174
174
|
}
|
|
175
175
|
return undefined;
|
|
176
176
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The SDK owns every id, descriptor, access flag, and I/O schema (auto-
|
|
11
11
|
* registered with handler:undefined). This module NEVER re-declares any of
|
|
12
|
-
* them
|
|
12
|
+
* them, it looks each up via catalog.get(id) (inside registerCatalogHandler)
|
|
13
13
|
* and re-registers with the wrapped handler. save/delete are confirm-gated via
|
|
14
14
|
* RegisterHandlerOptions.confirm; the wrapper enforces body.confirm === true
|
|
15
15
|
* AND context.explicitUserRequest === true before the handler runs.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* SECURITY: the message body is encrypted at rest and NEVER returned (the wire
|
|
18
18
|
* `message` field carries the sha256First(body,12) digest). Webhooks are
|
|
19
19
|
* encrypted at rest and ALWAYS redacted on read; RAW webhook tokens submitted
|
|
20
|
-
* to save are REJECTED
|
|
20
|
+
* to save are REJECTED, the agent must redact ('[redacted]') or pass a
|
|
21
21
|
* goodvibes://secrets/ reference before transmission.
|
|
22
22
|
*/
|
|
23
23
|
import type { HandlerContext } from '../context.ts';
|
|
@@ -131,7 +131,7 @@ function optionalIso8601(value: unknown, field: string): string | undefined {
|
|
|
131
131
|
* to be redacted before transmission; the daemon only stores '[redacted]' or a
|
|
132
132
|
* goodvibes://secrets/ reference (which carries no token material). Any other
|
|
133
133
|
* value bearing a scheme (e.g. an https:// URL with an embedded token) is a raw
|
|
134
|
-
* secret and is rejected outright
|
|
134
|
+
* secret and is rejected outright, never persisted, never logged.
|
|
135
135
|
*/
|
|
136
136
|
function validateWebhook(value: string | undefined): string | undefined {
|
|
137
137
|
if (value === undefined) return undefined;
|
|
@@ -287,7 +287,7 @@ export function registerDraftMethods(
|
|
|
287
287
|
const id = parseDraftId(body);
|
|
288
288
|
const record = store.get(id);
|
|
289
289
|
if (record === null) return { notFound: true, id };
|
|
290
|
-
// get output permits additional properties
|
|
290
|
+
// get output permits additional properties, expose messageDigest explicitly
|
|
291
291
|
// alongside the schema-required `message` field (both carry the digest).
|
|
292
292
|
return { ...record, messageDigest: record.message };
|
|
293
293
|
};
|