@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +119 -63
  2. package/README.md +24 -19
  3. package/bin/launcher-support.js +5 -5
  4. package/package.json +6 -4
  5. package/scripts/postinstall.js +8 -8
  6. package/src/cli/command-catalog.ts +22 -22
  7. package/src/cli/completion.ts +4 -4
  8. package/src/cli/help.ts +5 -5
  9. package/src/cli/index.ts +3 -3
  10. package/src/cli/parser.ts +2 -2
  11. package/src/cli/surface-catalog.ts +1 -1
  12. package/src/cli/types.ts +2 -2
  13. package/src/cluster/daemon-ws-call.ts +5 -5
  14. package/src/cluster/raw-reply-route.ts +5 -5
  15. package/src/config/checkpoint-settings.ts +7 -7
  16. package/src/config/config-key-guard.ts +22 -0
  17. package/src/config/run-daemon-config-migration.ts +3 -3
  18. package/src/config/secret-config.ts +7 -7
  19. package/src/config/surface.ts +3 -3
  20. package/src/core/pairing-banner.ts +5 -5
  21. package/src/daemon/cli.ts +45 -43
  22. package/src/daemon/config-command.ts +15 -15
  23. package/src/daemon/handlers/context.ts +1 -1
  24. package/src/daemon/handlers/contracts.ts +19 -4
  25. package/src/daemon/handlers/credentials.ts +1 -1
  26. package/src/daemon/handlers/drafts/draft-store.ts +3 -3
  27. package/src/daemon/handlers/drafts/register.ts +4 -4
  28. package/src/daemon/handlers/inbox/aggregator.ts +8 -8
  29. package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
  30. package/src/daemon/handlers/inbox/index.ts +7 -7
  31. package/src/daemon/handlers/inbox/mapping.ts +2 -2
  32. package/src/daemon/handlers/inbox/poller.ts +5 -5
  33. package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
  34. package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
  35. package/src/daemon/handlers/inbox/providers/email.ts +3 -3
  36. package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
  37. package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
  38. package/src/daemon/handlers/index.ts +18 -8
  39. package/src/daemon/handlers/payments/address-store.ts +54 -0
  40. package/src/daemon/handlers/payments/budget-store.ts +356 -0
  41. package/src/daemon/handlers/payments/card-store.ts +486 -0
  42. package/src/daemon/handlers/payments/checkout-handlers.ts +526 -0
  43. package/src/daemon/handlers/payments/index.ts +38 -0
  44. package/src/daemon/handlers/payments/merchant-judge.ts +57 -0
  45. package/src/daemon/handlers/payments/notifier.ts +112 -0
  46. package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
  47. package/src/daemon/handlers/payments/register.ts +518 -0
  48. package/src/daemon/handlers/register.ts +3 -3
  49. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
  50. package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
  51. package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
  52. package/src/daemon/handlers/remote/backends/types.ts +2 -2
  53. package/src/daemon/handlers/remote/dispatcher.ts +3 -3
  54. package/src/daemon/handlers/remote/index.ts +1 -1
  55. package/src/daemon/handlers/remote/peer-registry.ts +62 -13
  56. package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
  57. package/src/daemon/handlers/routing/index.ts +1 -1
  58. package/src/daemon/handlers/routing/route-store.ts +1 -1
  59. package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
  60. package/src/daemon/handlers/sqlite-store.ts +9 -9
  61. package/src/daemon/handlers/triage/index.ts +1 -1
  62. package/src/daemon/handlers/triage/integration.ts +3 -3
  63. package/src/daemon/handlers/triage/pipeline.ts +2 -2
  64. package/src/daemon/handlers/triage/scorer.ts +2 -2
  65. package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
  66. package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
  67. package/src/daemon/handlers/triage/tagger/index.ts +1 -1
  68. package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
  69. package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
  70. package/src/daemon/handlers/triage/types.ts +2 -2
  71. package/src/daemon/lifecycle.ts +5 -5
  72. package/src/daemon/local-daemon-state.ts +7 -7
  73. package/src/daemon/pair-command.ts +14 -14
  74. package/src/daemon/provision-wake-model.ts +5 -5
  75. package/src/daemon/send/channels.ts +7 -7
  76. package/src/daemon/send/command.ts +11 -11
  77. package/src/daemon/send/composition.ts +5 -5
  78. package/src/daemon/send/failure-text.ts +6 -6
  79. package/src/daemon/send/inert-text.ts +18 -18
  80. package/src/daemon/send/stdin.ts +3 -3
  81. package/src/daemon/service-commands.ts +32 -32
  82. package/src/daemon/sessions-command.ts +7 -7
  83. package/src/daemon/status-command.ts +22 -22
  84. package/src/daemon/webui-command.ts +14 -14
  85. package/src/runtime/boot-tasks.ts +1 -1
  86. package/src/runtime/browser-checkout-seam-holder.ts +55 -0
  87. package/src/runtime/cluster-composition.ts +9 -9
  88. package/src/runtime/cluster-group-composition.ts +7 -7
  89. package/src/runtime/conversation-rewind-port.ts +8 -8
  90. package/src/runtime/credential-composition.ts +2 -2
  91. package/src/runtime/daemon-handler-composition.ts +61 -4
  92. package/src/runtime/device-posture-composition.ts +10 -10
  93. package/src/runtime/disposal-wiring.ts +8 -8
  94. package/src/runtime/fleet-needs-input-push.ts +4 -4
  95. package/src/runtime/fleet-services.ts +1 -1
  96. package/src/runtime/hosted-session-composition.ts +13 -13
  97. package/src/runtime/index.ts +1 -1
  98. package/src/runtime/knowledge-services.ts +2 -2
  99. package/src/runtime/legacy-daemon-migration.ts +43 -43
  100. package/src/runtime/legacy-daemon-reconcile.ts +30 -30
  101. package/src/runtime/mail-composition.ts +6 -6
  102. package/src/runtime/notification-dispatch.ts +7 -7
  103. package/src/runtime/payments-composition.ts +187 -0
  104. package/src/runtime/plugin-composition.ts +7 -7
  105. package/src/runtime/runtime-services-types.ts +9 -9
  106. package/src/runtime/services.ts +41 -32
  107. package/src/runtime/trigger-services.ts +1 -1
  108. package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
  109. package/src/runtime/trust/trust-gated-approvals.ts +9 -9
  110. package/src/runtime/update-check.ts +4 -4
  111. package/src/runtime/workspace-checkpointing.ts +6 -6
  112. package/src/testing/daemon-fixture.ts +11 -11
  113. package/src/testing/hosted-session-failures.ts +4 -4
  114. 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 that is what the triage pipeline scores.
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 while the READ is deliberately ungated so a standby
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 its state, when it last synced, how
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 that one is the freshness watermark a caller
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 an inbox is a timeline, and each item carries its own `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 a provider missing from
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" so it is
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 nothing configured, versus configured and failing because a caller
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() 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
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 that whole-file model is
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 a very chatty month.
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 never item content. */
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 the pass is a function of the table's current contents.
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 the
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 bounds the lookup to the poll size instead
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 several items can share a receivedAt, and comparing on
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 it looks the descriptor up via the catalog and
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 the merged, paginated
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 a node that is not fetching still answers questions
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 and handing one account over must not
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 no sender ids, subjects or bodies.
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() the handler still
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 otherwise handing one account to another machine would stop
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 catches raw API keys pasted without a recognizable prefix.
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 keeps previews single-line and tidy. */
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 unlike
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 see ProviderStatus.
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 clearing the interval means no further tick can
125
- * be scheduled which is what lets the RESIGN that follows a handoff be an
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 mapping/redaction is
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 the
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 the inbox
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) NEVER the raw id. 16 hex chars == the
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` 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
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) failures
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) a long-lived socket the poller neither owns nor
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 the same DM data the Gateway streams, fetched on the poll cadence and
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 a genuine inbound reaction
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 an outage, a refusal, a
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 nothing is missing from a
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 reporting a guess here is how a
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 an outage, a refusal, a
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 nothing is missing from a
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 reporting a guess here is how a
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 uses node:tls (Bun-compatible).
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 a genuine inbound reaction
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 an outage, a refusal, a
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 nothing is missing from a
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 reporting a guess here is how a
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 the only module that
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, calendar, email, remote) onto the SDK gateway catalog
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 → calendar
50
- * email → remote.
49
+ * `registerDaemonHandlers`: routing → inbox(+triage) → drafts → payments
50
+ * remote.
51
51
  */
52
52
  export interface DaemonHandlerSurfaceProviders {
53
- /** channels.routing.* returns the resolver consumed by the inbox surface. */
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
- /** remote.peers.* — supplies the host DistributedRuntimeRouteService + dispatch adapter. */
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 two implementations behind one path, which is the drift the hoist
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 the inbox surface closes a SQLite handle, which a
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
+ }