@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,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pair-command.ts
|
|
2
|
+
* pair-command.ts, `goodvibes-daemon pair`.
|
|
3
3
|
*
|
|
4
4
|
* Two forms.
|
|
5
5
|
*
|
|
6
|
-
* LOCAL FORM
|
|
6
|
+
* LOCAL FORM, `pair` with no `--host`, or one naming this machine, reprints
|
|
7
7
|
* the pairing block a daemon prints once as it starts: the web origin, the
|
|
8
8
|
* offers a new device can accept, what it will be able to do, and a QR code
|
|
9
9
|
* encoding the deep link that opens the web app already signed in. It reads
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
* resolve to) and reprints the EXISTING token rather than minting a new one,
|
|
13
13
|
* so a link printed here and the one printed at boot are the same link.
|
|
14
14
|
*
|
|
15
|
-
* REMOTE FORM
|
|
15
|
+
* REMOTE FORM, `pair --host <name> [--port] [--token]`, asks THAT daemon to
|
|
16
16
|
* MINT A NEW per-device pairing token over `pairing.handoff.create` and prints
|
|
17
17
|
* the pairing block for it. Minting is a different act than reprinting: it is
|
|
18
18
|
* a fresh token, and every token that daemon already issued (its own shared
|
|
19
19
|
* token included) is left exactly as it was. Because it changes state on a
|
|
20
20
|
* daemon that may not be this process's own, it states the plan and asks for
|
|
21
|
-
* confirmation before acting
|
|
21
|
+
* confirmation before acting, `-y`/`--yes` satisfies that non-interactively,
|
|
22
22
|
* the same convention `migrate-service` uses. An unreachable daemon, a
|
|
23
23
|
* rejected token, and a daemon too old to serve the verb are each refused by
|
|
24
24
|
* name (see `callDaemonWsVerb`), never a stack trace.
|
|
@@ -84,7 +84,7 @@ function failure(error: string, fix: string, json: boolean): DaemonCommandResult
|
|
|
84
84
|
/**
|
|
85
85
|
* The web origin a remote handoff's deep link opens, recovered from the link
|
|
86
86
|
* itself. `buildPairingHandoffLink` in the SDK builds the link as exactly
|
|
87
|
-
* `<webOrigin, trailing slashes stripped>/#<params
|
|
87
|
+
* `<webOrigin, trailing slashes stripped>/#<params>`, so slicing at the
|
|
88
88
|
* first `/#` is the precise inverse, and feeding the result back into
|
|
89
89
|
* `renderPairingBanner` (which rebuilds the SAME link from origin + token +
|
|
90
90
|
* offers) reproduces byte-for-byte what the remote daemon already returned.
|
|
@@ -103,19 +103,19 @@ interface PairingHandoffOfferDetail {
|
|
|
103
103
|
readonly available?: boolean;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/** The shape `pairing.handoff.create` returns
|
|
106
|
+
/** The shape `pairing.handoff.create` returns, see routes/pairing-handoff.ts. */
|
|
107
107
|
interface PairingHandoffCreateResult {
|
|
108
108
|
readonly token: {
|
|
109
109
|
readonly id: string;
|
|
110
110
|
readonly name: string;
|
|
111
|
-
/** The plaintext secret
|
|
111
|
+
/** The plaintext secret, returned exactly once. */
|
|
112
112
|
readonly token: string;
|
|
113
113
|
readonly createdAt: number;
|
|
114
114
|
};
|
|
115
115
|
readonly offers: readonly PairingHandoffOfferDetail[];
|
|
116
|
-
/** `#pair=<token>&offers
|
|
116
|
+
/** `#pair=<token>&offers=...`, present even when no web origin is configured. */
|
|
117
117
|
readonly fragment: string;
|
|
118
|
-
/** `<webOrigin>/#pair
|
|
118
|
+
/** `<webOrigin>/#pair=...`, present only when that daemon has a web origin configured. */
|
|
119
119
|
readonly deepLink?: string;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -127,7 +127,7 @@ function runLocalReprint(input: RunPairCommandInput): DaemonCommandResult {
|
|
|
127
127
|
if (token === undefined) {
|
|
128
128
|
return failure(
|
|
129
129
|
'no operator token was found for this machine, so there is no link to print',
|
|
130
|
-
'start the daemon once
|
|
130
|
+
'start the daemon once; it creates the token as it starts: goodvibes-daemon serve',
|
|
131
131
|
flags.json,
|
|
132
132
|
);
|
|
133
133
|
}
|
|
@@ -175,7 +175,7 @@ function mintPlanResult(target: RemoteDaemonTarget, json: boolean): DaemonComman
|
|
|
175
175
|
`pair --host: this will MINT A NEW per-device pairing token on the daemon at ${target.baseUrl}`,
|
|
176
176
|
'and print the pairing link/QR for that new token.',
|
|
177
177
|
'',
|
|
178
|
-
"That daemon's existing tokens
|
|
178
|
+
"That daemon's existing tokens (its shared token and every other paired device) are",
|
|
179
179
|
'left exactly as they are: minting is a different act than reprinting, and this is a',
|
|
180
180
|
'fresh token, not a link to one that already exists.',
|
|
181
181
|
'',
|
|
@@ -208,15 +208,15 @@ async function runRemoteMint(input: RunPairCommandInput, target: RemoteDaemonTar
|
|
|
208
208
|
const mintedLine = `minted a new per-device pairing token ("${data.token.name}") on the daemon at ${target.baseUrl}.`;
|
|
209
209
|
|
|
210
210
|
if (data.deepLink === undefined) {
|
|
211
|
-
// Honest degraded path: the mint itself succeeded
|
|
212
|
-
// usable
|
|
211
|
+
// Honest degraded path: the mint itself succeeded, the token is real and
|
|
212
|
+
// usable, but that daemon has no web origin configured, and this process
|
|
213
213
|
// has no way to read or fabricate one for it. `resolvePairingWebOrigin` is
|
|
214
214
|
// what the local form reads instead, and it is exactly the thing this
|
|
215
215
|
// process cannot ask a REMOTE daemon for outside this verb's own reply.
|
|
216
216
|
const lines = [
|
|
217
217
|
mintedLine,
|
|
218
218
|
'',
|
|
219
|
-
'that daemon has no web origin configured, so no deep link or QR could be built
|
|
219
|
+
'that daemon has no web origin configured, so no deep link or QR could be built; only',
|
|
220
220
|
'the raw token and pairing fragment:',
|
|
221
221
|
'',
|
|
222
222
|
` token: ${data.token.token}`,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// provision-wake-model.ts
|
|
2
|
+
// provision-wake-model.ts, `goodvibes-daemon provision-wake-model`.
|
|
3
3
|
//
|
|
4
4
|
// WHAT THIS IS FOR
|
|
5
5
|
//
|
|
6
6
|
// The curl installer needs to put the wake-word model on the machine, and it must
|
|
7
7
|
// not hold the pinned URLs, byte counts or checksums to do it. Those live in ONE
|
|
8
|
-
// place
|
|
8
|
+
// place, the SDK's wake-word manifest, and a shell script that copied them
|
|
9
9
|
// would be a second copy of a pin, drifting silently the first time the model is
|
|
10
10
|
// retrained. So the installer runs the binary it just installed and lets the SDK
|
|
11
11
|
// do what the SDK owns.
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
//
|
|
15
15
|
// The caller is an installer. A wake-word model is not a reason to fail installing
|
|
16
16
|
// a coding tool, and an installer that aborts half-way through is worse than one
|
|
17
|
-
// that finishes without a wake word. The outcome is printed either way
|
|
18
|
-
// line naming what happened and how to retry
|
|
17
|
+
// that finishes without a wake word. The outcome is printed either way, one plain
|
|
18
|
+
// line naming what happened and how to retry, and a running daemon retries at
|
|
19
19
|
// every boot. `--strict` is there for a caller that genuinely wants the exit code
|
|
20
20
|
// to carry the result (a test, a provisioning script that is checking); the
|
|
21
21
|
// installer does not pass it.
|
|
@@ -66,7 +66,7 @@ export async function runProvisionWakeModelCommand(
|
|
|
66
66
|
const message = error instanceof Error ? error.message : String(error);
|
|
67
67
|
return {
|
|
68
68
|
exitCode: strict ? 1 : 0,
|
|
69
|
-
lines: [`wake-word model: skipped
|
|
69
|
+
lines: [`wake-word model: skipped: ${message}`],
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
const outcome = await provision({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* channels.ts
|
|
2
|
+
* channels.ts, which channels `goodvibes-daemon send` can reach, and which one
|
|
3
3
|
* it uses when the operator names none.
|
|
4
4
|
*
|
|
5
5
|
* The surface list, labels and required-setup keys are IMPORTED from
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* list` and `goodvibes-daemon send --list` can never disagree about what a
|
|
8
8
|
* channel is called or what it needs. What this module adds on top is the two
|
|
9
9
|
* facts that command needs and `SURFACE_CONFIGS` does not carry: the routable
|
|
10
|
-
* `ChannelDeliverySurfaceKind` for each surface id (they differ in case
|
|
10
|
+
* `ChannelDeliverySurfaceKind` for each surface id (they differ in case,
|
|
11
11
|
* `googleChat` vs `google-chat`), and the settings key holding each surface's
|
|
12
12
|
* default destination.
|
|
13
13
|
*/
|
|
@@ -18,7 +18,7 @@ import { SURFACE_CONFIGS } from '../../cli/surface-catalog.ts';
|
|
|
18
18
|
import { canRenderInert } from './inert-text.ts';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The settings key holding each supported surface's default destination
|
|
21
|
+
* The settings key holding each supported surface's default destination, the
|
|
22
22
|
* chat, channel, topic or URL a message goes to when `--to` is not given.
|
|
23
23
|
*
|
|
24
24
|
* These are the SAME keys the delivery strategies in the SDK's
|
|
@@ -58,7 +58,7 @@ const SURFACE_KIND_BY_ID: Readonly<Record<string, ChannelDeliverySurfaceKind>> =
|
|
|
58
58
|
* This is naming, not a scheme: `--to` becomes `ChannelDeliveryTarget.address`,
|
|
59
59
|
* which is the first thing every strategy in `strategies-core.ts`,
|
|
60
60
|
* `strategies-bridge.ts` and `strategies-enterprise.ts` already checks before
|
|
61
|
-
* falling back to its configured default. Nothing new is invented
|
|
61
|
+
* falling back to its configured default. Nothing new is invented, the label
|
|
62
62
|
* exists so `--list` and the help can say "topic" where the channel says topic
|
|
63
63
|
* and "chat id" where it says chat id, rather than making the operator work out
|
|
64
64
|
* what an "address" is for ntfy.
|
|
@@ -120,7 +120,7 @@ export const SEND_CHANNELS: readonly SendChannel[] = SURFACE_CONFIGS
|
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Look a channel up by the id an operator typed, accepting either the settings
|
|
123
|
-
* id (`googleChat`) or the routable kind (`google-chat`)
|
|
123
|
+
* id (`googleChat`) or the routable kind (`google-chat`), the two spellings
|
|
124
124
|
* are both visible in this product's own output, and making the operator
|
|
125
125
|
* remember which one this command wants would be a trap.
|
|
126
126
|
*/
|
|
@@ -172,7 +172,7 @@ export type DefaultChannelResolution =
|
|
|
172
172
|
/**
|
|
173
173
|
* Which channel a `send` with no `--channel` goes to.
|
|
174
174
|
*
|
|
175
|
-
* A channel qualifies when it is switched on AND has a destination configured
|
|
175
|
+
* A channel qualifies when it is switched on AND has a destination configured,
|
|
176
176
|
* "enabled" alone is not enough, because an enabled surface with a blank
|
|
177
177
|
* destination is a channel that would throw at the provider rather than deliver.
|
|
178
178
|
*
|
|
@@ -180,7 +180,7 @@ export type DefaultChannelResolution =
|
|
|
180
180
|
* no non-arbitrary winner, and this command has an outward effect: sending the
|
|
181
181
|
* owner's message to the wrong one of his channels is worse than printing the
|
|
182
182
|
* list and exiting non-zero. There is deliberately no priority order here to
|
|
183
|
-
* silently break that tie
|
|
183
|
+
* silently break that tie, a preference ordering baked into this file would be
|
|
184
184
|
* an invisible decision about where his messages go.
|
|
185
185
|
*/
|
|
186
186
|
export function resolveDefaultChannel(config: Pick<ConfigManager, 'get'>): DefaultChannelResolution {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* command.ts
|
|
2
|
+
* command.ts, `goodvibes-daemon send`, the shell's way to put a message on one
|
|
3
3
|
* of the owner's channels.
|
|
4
4
|
*
|
|
5
5
|
* ## Why this exists
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Nothing on this machine could send the owner a message from a script. The
|
|
8
8
|
* three binaries exposed no send verb, the daemon's HTTP API answers
|
|
9
9
|
* `401 AUTH_REQUIRED` to the operator token as stored, and driving the agent to
|
|
10
|
-
* do it failed with `Missing Telegram bot token
|
|
10
|
+
* do it failed with `Missing Telegram bot token`, because the credential lives
|
|
11
11
|
* in the DAEMON tier (`~/.goodvibes/daemon/settings.json`) and the agent was
|
|
12
12
|
* reading its own surface silo. So the one process that could always send was
|
|
13
13
|
* the daemon, and only while it was running.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ## Three properties this command is built around
|
|
16
16
|
*
|
|
17
17
|
* 1. **It uses the delivery path, it is not a second sender.** Everything below
|
|
18
|
-
* ends in `ChannelDeliveryRouter.deliver()
|
|
18
|
+
* ends in `ChannelDeliveryRouter.deliver()`, the same call
|
|
19
19
|
* `AutomationDeliveryManager.sendTarget` makes, reaching the same
|
|
20
20
|
* per-surface strategies in `strategies-core.ts`. Nothing here talks to a
|
|
21
21
|
* provider API directly.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* was NOT used, deliberately: it returns an empty array when a feature gate
|
|
27
27
|
* is off and returns failed attempts rather than throwing, so a caller that
|
|
28
28
|
* did not inspect its result would report success for a message that never
|
|
29
|
-
* left the machine
|
|
29
|
+
* left the machine, the exact false-green this command exists to avoid. The
|
|
30
30
|
* gate check it would have done is done here instead, explicitly, and a gate
|
|
31
31
|
* that is off produces a refusal naming the settings key rather than silence.
|
|
32
32
|
*
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* through `inertBodyFor` for the target surface before it reaches the router,
|
|
35
35
|
* and there is no flag, env var or code path that skips that. The message
|
|
36
36
|
* normally comes from the operator's own shell, but the command must not
|
|
37
|
-
* become the way something else's text
|
|
38
|
-
* remote agent's output piped in
|
|
37
|
+
* become the way something else's text, a log line, a captured error, a
|
|
38
|
+
* remote agent's output piped in, arrives on the owner's phone rendered as
|
|
39
39
|
* live markup with a clickable link in it. See inert-text.ts.
|
|
40
40
|
*/
|
|
41
41
|
|
|
@@ -51,7 +51,7 @@ import { inertBodyFor } from './inert-text.ts';
|
|
|
51
51
|
export type SendDeliver = (request: ChannelDeliveryRequest) => Promise<string | undefined>;
|
|
52
52
|
|
|
53
53
|
export interface SendCommandDeps {
|
|
54
|
-
/** Built with a `homeDir` so the daemon tier overlays
|
|
54
|
+
/** Built with a `homeDir` so the daemon tier overlays, see channels.ts. */
|
|
55
55
|
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
56
56
|
readonly deliver: SendDeliver;
|
|
57
57
|
/** Reads the whole of stdin; only called when no message argument was given. */
|
|
@@ -93,12 +93,12 @@ const USAGE = [
|
|
|
93
93
|
' --list Show every channel, whether it is on, and where it sends.',
|
|
94
94
|
' -h, --help Print this help',
|
|
95
95
|
'',
|
|
96
|
-
'A channel that is switched off is refused by name and NOTHING is sent
|
|
96
|
+
'A channel that is switched off is refused by name and NOTHING is sent: the',
|
|
97
97
|
'command never quietly falls back to the default, so a message meant for a',
|
|
98
98
|
'quiet channel cannot end up on a noisy one.',
|
|
99
99
|
'',
|
|
100
100
|
'The message is always delivered as literal text: markup a channel would',
|
|
101
|
-
'otherwise render
|
|
101
|
+
'otherwise render (a Discord masked link, a Slack mention) arrives inert.',
|
|
102
102
|
].join('\n');
|
|
103
103
|
|
|
104
104
|
function parseSendArgs(argv: readonly string[]): ParsedSendArgs {
|
|
@@ -153,7 +153,7 @@ function describeConfiguredChannels(config: Pick<ConfigManager, 'get'>): string
|
|
|
153
153
|
.map((entry) => entry.channel.id);
|
|
154
154
|
return usable.length > 0
|
|
155
155
|
? `Configured and ready: ${usable.join(', ')}.`
|
|
156
|
-
: 'No channel is currently both switched on and given a destination
|
|
156
|
+
: 'No channel is currently both switched on and given a destination; run: goodvibes-daemon send --list';
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function renderChannelList(config: Pick<ConfigManager, 'get'>): string[] {
|
|
@@ -304,7 +304,7 @@ export async function runSendCommand(
|
|
|
304
304
|
};
|
|
305
305
|
|
|
306
306
|
const preamble = usedDefault
|
|
307
|
-
? [`No channel named
|
|
307
|
+
? [`No channel named; using ${channel.id}: ${defaultReason}.`]
|
|
308
308
|
: [];
|
|
309
309
|
try {
|
|
310
310
|
const responseId = await deps.deliver(request);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* composition.ts
|
|
2
|
+
* composition.ts, the smallest set of services that can put a message on a
|
|
3
3
|
* channel, built from the daemon's own tier.
|
|
4
4
|
*
|
|
5
5
|
* ## Why not `createRuntimeServices`
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* The daemon's full runtime graph starts a LAN scan, a cluster coordinator, an
|
|
8
8
|
* inbox poller, a fleet tick, a memory governor and a config watch. Composing
|
|
9
9
|
* it to send one message would build a second, competing set of that state on a
|
|
10
|
-
* machine that is already running a daemon
|
|
10
|
+
* machine that is already running a daemon, the same reason `cluster …` is
|
|
11
11
|
* intercepted before any runtime is constructed (see src/daemon/cli.ts). This
|
|
12
12
|
* builds only the five objects `ChannelDeliveryRouter` needs and starts no
|
|
13
13
|
* timers, binds no sockets and joins no election, so it is safe to run beside a
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* That was tried before this command existed: the control-plane API answers
|
|
19
19
|
* `401 AUTH_REQUIRED` to the operator token as stored on disk. More importantly
|
|
20
20
|
* an HTTP-backed send would only work while a daemon is up, and the case this
|
|
21
|
-
* command is for
|
|
21
|
+
* command is for, telling the owner that something has stopped, is exactly
|
|
22
22
|
* when it may not be.
|
|
23
23
|
*
|
|
24
24
|
* ## Where the credentials come from
|
|
@@ -42,7 +42,7 @@ import { GOODVIBES_DAEMON_SURFACE_ROOT } from '../../config/surface.ts';
|
|
|
42
42
|
|
|
43
43
|
export interface SendStackRoots {
|
|
44
44
|
readonly workingDirectory: string;
|
|
45
|
-
/** The GoodVibes tree root
|
|
45
|
+
/** The GoodVibes tree root, the directory `.goodvibes/` sits under. */
|
|
46
46
|
readonly homeDirectory: string;
|
|
47
47
|
/** The daemon's own state root, holding the daemon-scoped secret stores. */
|
|
48
48
|
readonly daemonHomeDirectory: string;
|
|
@@ -62,7 +62,7 @@ export interface SendStack {
|
|
|
62
62
|
* roots (goodvibes-tui and goodvibes-agent) omitted it, still type-checked,
|
|
63
63
|
* still delivered on every surface whose credential happens to sit in config or
|
|
64
64
|
* the environment, and failed only on the surfaces that use a secret reference
|
|
65
|
-
|
|
65
|
+
*, at send time, as `Missing Telegram bot token`. This composition root does
|
|
66
66
|
* not repeat that.
|
|
67
67
|
*/
|
|
68
68
|
export function createSendStack(roots: SendStackRoots): SendStack {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* failure-text.ts
|
|
2
|
+
* failure-text.ts, what the operator is told when a send does not happen.
|
|
3
3
|
*
|
|
4
4
|
* ## Why this is not `summarizeError`
|
|
5
5
|
*
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Telegram delivery failed HTTP 401: {"ok":false,"description":"Unauthorized"}
|
|
12
12
|
*
|
|
13
|
-
* it returns `Telegram delivery failed HTTP 401
|
|
13
|
+
* it returns `Telegram delivery failed HTTP 401`, its `stripJson` pass removes
|
|
14
14
|
* the `{...}`, which is exactly the part naming what went wrong. Given
|
|
15
15
|
* `connect ECONNREFUSED 149.154.167.220:443` it returns "Cannot connect to the
|
|
16
|
-
* provider. Check whether the service is reachable."
|
|
16
|
+
* provider. Check whether the service is reachable.", advice in place of the
|
|
17
17
|
* address that was refused.
|
|
18
18
|
*
|
|
19
19
|
* A person debugging a message that did not arrive needs the provider's own
|
|
@@ -39,7 +39,7 @@ import { redactSensitiveData } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* A generous cap. The point of this text is diagnosis, so it is far larger than
|
|
42
|
-
* the 240 characters `summarizeError` allows
|
|
42
|
+
* the 240 characters `summarizeError` allows, but an HTML error page or a
|
|
43
43
|
* multi-megabyte body still must not flood the terminal.
|
|
44
44
|
*/
|
|
45
45
|
const MAX_FAILURE_TEXT = 2_000;
|
|
@@ -47,10 +47,10 @@ const MAX_FAILURE_TEXT = 2_000;
|
|
|
47
47
|
const URL_CREDENTIAL_PATTERNS: ReadonlyArray<{ readonly pattern: RegExp; readonly replacement: string }> = [
|
|
48
48
|
// Telegram: the bot token IS the path segment.
|
|
49
49
|
{ pattern: /\/bot\d{5,}:[A-Za-z0-9_-]{10,}/g, replacement: '/bot[REDACTED_BOT_TOKEN]' },
|
|
50
|
-
// Any URL carrying `user:password@host
|
|
50
|
+
// Any URL carrying `user:password@host`, BlueBubbles, Mattermost and Matrix
|
|
51
51
|
// base URLs are all operator-supplied and can be written this way.
|
|
52
52
|
{ pattern: /(\b[a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s@]+@/gi, replacement: '$1[REDACTED_CREDENTIALS]@' },
|
|
53
|
-
// A credential passed as a query parameter
|
|
53
|
+
// A credential passed as a query parameter, BlueBubbles sends `?password=`.
|
|
54
54
|
{
|
|
55
55
|
pattern: /([?&](?:password|token|secret|access_token|api_?key|auth)=)[^&\s"']+/gi,
|
|
56
56
|
replacement: '$1[REDACTED]',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* inert-text.ts
|
|
2
|
+
* inert-text.ts, make a message arrive as LITERAL TEXT on whichever channel
|
|
3
3
|
* `goodvibes-daemon send` delivers it to.
|
|
4
4
|
*
|
|
5
5
|
* ## What "inert" means here, and why it is per (surface × transport), not
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* - Telegram's `sendMessage` CAN parse MarkdownV2, but the strategy sends with
|
|
14
14
|
* NO `parse_mode`, which is Telegram's plain-text mode. Running a MarkdownV2
|
|
15
|
-
* escaper over the body on this path would not protect anything
|
|
15
|
+
* escaper over the body on this path would not protect anything, it would
|
|
16
16
|
* put visible backslashes in front of every `.` `-` `!` `(` in the owner's
|
|
17
17
|
* message. The correct transform is identity, and the property that makes it
|
|
18
18
|
* correct is asserted by a test against the real wire payload rather than
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* `escapeDiscordMarkdown` and `escapeSlackMrkdwn` are the escapers written for
|
|
31
31
|
* the inbound-mail structured notices, in the SDK at
|
|
32
|
-
* `packages/sdk/src/platform/email/inbound-notice.ts
|
|
32
|
+
* `packages/sdk/src/platform/email/inbound-notice.ts`, same character classes,
|
|
33
33
|
* same zero-width-space mention break, same rationale. They are reproduced here
|
|
34
34
|
* rather than imported for one reason: that module lives on the unmerged
|
|
35
35
|
* `inbound-email-*` branches and is absent from the published
|
|
@@ -72,8 +72,8 @@ function breakMentionForms(text: string): string {
|
|
|
72
72
|
*
|
|
73
73
|
* `[` `]` `(` `)` are escaped too, and this is REQUIRED rather than
|
|
74
74
|
* precautionary: masked links (`[text](url)`) DO render as clickable in
|
|
75
|
-
* bot-sent and webhook messages
|
|
76
|
-
* Discord
|
|
75
|
+
* bot-sent and webhook messages, which is exactly how this product delivers to
|
|
76
|
+
* Discord, even though they do not render for text a human typed into the
|
|
77
77
|
* client.
|
|
78
78
|
* https://github.com/discord/discord-api-docs/issues/6096
|
|
79
79
|
* https://gist.github.com/matthewzring/9f7bbfd102003963f9be7dbcf7d40e51
|
|
@@ -97,11 +97,11 @@ function escapeDiscordMarkdown(text: string): string {
|
|
|
97
97
|
* unescaped `<`.
|
|
98
98
|
*
|
|
99
99
|
* Slack has no backslash escape for `* _ ~ \``, so the zero-width break is
|
|
100
|
-
* applied to those as the best available mitigation
|
|
100
|
+
* applied to those as the best available mitigation, stated as a mitigation,
|
|
101
101
|
* not a guarantee, because Slack's whitespace-adjacency rule for what breaks a
|
|
102
102
|
* delimiter pair is not publicly specified to that precision. The residual risk
|
|
103
103
|
* is cosmetic (accidental bold/italic); the injection class this exists to
|
|
104
|
-
* close
|
|
104
|
+
* close, a clickable link or a real mention, is closed by the entity escaping
|
|
105
105
|
* above.
|
|
106
106
|
*/
|
|
107
107
|
function escapeSlackMrkdwn(text: string): string {
|
|
@@ -122,8 +122,8 @@ function escapeGoogleChatMarkup(text: string): string {
|
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* WhatsApp Cloud API text messages render `*bold*`, `_italic_`, `~strike~` and
|
|
125
|
-
* ```` ```mono``` ````. There is no masked-link syntax
|
|
126
|
-
* auto-linked showing its real address
|
|
125
|
+
* ```` ```mono``` ````. There is no masked-link syntax, a URL in the body is
|
|
126
|
+
* auto-linked showing its real address, so the injection class Discord has
|
|
127
127
|
* does not exist here and only the delimiters need neutralizing. WhatsApp
|
|
128
128
|
* documents no backslash escape, so the zero-width break is used.
|
|
129
129
|
*/
|
|
@@ -136,24 +136,24 @@ function escapeWhatsAppMarkup(text: string): string {
|
|
|
136
136
|
* markup. Every other routable surface is absent DELIBERATELY, and each absence
|
|
137
137
|
* is a checked claim about the strategy, not an oversight:
|
|
138
138
|
*
|
|
139
|
-
* - `telegram`
|
|
139
|
+
* - `telegram` , `sendMessage` without `parse_mode`; Telegram's plain-text
|
|
140
140
|
* mode. Escaping here would corrupt, not protect.
|
|
141
|
-
* - `ntfy`
|
|
141
|
+
* - `ntfy` , `publish` sends the body as `text/plain` and never sets the
|
|
142
142
|
* `Markdown` header, so ntfy renders it literally. The title
|
|
143
143
|
* is derived by `titleFromBody` (first non-empty LINE) and
|
|
144
144
|
* passed through `toHeaderSafeTitle`, so no line break or
|
|
145
145
|
* non-ASCII byte from the body can reach an HTTP header.
|
|
146
|
-
* - `webhook`
|
|
146
|
+
* - `webhook` , the body is a JSON string field; the receiver decides what
|
|
147
147
|
* to do with it and there is no markup layer to neutralize.
|
|
148
148
|
*
|
|
149
|
-
* - `signal`
|
|
149
|
+
* - `signal` , the strategy posts `text` to a signal bridge. Signal renders
|
|
150
150
|
* no markup in a plain message body; styling travels as
|
|
151
151
|
* explicit range metadata the strategy never sends.
|
|
152
152
|
* - `imessage`,
|
|
153
|
-
* `bluebubbles
|
|
154
|
-
* - `msteams`
|
|
153
|
+
* `bluebubbles`, both end at iMessage, which renders no markup at all.
|
|
154
|
+
* - `msteams` , the strategy sends `textFormat: 'plain'` alongside the text,
|
|
155
155
|
* which is Teams' own instruction not to parse it.
|
|
156
|
-
* - `matrix`
|
|
156
|
+
* - `matrix` , `msgtype: 'm.text'` with no `format`/`formatted_body`. A
|
|
157
157
|
* Matrix event without the HTML format field is rendered
|
|
158
158
|
* literally by clients, per the spec.
|
|
159
159
|
*
|
|
@@ -161,7 +161,7 @@ function escapeWhatsAppMarkup(text: string): string {
|
|
|
161
161
|
* `ControlPlaneGateway` in the same process, which a short-lived CLI does not
|
|
162
162
|
* have, so the command does not offer it as a channel at all. `telephony` is
|
|
163
163
|
* absent because a CLI `send` to it would place a phone call or an SMS through
|
|
164
|
-
* a paid carrier
|
|
164
|
+
* a paid carrier, a different act from messaging a channel, and because the
|
|
165
165
|
* strategy already XML-escapes the voice path itself via `escapeTwiml`.
|
|
166
166
|
*/
|
|
167
167
|
const INERT_TRANSFORMS: Partial<Record<ChannelDeliverySurfaceKind, (text: string) => string>> = {
|
|
@@ -173,7 +173,7 @@ const INERT_TRANSFORMS: Partial<Record<ChannelDeliverySurfaceKind, (text: string
|
|
|
173
173
|
'google-chat': escapeGoogleChatMarkup,
|
|
174
174
|
whatsapp: escapeWhatsAppMarkup,
|
|
175
175
|
// Delivered as plain text by the strategy: transforming would corrupt, not
|
|
176
|
-
// protect. Each of these is a checked claim about the strategy, not a guess
|
|
176
|
+
// protect. Each of these is a checked claim about the strategy, not a guess,
|
|
177
177
|
// see the list above.
|
|
178
178
|
telegram: (text) => text,
|
|
179
179
|
ntfy: (text) => text,
|
package/src/daemon/send/stdin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* stdin.ts
|
|
2
|
+
* stdin.ts, read the whole of stdin as the message body.
|
|
3
3
|
*
|
|
4
4
|
* Kept out of command.ts so the command stays a pure function of its arguments
|
|
5
5
|
* and its injected dependencies: every test drives it with a stub rather than
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Read stdin to end, as UTF-8.
|
|
11
11
|
*
|
|
12
12
|
* There is deliberately no timeout. `send` reads stdin only when the operator
|
|
13
|
-
* gave no message argument AND stdin is not a terminal
|
|
14
|
-
* piping into it
|
|
13
|
+
* gave no message argument AND stdin is not a terminal, meaning something is
|
|
14
|
+
* piping into it, so waiting for that producer to finish is the correct
|
|
15
15
|
* behaviour, and cutting it off at an arbitrary deadline would silently
|
|
16
16
|
* truncate a long message.
|
|
17
17
|
*/
|