@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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// * The mirror is already live. Each adapter polls its provider on that
|
|
10
10
|
// provider's own cadence (Slack/Discord 30s, email 60s) and writes what it
|
|
11
|
-
// pulls straight into the store
|
|
11
|
+
// pulls straight into the store, that is what the triage pipeline scores.
|
|
12
12
|
// Reading it IS reading what has arrived.
|
|
13
13
|
// * A fetch-per-call would put a third-party rate limit behind a read verb
|
|
14
14
|
// any client may call at any rate, so one impatient UI could get every
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// * The cluster hands FETCHING for each inbox account to one elected node
|
|
17
17
|
// (runtime/cluster-composition.ts). A read that fetched would make every
|
|
18
18
|
// standby node fetch too, which is exactly the double-read the election
|
|
19
|
-
// exists to prevent
|
|
19
|
+
// exists to prevent, while the READ is deliberately ungated so a standby
|
|
20
20
|
// still answers.
|
|
21
21
|
// * Triage scores are applied as items are persisted. Items fetched inline
|
|
22
22
|
// would arrive unscored, so the verb would answer two different shapes
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
//
|
|
27
27
|
// The cost of serving a mirror is that its age is not visible in the items. So
|
|
28
28
|
// the aggregator does not leave it implicit: `providers` reports every provider
|
|
29
|
-
// this node knows about on EVERY call
|
|
29
|
+
// this node knows about on EVERY call, its state, when it last synced, how
|
|
30
30
|
// much of the mirror is its, and whether this node is the one fetching it.
|
|
31
31
|
//
|
|
32
32
|
// ── Honest partial results ────────────────────────────────────────────────
|
|
@@ -119,7 +119,7 @@ export interface InboxAggregatorSources {
|
|
|
119
119
|
/**
|
|
120
120
|
* A page cursor is the position of the last item handed out, base64url-encoded
|
|
121
121
|
* so it reads as opaque and callers do not build one by hand. It is NOT the
|
|
122
|
-
* `cursor` field in the answer
|
|
122
|
+
* `cursor` field in the answer, that one is the freshness watermark a caller
|
|
123
123
|
* feeds back as `since`. Two different questions ("what is new" vs "the next
|
|
124
124
|
* page down"), so two different values; collapsing them is how a paging client
|
|
125
125
|
* ends up silently re-reading page one forever.
|
|
@@ -234,7 +234,7 @@ export function toWireItem(item: InboundChannelItem): ChannelInboxItem {
|
|
|
234
234
|
*
|
|
235
235
|
* The merge itself is the store's ordering (receivedAt DESC, id ASC across all
|
|
236
236
|
* providers), so items interleave by arrival rather than being grouped by
|
|
237
|
-
* provider
|
|
237
|
+
* provider, an inbox is a timeline, and each item carries its own `provider`
|
|
238
238
|
* so attribution survives the merge.
|
|
239
239
|
*/
|
|
240
240
|
export function aggregateInbox(
|
|
@@ -300,7 +300,7 @@ export function aggregateInbox(
|
|
|
300
300
|
* Turn the poller's per-provider record into the wire statuses.
|
|
301
301
|
*
|
|
302
302
|
* Every provider the node has an adapter for appears, including ones that
|
|
303
|
-
* contributed nothing and ones nobody has configured
|
|
303
|
+
* contributed nothing and ones nobody has configured, a provider missing from
|
|
304
304
|
* this list would be a hole the caller could not even see.
|
|
305
305
|
*/
|
|
306
306
|
function describeProviders(input: {
|
|
@@ -313,7 +313,7 @@ function describeProviders(input: {
|
|
|
313
313
|
const known = poller.snapshotStatuses(requested);
|
|
314
314
|
|
|
315
315
|
// A `?provider=` filter naming something this node has no adapter for gets an
|
|
316
|
-
// empty list and no status, which reads as "we have nothing"
|
|
316
|
+
// empty list and no status, which reads as "we have nothing", so it is
|
|
317
317
|
// reported explicitly instead.
|
|
318
318
|
const reported = new Set(known.map((status) => status.id));
|
|
319
319
|
const unknownRequested = (requested ?? []).filter((id) => !reported.has(id));
|
|
@@ -354,7 +354,7 @@ function describeProviders(input: {
|
|
|
354
354
|
* Map the poller's internal state onto the wire vocabulary.
|
|
355
355
|
*
|
|
356
356
|
* The internal 'unavailable' covers two situations the wire deliberately keeps
|
|
357
|
-
* apart
|
|
357
|
+
* apart, nothing configured, versus configured and failing, because a caller
|
|
358
358
|
* does something different about each.
|
|
359
359
|
*
|
|
360
360
|
* ready-vs-empty is decided by the MIRROR, not by the last poll. The poller's
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
// separate store, so this feed store carries only the raw inbound fields.
|
|
16
16
|
//
|
|
17
17
|
// RETENTION. The items table is bounded by BOTH an age TTL and a count cap, and
|
|
18
|
-
// the sweep runs at init()
|
|
19
|
-
// is opened
|
|
18
|
+
// the sweep runs at init(), the recovery point, right after the database file
|
|
19
|
+
// is opened, and then on a timer for the life of the store, so a daemon that
|
|
20
20
|
// stays up for weeks keeps reclaiming. `pruneOlderThan` used to exist with no
|
|
21
21
|
// production caller at all, which meant the table grew without bound in
|
|
22
|
-
// practice. Reclaimed counts are handed to the `onSweep` hook (counts only
|
|
22
|
+
// practice. Reclaimed counts are handed to the `onSweep` hook (counts only,
|
|
23
23
|
// message previews and sender ids never reach a log line).
|
|
24
24
|
//
|
|
25
25
|
// Cursors are deliberately NOT reaped: they are monotonic watermarks, so
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// Idempotence/concurrency: a sweep re-run immediately reclaims nothing (the
|
|
29
29
|
// DELETEs are set-based over the current contents). Two processes opening the
|
|
30
30
|
// same file each hold their own sql.js snapshot and `save()` writes the whole
|
|
31
|
-
// file via temp+rename, so the last writer wins
|
|
31
|
+
// file via temp+rename, so the last writer wins, that whole-file model is
|
|
32
32
|
// HandlerSqliteStore's, and deletion converging on the same surviving set is
|
|
33
33
|
// what makes concurrent sweeps safe rather than corrupting.
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
@@ -45,7 +45,7 @@ export const INBOX_ITEM_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Count cap for feed items: the newest this many rows survive a sweep, older
|
|
48
|
-
* ones are dropped. Guards the case the TTL cannot
|
|
48
|
+
* ones are dropped. Guards the case the TTL cannot, a very chatty month.
|
|
49
49
|
*/
|
|
50
50
|
export const INBOX_ITEM_CAP = 5_000;
|
|
51
51
|
|
|
@@ -57,7 +57,7 @@ export const INBOX_SWEEP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
|
57
57
|
|
|
58
58
|
const DEFAULT_STORE_FILE_NAME = 'inbox.sqlite';
|
|
59
59
|
|
|
60
|
-
/** Result of one retention sweep. Counts only
|
|
60
|
+
/** Result of one retention sweep. Counts only, never item content. */
|
|
61
61
|
export interface InboxSweepSummary {
|
|
62
62
|
/** Unix ms of the sweep. */
|
|
63
63
|
readonly at: number;
|
|
@@ -202,7 +202,7 @@ export class InboxCursorStore {
|
|
|
202
202
|
/**
|
|
203
203
|
* One retention pass: age TTL first, then the count cap over what is left.
|
|
204
204
|
* Returns counts only. Running it twice in a row reclaims nothing the second
|
|
205
|
-
* time
|
|
205
|
+
* time, the pass is a function of the table's current contents.
|
|
206
206
|
*/
|
|
207
207
|
sweepRetention(): InboxSweepSummary {
|
|
208
208
|
const at = this.now();
|
|
@@ -240,7 +240,7 @@ export class InboxCursorStore {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
|
-
* Count cap: keep the newest `itemCap` rows (receivedAt DESC, id ASC
|
|
243
|
+
* Count cap: keep the newest `itemCap` rows (receivedAt DESC, id ASC, the
|
|
244
244
|
* same order listItems() uses), delete the rest. Returns rows removed.
|
|
245
245
|
*/
|
|
246
246
|
private enforceItemCap(): number {
|
|
@@ -266,7 +266,7 @@ export class InboxCursorStore {
|
|
|
266
266
|
if (items.length === 0) return 0;
|
|
267
267
|
let inserted = 0;
|
|
268
268
|
// Only the ids in THIS batch can collide, so probe for exactly those rather
|
|
269
|
-
// than loading the whole table
|
|
269
|
+
// than loading the whole table, bounds the lookup to the poll size instead
|
|
270
270
|
// of growing O(n) with the (unbounded) feed.
|
|
271
271
|
const batchIds = [...new Set(items.map((i) => i.id))];
|
|
272
272
|
const placeholders = batchIds.map(() => '?').join(', ');
|
|
@@ -364,7 +364,7 @@ export class InboxCursorStore {
|
|
|
364
364
|
if (query.after && Number.isFinite(query.after.receivedAt)) {
|
|
365
365
|
// Strictly after the cursor row in (receivedAt DESC, id ASC) order: an
|
|
366
366
|
// older timestamp, or the same timestamp with a higher id. Both halves
|
|
367
|
-
// are needed
|
|
367
|
+
// are needed, several items can share a receivedAt, and comparing on
|
|
368
368
|
// the timestamp alone would drop every one of its ties.
|
|
369
369
|
clauses.push('(receivedAt < ? OR (receivedAt = ? AND id > ?))');
|
|
370
370
|
const at = Math.floor(query.after.receivedAt);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//
|
|
5
5
|
// The SDK already declares the method id, input schema, output schema, scopes
|
|
6
6
|
// (read:channels) and HTTP binding (GET /api/channels/inbox). This module does
|
|
7
|
-
// NOT re-declare any of that
|
|
7
|
+
// NOT re-declare any of that, it looks the descriptor up via the catalog and
|
|
8
8
|
// attaches an implementation with `registerCatalogHandler` ({ replace: true }).
|
|
9
9
|
//
|
|
10
10
|
// register(ctx, routing) =>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// 6. returns an Unregister that detaches the handler, stops the poller, and
|
|
18
18
|
// closes the store.
|
|
19
19
|
//
|
|
20
|
-
// The handler composes the answer in ./aggregator.ts
|
|
20
|
+
// The handler composes the answer in ./aggregator.ts, the merged, paginated
|
|
21
21
|
// page plus every provider's standing. Read that file's header for WHY the
|
|
22
22
|
// answer comes from the synced mirror rather than a fresh remote fetch. The
|
|
23
23
|
// redacted `fromDigest` is the only sender value emitted (as `from`); raw
|
|
@@ -78,13 +78,13 @@ export interface RegisterInboxOptions {
|
|
|
78
78
|
* relies on.
|
|
79
79
|
*
|
|
80
80
|
* The READ path is never gated. `channels.inbox.list` serves the persisted
|
|
81
|
-
* feed on every node
|
|
81
|
+
* feed on every node, a node that is not fetching still answers questions
|
|
82
82
|
* about what has already arrived.
|
|
83
83
|
*
|
|
84
84
|
* Called once PER PROVIDER, with that provider's id and a control that
|
|
85
85
|
* starts and stops only its loop. Each inbox account is its own surface in
|
|
86
86
|
* the LAN election, so the machine reading the work Slack account need not
|
|
87
|
-
* be the machine reading the mailbox
|
|
87
|
+
* be the machine reading the mailbox, and handing one account over must not
|
|
88
88
|
* take the others down with it.
|
|
89
89
|
*/
|
|
90
90
|
gatePolling?: (providerId: string, control: InboxPollingControl) => void;
|
|
@@ -132,7 +132,7 @@ export function registerInboxMethods(
|
|
|
132
132
|
};
|
|
133
133
|
const adapters = buildAdapters(adapterContext);
|
|
134
134
|
// Retention runs inside the store (age TTL + count cap, at init and then on a
|
|
135
|
-
// timer). Both hooks carry COUNTS ONLY
|
|
135
|
+
// timer). Both hooks carry COUNTS ONLY, no sender ids, subjects or bodies.
|
|
136
136
|
const store = new InboxCursorStore(ctx.workingDirectory, options.storeFileName, {
|
|
137
137
|
onSweep: (summary) => {
|
|
138
138
|
ctx.logger.info('inbox retention sweep reclaimed items', {
|
|
@@ -150,7 +150,7 @@ export function registerInboxMethods(
|
|
|
150
150
|
const gated = options.gatePolling !== undefined;
|
|
151
151
|
|
|
152
152
|
// Async bootstrap: init store, and (ungated) seed one poll and start loops.
|
|
153
|
-
// Failures are logged but never thrown out of register()
|
|
153
|
+
// Failures are logged but never thrown out of register(), the handler still
|
|
154
154
|
// serves the (possibly empty) persisted feed.
|
|
155
155
|
const ready: Promise<void> = (async () => {
|
|
156
156
|
await store.init();
|
|
@@ -169,7 +169,7 @@ export function registerInboxMethods(
|
|
|
169
169
|
if (gate) {
|
|
170
170
|
// One gate per provider, not one for the poller. Leadership is decided per
|
|
171
171
|
// inbox account, so each account's loop has to be startable and stoppable
|
|
172
|
-
// on its own
|
|
172
|
+
// on its own, otherwise handing one account to another machine would stop
|
|
173
173
|
// fetching for every account this node reads.
|
|
174
174
|
for (const providerId of poller.providerIds()) {
|
|
175
175
|
gate(providerId, {
|
|
@@ -54,7 +54,7 @@ const PREFIXED_KEY_RE =
|
|
|
54
54
|
const KV_SECRET_RE =
|
|
55
55
|
/\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|secret|token|password|passwd|pwd|authorization|auth)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|[A-Za-z0-9._~+/=-]{6,})/gi;
|
|
56
56
|
// Generic high-entropy opaque blobs (>=24 chars of base64url/hex) not already
|
|
57
|
-
// caught above
|
|
57
|
+
// caught above, catches raw API keys pasted without a recognizable prefix.
|
|
58
58
|
const OPAQUE_SECRET_RE = /\b[A-Za-z0-9_-]{24,}\b/g;
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -169,7 +169,7 @@ export function stripMarkup(input: string): string {
|
|
|
169
169
|
return decodeEntities(text);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
/** Collapse whitespace and trim
|
|
172
|
+
/** Collapse whitespace and trim, keeps previews single-line and tidy. */
|
|
173
173
|
export function normalizeWhitespace(input: string): string {
|
|
174
174
|
return input.replace(/\s+/g, ' ').trim();
|
|
175
175
|
}
|
|
@@ -68,7 +68,7 @@ export class InboundPoller {
|
|
|
68
68
|
private readonly inFlight = new Set<string>();
|
|
69
69
|
private started = false;
|
|
70
70
|
/**
|
|
71
|
-
* Set by stop(), which the surface teardown calls and nothing else
|
|
71
|
+
* Set by stop(), which the surface teardown calls and nothing else, unlike
|
|
72
72
|
* stopProvider(), which a leadership handover uses and which must stay
|
|
73
73
|
* resumable. Once the surface is released no interval may be armed again,
|
|
74
74
|
* including by work that was already in flight: registerInboxMethods() starts
|
|
@@ -85,7 +85,7 @@ export class InboundPoller {
|
|
|
85
85
|
this.setIntervalImpl = options.setIntervalImpl ?? setInterval;
|
|
86
86
|
this.clearIntervalImpl = options.clearIntervalImpl ?? clearInterval;
|
|
87
87
|
for (const id of this.adapters.keys()) {
|
|
88
|
-
// `polled: false` until a poll actually completes
|
|
88
|
+
// `polled: false` until a poll actually completes, see ProviderStatus.
|
|
89
89
|
this.statuses.set(id, { id, state: 'empty', itemCount: 0, polled: false });
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -121,8 +121,8 @@ export class InboundPoller {
|
|
|
121
121
|
/**
|
|
122
122
|
* Stop ONE provider's interval loop. Idempotent.
|
|
123
123
|
*
|
|
124
|
-
* Synchronous underneath
|
|
125
|
-
* be scheduled
|
|
124
|
+
* Synchronous underneath, clearing the interval means no further tick can
|
|
125
|
+
* be scheduled, which is what lets the RESIGN that follows a handoff be an
|
|
126
126
|
* honest claim that this node has stopped reading that account.
|
|
127
127
|
*/
|
|
128
128
|
stopProvider(id: string): void {
|
|
@@ -195,7 +195,7 @@ export class InboundPoller {
|
|
|
195
195
|
const message = summarizeError(error);
|
|
196
196
|
this.logger.warn('inbound poll failed', { provider: id, error: message });
|
|
197
197
|
// A THROW rather than an honest 'unavailable' result. The adapter got far
|
|
198
|
-
// enough to run, so the credentials are not the thing that failed
|
|
198
|
+
// enough to run, so the credentials are not the thing that failed,
|
|
199
199
|
// reporting it as configured is what keeps this out of the
|
|
200
200
|
// "you never set this up" bucket.
|
|
201
201
|
this.setStatus(id, {
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// Each adapter owns ONE provider (slack, discord, email, ...). The poller calls
|
|
5
5
|
// adapter.poll() on a cadence, dedups by item.id, and persists into the cursor
|
|
6
6
|
// store. Adapters resolve credentials ONLY through the daemon credential store
|
|
7
|
-
// and NEVER return raw sender ids or unredacted bodies
|
|
7
|
+
// and NEVER return raw sender ids or unredacted bodies, mapping/redaction is
|
|
8
8
|
// the adapter's responsibility (see mapping helpers in `./mapping.ts`).
|
|
9
9
|
//
|
|
10
10
|
// CRITICAL: when a credential is missing/misconfigured an adapter returns
|
|
11
|
-
// state 'unavailable' WITH an error string. It is NEVER silently omitted
|
|
11
|
+
// state 'unavailable' WITH an error string. It is NEVER silently omitted, the
|
|
12
12
|
// caller must be able to distinguish "configured-but-empty" from "not wired".
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ import type { DaemonCredentialStore } from '../credentials.ts';
|
|
|
16
16
|
import type { HandlerLogger } from '../context.ts';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Daemon-internal inbound item. This is NOT the SDK wire shape
|
|
19
|
+
* Daemon-internal inbound item. This is NOT the SDK wire shape, the inbox
|
|
20
20
|
* surface maps it onto the SDK `CHANNEL_INBOX_ITEM_SCHEMA` shape
|
|
21
21
|
* (`from`/`subject`/`bodyPreview`/...) before returning. `fromDigest` is the
|
|
22
22
|
* redacted sender (the only sender value that ever leaves the daemon).
|
|
@@ -27,7 +27,7 @@ export interface InboundChannelItem {
|
|
|
27
27
|
provider: string;
|
|
28
28
|
kind: 'dm' | 'thread' | 'mention' | 'reaction';
|
|
29
29
|
/**
|
|
30
|
-
* sha256First(senderExternalId, 16)
|
|
30
|
+
* sha256First(senderExternalId, 16), NEVER the raw id. 16 hex chars == the
|
|
31
31
|
* first 8 bytes of the SHA-256 digest.
|
|
32
32
|
*/
|
|
33
33
|
fromDigest: string;
|
|
@@ -61,9 +61,9 @@ export interface ProviderPollResult {
|
|
|
61
61
|
* is an outage hiding items that exist, and `channels.inbox.list` reports
|
|
62
62
|
* them as different states because a caller acts on them differently.
|
|
63
63
|
*
|
|
64
|
-
* `false
|
|
65
|
-
* `true`
|
|
66
|
-
* absent
|
|
64
|
+
* `false`, no credential (or an unusable one), so nothing was even asked.
|
|
65
|
+
* `true` , credentials resolved; whatever happened next happened WITH them.
|
|
66
|
+
* absent , the adapter could not find out (the credential store itself
|
|
67
67
|
* failed), which is neither claim and is reported as neither.
|
|
68
68
|
*/
|
|
69
69
|
configured?: boolean;
|
|
@@ -108,7 +108,7 @@ export interface InboundProviderAdapter {
|
|
|
108
108
|
*/
|
|
109
109
|
readonly pollIntervalMs: number;
|
|
110
110
|
/**
|
|
111
|
-
* Pull recent DMs/threads/mentions. MUST resolve (never reject)
|
|
111
|
+
* Pull recent DMs/threads/mentions. MUST resolve (never reject), failures
|
|
112
112
|
* are reported via state:'unavailable' + error so one bad provider cannot
|
|
113
113
|
* crash the aggregate feed.
|
|
114
114
|
*/
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// Discord Gateway for DM polling. The Gateway is a persistent websocket push
|
|
6
6
|
// transport and cannot be driven by the inbound poller, whose contract is a
|
|
7
7
|
// stateless, cadence-driven adapter.poll() that MUST resolve each call (see
|
|
8
|
-
// provider-adapter.ts)
|
|
8
|
+
// provider-adapter.ts), a long-lived socket the poller neither owns nor
|
|
9
9
|
// supervises is out of scope for that contract. We therefore satisfy the
|
|
10
10
|
// DM-polling goal over Discord's supported request/response surface, the REST
|
|
11
|
-
// API
|
|
11
|
+
// API, the same DM data the Gateway streams, fetched on the poll cadence and
|
|
12
12
|
// paged so a busy DM is never truncated:
|
|
13
13
|
// GET /users/@me/channels -> list DM channels the bot participates in
|
|
14
14
|
// GET /channels/{id}/messages -> recent messages per DM channel (paged via
|
|
@@ -66,7 +66,7 @@ interface DiscordMessage {
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Classify a Discord message into the InboundChannelItem `kind`.
|
|
69
|
-
* - reaction: someone reacted to OUR OWN message
|
|
69
|
+
* - reaction: someone reacted to OUR OWN message, a genuine inbound reaction
|
|
70
70
|
* event. Requires the message to be authored by us (its author id
|
|
71
71
|
* is selfId) AND to carry a non-empty reactions[]. A message
|
|
72
72
|
* authored by someone else that merely carries reactions[] is a
|
|
@@ -244,7 +244,7 @@ function msToSnowflake(ms: number): string {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
|
-
* The provider is wired up but this attempt failed
|
|
247
|
+
* The provider is wired up but this attempt failed, an outage, a refusal, a
|
|
248
248
|
* bad response. Items that exist are missing from the feed, which is what
|
|
249
249
|
* `configured: true` here tells the aggregator to report as a partial answer
|
|
250
250
|
* rather than as an empty one.
|
|
@@ -255,7 +255,7 @@ function failed(error: string): ProviderPollResult {
|
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
257
|
* Nothing to poll with: no credential, or an unusable one. Normal on a fresh
|
|
258
|
-
* install, and deliberately NOT a partial answer
|
|
258
|
+
* install, and deliberately NOT a partial answer, nothing is missing from a
|
|
259
259
|
* provider nobody asked us to read.
|
|
260
260
|
*/
|
|
261
261
|
function notConfigured(error: string): ProviderPollResult {
|
|
@@ -264,7 +264,7 @@ function notConfigured(error: string): ProviderPollResult {
|
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
266
|
* The credential store itself failed, so we do not know whether this provider
|
|
267
|
-
* is configured. Neither claim is made
|
|
267
|
+
* is configured. Neither claim is made, reporting a guess here is how a
|
|
268
268
|
* transient store fault would get read as "you never set this up".
|
|
269
269
|
*/
|
|
270
270
|
function unavailable(error: string): ProviderPollResult {
|
|
@@ -144,7 +144,7 @@ function normalizeAddress(from: string): string {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* The provider is wired up but this attempt failed
|
|
147
|
+
* The provider is wired up but this attempt failed, an outage, a refusal, a
|
|
148
148
|
* bad response. Items that exist are missing from the feed, which is what
|
|
149
149
|
* `configured: true` here tells the aggregator to report as a partial answer
|
|
150
150
|
* rather than as an empty one.
|
|
@@ -155,7 +155,7 @@ function failed(error: string): ProviderPollResult {
|
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Nothing to poll with: no credential, or an unusable one. Normal on a fresh
|
|
158
|
-
* install, and deliberately NOT a partial answer
|
|
158
|
+
* install, and deliberately NOT a partial answer, nothing is missing from a
|
|
159
159
|
* provider nobody asked us to read.
|
|
160
160
|
*/
|
|
161
161
|
function notConfigured(error: string): ProviderPollResult {
|
|
@@ -164,7 +164,7 @@ function notConfigured(error: string): ProviderPollResult {
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* The credential store itself failed, so we do not know whether this provider
|
|
167
|
-
* is configured. Neither claim is made
|
|
167
|
+
* is configured. Neither claim is made, reporting a guess here is how a
|
|
168
168
|
* transient store fault would get read as "you never set this up".
|
|
169
169
|
*/
|
|
170
170
|
function unavailable(error: string): ProviderPollResult {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Implements exactly what the inbound poller needs:
|
|
5
5
|
// LOGIN, SELECT, UID SEARCH (SINCE / ALL), UID FETCH (ENVELOPE + body peek),
|
|
6
|
-
// LOGOUT. No external npm dependency
|
|
6
|
+
// LOGOUT. No external npm dependency, uses node:tls (Bun-compatible).
|
|
7
7
|
//
|
|
8
8
|
// This is intentionally conservative: line-buffered tagged-command protocol,
|
|
9
9
|
// per-command timeout, and a hard cap on response size to avoid unbounded
|
|
@@ -76,7 +76,7 @@ interface SlackHistoryResponse {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Classify a Slack message into the InboundChannelItem `kind`.
|
|
79
|
-
* - reaction: someone reacted to OUR OWN message
|
|
79
|
+
* - reaction: someone reacted to OUR OWN message, a genuine inbound reaction
|
|
80
80
|
* event. Requires the message to be authored by us (its user id
|
|
81
81
|
* is selfUserId) AND to carry a non-empty reactions[]. A message
|
|
82
82
|
* authored by someone else that merely carries reactions[] is a
|
|
@@ -255,7 +255,7 @@ export function createSlackAdapter(ctx: AdapterContext): InboundProviderAdapter
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* The provider is wired up but this attempt failed
|
|
258
|
+
* The provider is wired up but this attempt failed, an outage, a refusal, a
|
|
259
259
|
* bad response. Items that exist are missing from the feed, which is what
|
|
260
260
|
* `configured: true` here tells the aggregator to report as a partial answer
|
|
261
261
|
* rather than as an empty one.
|
|
@@ -266,7 +266,7 @@ function failed(error: string): ProviderPollResult {
|
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
268
|
* Nothing to poll with: no credential, or an unusable one. Normal on a fresh
|
|
269
|
-
* install, and deliberately NOT a partial answer
|
|
269
|
+
* install, and deliberately NOT a partial answer, nothing is missing from a
|
|
270
270
|
* provider nobody asked us to read.
|
|
271
271
|
*/
|
|
272
272
|
function notConfigured(error: string): ProviderPollResult {
|
|
@@ -275,7 +275,7 @@ function notConfigured(error: string): ProviderPollResult {
|
|
|
275
275
|
|
|
276
276
|
/**
|
|
277
277
|
* The credential store itself failed, so we do not know whether this provider
|
|
278
|
-
* is configured. Neither claim is made
|
|
278
|
+
* is configured. Neither claim is made, reporting a guess here is how a
|
|
279
279
|
* transient store fault would get read as "you never set this up".
|
|
280
280
|
*/
|
|
281
281
|
function unavailable(error: string): ProviderPollResult {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Composition root for the daemon handler layer
|
|
2
|
+
* Composition root for the daemon handler layer, the only module that
|
|
3
3
|
* `src/runtime/services.ts` imports. It assembles every surface (routing,
|
|
4
|
-
* inbox+triage, drafts,
|
|
4
|
+
* inbox+triage, drafts, payments, remote) onto the SDK gateway catalog
|
|
5
5
|
* and returns a single teardown plus the cross-surface handles the runtime
|
|
6
6
|
* needs (routing resolver, the remote `DistributedRuntimeRouteService` the SDK
|
|
7
7
|
* facade injects, and the `remote.peers.invoke` dispatch adapter).
|
|
@@ -46,17 +46,26 @@ export interface DaemonHandlerSurfaces {
|
|
|
46
46
|
* attaches handlers to the SDK catalog via `registerCatalogHandler(s)`.
|
|
47
47
|
*
|
|
48
48
|
* Order of composition (and reverse teardown) is enforced by
|
|
49
|
-
* `registerDaemonHandlers`: routing → inbox(+triage) → drafts →
|
|
50
|
-
*
|
|
49
|
+
* `registerDaemonHandlers`: routing → inbox(+triage) → drafts → payments →
|
|
50
|
+
* remote.
|
|
51
51
|
*/
|
|
52
52
|
export interface DaemonHandlerSurfaceProviders {
|
|
53
|
-
/** channels.routing
|
|
53
|
+
/** channels.routing.*, returns the resolver consumed by the inbox surface. */
|
|
54
54
|
readonly registerRouting: (ctx: HandlerContext) => RoutingRegistration;
|
|
55
55
|
/** channels.inbox.list (triage-decorated) + inbox.triage.* (daemon-internal). */
|
|
56
56
|
readonly registerInbox: (ctx: HandlerContext, routing: RoutingRegistration) => Unregister;
|
|
57
57
|
/** channels.drafts.* */
|
|
58
58
|
readonly registerDrafts: SurfaceRegister;
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* payments.budget.status / cards.* / purchases.list / checkout.begin / checkout.fillCard.
|
|
61
|
+
*
|
|
62
|
+
* Its stores need a shell-path resolver and a scoped secret writer, neither of
|
|
63
|
+
* which is on `HandlerContext`, so the composition root builds them and hands
|
|
64
|
+
* this provider the closed-over registration; the provider signature is the
|
|
65
|
+
* ordinary one so teardown ordering stays uniform across every surface.
|
|
66
|
+
*/
|
|
67
|
+
readonly registerPayments: SurfaceRegister;
|
|
68
|
+
/** remote.peers.*, supplies the host DistributedRuntimeRouteService + dispatch adapter. */
|
|
60
69
|
readonly registerRemote: (ctx: HandlerContext) => RemoteSurfaceRegistration;
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -78,12 +87,13 @@ export function registerDaemonHandlers(
|
|
|
78
87
|
|
|
79
88
|
teardowns.push(providers.registerInbox(ctx, routing));
|
|
80
89
|
teardowns.push(providers.registerDrafts(ctx));
|
|
90
|
+
teardowns.push(providers.registerPayments(ctx));
|
|
81
91
|
// calendar.* and email.* are NOT registered here any more. Both are served
|
|
82
92
|
// by the SDK (control-plane/routes/{calendar,email}.ts over the platform
|
|
83
93
|
// CalDAV/Google and IMAP/SMTP implementations), registered through
|
|
84
94
|
// registerGatewayVerbGroups in runtime/services.ts. This product used to
|
|
85
95
|
// carry its own handlers for the same descriptor ids and, registering later,
|
|
86
|
-
// won
|
|
96
|
+
// won, two implementations behind one path, which is the drift the hoist
|
|
87
97
|
// exists to end.
|
|
88
98
|
|
|
89
99
|
const remote = providers.registerRemote(ctx);
|
|
@@ -91,7 +101,7 @@ export function registerDaemonHandlers(
|
|
|
91
101
|
|
|
92
102
|
// Idempotent: teardown is reachable from more than one shutdown path now that
|
|
93
103
|
// the runtime disposal scope owns it, and the surfaces underneath are not all
|
|
94
|
-
// safe to unwind twice
|
|
104
|
+
// safe to unwind twice, the inbox surface closes a SQLite handle, which a
|
|
95
105
|
// second pass would close again inside a floating promise (an unhandled
|
|
96
106
|
// rejection, not a caught one). Running once is also simply the honest
|
|
97
107
|
// meaning of "release these surfaces".
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* address-store.ts, the stored shipping and billing addresses, read from
|
|
3
|
+
* config.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's `platform/config/schema-domain-payments.ts` already declares flat
|
|
6
|
+
* `payments.shippingAddress.*` / `payments.billingAddress.*` keys (name,
|
|
7
|
+
* line1, line2, city, region, postalCode, country each), settable through the
|
|
8
|
+
* owner profile and the settings surfaces; address.ts's own header names the
|
|
9
|
+
* defect this closes: "nothing in the checkout path read either." This is
|
|
10
|
+
* that read path, the daemon-owned counterpart to `DaemonCardStore` for the
|
|
11
|
+
* OTHER thing a checkout needs supplied rather than typed by the model.
|
|
12
|
+
*
|
|
13
|
+
* A field that is present but blank reads as absent (`''` is not a value any
|
|
14
|
+
* of these fields can honestly hold), so a half-set address is reported the
|
|
15
|
+
* same way a wholly-unset one is: `AddressStore.read` returns the address with
|
|
16
|
+
* whatever is there, and `checkAddress` (address.ts) is what decides whether
|
|
17
|
+
* that is complete enough to ship to, exactly as it already does for the SDK's
|
|
18
|
+
* own test doubles. Nothing here validates completeness; this module only
|
|
19
|
+
* reads what config holds.
|
|
20
|
+
*/
|
|
21
|
+
import type { AddressKind, AddressStore, PostalAddress, PaymentsConfigReader } from '@pellux/goodvibes-sdk/platform/payments';
|
|
22
|
+
|
|
23
|
+
function readAddressField(config: PaymentsConfigReader, kind: AddressKind, field: string): string {
|
|
24
|
+
const value = config.get(`payments.${kind}Address.${field}`);
|
|
25
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Every field blank means "nothing stored", read back as `null` rather than an all-empty address. */
|
|
29
|
+
function isEntirelyBlank(address: PostalAddress): boolean {
|
|
30
|
+
return address.name === ''
|
|
31
|
+
&& address.line1 === ''
|
|
32
|
+
&& address.line2 === ''
|
|
33
|
+
&& address.city === ''
|
|
34
|
+
&& address.region === ''
|
|
35
|
+
&& address.postalCode === ''
|
|
36
|
+
&& address.country === '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function configBackedAddressStore(config: PaymentsConfigReader): AddressStore {
|
|
40
|
+
return {
|
|
41
|
+
async read(kind: AddressKind): Promise<PostalAddress | null> {
|
|
42
|
+
const address: PostalAddress = {
|
|
43
|
+
name: readAddressField(config, kind, 'name'),
|
|
44
|
+
line1: readAddressField(config, kind, 'line1'),
|
|
45
|
+
line2: readAddressField(config, kind, 'line2'),
|
|
46
|
+
city: readAddressField(config, kind, 'city'),
|
|
47
|
+
region: readAddressField(config, kind, 'region'),
|
|
48
|
+
postalCode: readAddressField(config, kind, 'postalCode'),
|
|
49
|
+
country: readAddressField(config, kind, 'country'),
|
|
50
|
+
};
|
|
51
|
+
return isEntirelyBlank(address) ? null : address;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|