@abot-ai/socket 0.0.0-stage → 0.1.0

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -2
  3. package/bin/abot-socket.mjs +15 -0
  4. package/config.example.json +14 -0
  5. package/docs/APP-REVIEW.md +184 -0
  6. package/docs/DEPLOYMENT.md +124 -0
  7. package/docs/INSTALLATION.md +166 -0
  8. package/docs/MIGRATION.md +48 -0
  9. package/docs/PROTOCOL.md +278 -0
  10. package/docs/SECURITY.md +98 -0
  11. package/docs/SELF-HOSTING.md +154 -0
  12. package/package.json +64 -4
  13. package/src/auth/auth-database.mjs +102 -0
  14. package/src/auth/auth-limits.mjs +17 -0
  15. package/src/auth/auth-pairing.mjs +95 -0
  16. package/src/auth/auth-sessions.mjs +119 -0
  17. package/src/auth/auth-storage-path.mjs +70 -0
  18. package/src/auth/auth.mjs +45 -0
  19. package/src/auth/runtime-credentials.mjs +39 -0
  20. package/src/cli/cli-arguments.mjs +23 -0
  21. package/src/cli/cli-entry.mjs +84 -0
  22. package/src/cli/cli-environment.mjs +31 -0
  23. package/src/cli/cli-help.mjs +34 -0
  24. package/src/cli/cli-output.mjs +35 -0
  25. package/src/cli/cli.mjs +58 -0
  26. package/src/cli/client-capabilities.mjs +4 -0
  27. package/src/cli/doctor.mjs +106 -0
  28. package/src/config/config-validation.mjs +41 -0
  29. package/src/config/config.mjs +57 -0
  30. package/src/deployment/deployment-command.mjs +42 -0
  31. package/src/deployment/deployment-input.mjs +76 -0
  32. package/src/deployment/deployment-plan.mjs +62 -0
  33. package/src/deployment/deployment-templates.mjs +109 -0
  34. package/src/main.mjs +26 -0
  35. package/src/notifications/notification-delivery-credentials.mjs +48 -0
  36. package/src/notifications/notification-delivery-fcm.mjs +100 -0
  37. package/src/notifications/notification-delivery-gateway.mjs +36 -0
  38. package/src/notifications/notification-delivery-http.mjs +53 -0
  39. package/src/notifications/notification-delivery-payload.mjs +45 -0
  40. package/src/notifications/notification-delivery.mjs +43 -0
  41. package/src/notifications/notification-gateway-command.mjs +48 -0
  42. package/src/notifications/notification-gateway-store.mjs +159 -0
  43. package/src/notifications/notification-gateway.mjs +181 -0
  44. package/src/notifications/notification-http.mjs +39 -0
  45. package/src/notifications/notification-input.mjs +82 -0
  46. package/src/notifications/notification-outbox.mjs +60 -0
  47. package/src/notifications/notification-preview-crypto.mjs +42 -0
  48. package/src/notifications/notification-preview-schema.mjs +7 -0
  49. package/src/notifications/notification-preview-store.mjs +63 -0
  50. package/src/notifications/notification-preview-text.mjs +20 -0
  51. package/src/notifications/notification-projection.mjs +20 -0
  52. package/src/notifications/notification-schema.mjs +22 -0
  53. package/src/notifications/notification-service.mjs +147 -0
  54. package/src/notifications/notification-store.mjs +132 -0
  55. package/src/pairing/pairing-qr-payload.mjs +32 -0
  56. package/src/pairing/pairing-qr.mjs +42 -0
  57. package/src/protocol/protocol-validation.mjs +137 -0
  58. package/src/protocol/protocol.mjs +110 -0
  59. package/src/relay/relay-client-admission.mjs +40 -0
  60. package/src/relay/relay-correlations.mjs +96 -0
  61. package/src/relay/relay-model-catalog.mjs +69 -0
  62. package/src/relay/relay-requests.mjs +155 -0
  63. package/src/relay/relay-runtime-registration.mjs +49 -0
  64. package/src/relay/relay-session-mutations.mjs +88 -0
  65. package/src/relay/relay.mjs +202 -0
  66. package/src/review/review-demo-isolation.mjs +132 -0
  67. package/src/review/review-demo-setup.mjs +97 -0
  68. package/src/review/review-portal-access.mjs +64 -0
  69. package/src/review/review-portal-form.mjs +24 -0
  70. package/src/review/review-portal-page.mjs +68 -0
  71. package/src/review/review-portal-server.mjs +105 -0
  72. package/src/setup/setup-files.mjs +37 -0
  73. package/src/setup/setup-questions.mjs +62 -0
  74. package/src/setup/setup-wizard.mjs +50 -0
  75. package/src/setup/setup.mjs +42 -0
  76. package/src/transport/http-api.mjs +31 -0
  77. package/src/transport/server.mjs +165 -0
  78. package/src/transport/transport-limits.mjs +85 -0
@@ -0,0 +1,48 @@
1
+ # Moving to pairing
2
+
3
+ Installing the package does not change existing services or applications. Use a
4
+ separately approved cutover after real-device verification.
5
+
6
+ The chat envelopes, runtime bridge and runtime credential grants remain compatible.
7
+ Authentication changes: the new client uses local-operator pairing instead of
8
+ email. Old in-memory sessions cannot be imported. Existing devices must pair to
9
+ the new service, receiving separate credentials bound to its HTTPS origin.
10
+
11
+ 1. Back up the current private configuration and deployed version.
12
+ 2. Prepare a separate candidate endpoint and private state with a unique test
13
+ runtime identity. Do not attach two active runtimes using the same ID.
14
+ 3. Issue pairing locally; verify pairing, restart persistence, runtime discovery,
15
+ history, chat, steering, approvals and revocation through the actual app.
16
+ 4. Perform the approved service/client cutover while retaining a rollback path.
17
+
18
+ The new configuration has `publicOrigin`, `port`, `stateDirectory`, and
19
+ `runtimeGrants`. Remove old `allowedEmail` and `mail` settings; they are rejected.
20
+ `ABOT_SOCKET_CONFIG` accepts `SOCKET_V2_CONFIG` as a path alias. No mail-token
21
+ variable is used. The existing runtime adapter needs configuration only, not
22
+ source changes. Setup generates new runtime credentials rather than rotating or
23
+ migrating an existing installation automatically.
24
+
25
+ After cutover, retain the private state directory through upgrades. Device
26
+ sessions persist there; transient relay routes still disappear on restart.
27
+ Interrupted requests must be reconciled against runtime history before retry.
28
+
29
+
30
+ ## Notification preview schema upgrade
31
+
32
+ Schema v3 adds a device-scoped preview-key table and nullable encrypted-preview
33
+ column on the existing outbox. Both schema v1 and v2 upgrade transactionally;
34
+ paired-device tokens, pending pairing, owner identity, notification metadata and
35
+ generic delivery queues are preserved. Preview remains off until that device
36
+ explicitly provisions a key after push registration. No runtime changes are needed.
37
+
38
+ Stop the service and back up the private state directory and matching deployed
39
+ code before upgrading. Older binaries reject schema v3. Rollback requires restoring
40
+ the matching pre-upgrade database snapshot together with old code while stopped;
41
+ do not point an old binary at the migrated state or delete the database to make it
42
+ start. Restoring a snapshot also restores its older device/revocation/delivery state,
43
+ so reconcile changes made since that snapshot before resuming service.
44
+
45
+ For gateway delivery, upgrade the publisher gateway before clients enable previews.
46
+ Keep previews off until compatible client support is installed. Old clients and
47
+ opted-out devices retain the original generic push behavior. This source change
48
+ alone does not deploy or enable preview delivery on an existing installation.
@@ -0,0 +1,278 @@
1
+ # Protocol and compatibility
2
+
3
+ This candidate retains the Socket v2 chat transport contract and replaces email
4
+ authentication with operator-issued pairing. It does not change
5
+ runtime source or expose the complete runtime Web UI API. The `/health` value
6
+ `version: 2` identifies the protocol family, not a package release number.
7
+
8
+ ## HTTP
9
+
10
+ Use the configured HTTPS origin. JSON body limits depend on the route:
11
+
12
+ | Route | Maximum JSON request body |
13
+ | --- | --- |
14
+ | `POST /v2/pairing/redeem`, `POST /v2/notifications/read`, `POST /v2/notifications/presence` | 4 KiB (4096 bytes). |
15
+ | `PUT /v2/notifications/push` | 8 KiB (8192 bytes). |
16
+ | `PUT /v2/notifications/push/preview` | 512 bytes. |
17
+
18
+ These are application limits; a reverse proxy can impose a smaller limit. The
19
+ generated direct-proxy plans currently cap request bodies at 4 KiB. The separate
20
+ notification gateway accepts JSON bodies up to 8 KiB and needs its own proxy
21
+ configuration. Field validation still applies within each body limit.
22
+
23
+ Responses are `no-store`. Host must match the configured origin, and a supplied
24
+ browser Origin must match it exactly. The local proxy requirements apply to all
25
+ routes, including `/health`.
26
+
27
+ | Method and path | Request / response |
28
+ | --- | --- |
29
+ | `GET /health` | `{ok:true, version:2}`; no account or runtime details. |
30
+ | `POST /v2/pairing/redeem` | `{pairingId, code, deviceName}` → `{token, expiresAt, session:{id,subject,deviceName}}`. |
31
+ | `GET /v2/auth/sessions` | Bearer session token → `{sessions:[{id,deviceName,createdAt,expiresAt,current}]}`. |
32
+ | `DELETE /v2/auth/sessions/:id` | Bearer session token → `{revoked}`. |
33
+ | `POST /v2/auth/logout` | Bearer session token → `{ok:true}`. |
34
+ | `GET /v2/runtimes` | Bearer session token → `{runtimes:[...]}` with IDs, online state, model catalog, allowed modes, and capabilities. |
35
+
36
+ `pairingId` is a random 32-character base64url temporary key; `code` is exactly
37
+ six decimal digits, including leading zeros. Both are required. A bundle is
38
+ valid for five minutes and five failed attempts; consume and device issuance
39
+ share a database transaction. A retry after a lost success response cannot reuse
40
+ it: the operator must create a fresh bundle. Neither secret belongs in a URL.
41
+ The ordinary Socket API has no HTTP endpoint for generating pairing bundles.
42
+ The optional, separately enabled [app-review portal](APP-REVIEW.md) exposes an
43
+ authenticated `POST /review/pair` on its own listener/origin, bound to a dedicated
44
+ review installation. It is not part of the ordinary Socket API. Legacy email
45
+ endpoints are no longer supported.
46
+
47
+ ### Pairing QR, version 1
48
+
49
+ The operator CLI can encode the existing bundle as raw UTF-8 JSON in a QR:
50
+
51
+ ```json
52
+ {"type":"abot-pairing","version":1,"serverUrl":"https://socket.example.test","pairingId":"0123456789abcdefghijklmnopqrstuv","code":"000123","expiresAt":1800000300000}
53
+ ```
54
+
55
+ This example is illustrative, not a usable pairing. `expiresAt` is Unix time in
56
+ milliseconds; the server remains authoritative for validity. The code is a string
57
+ to preserve leading zeros. The QR contains only these six fields, never a runtime
58
+ token, permanent device token, configuration file, or secret-bearing URL/deep link.
59
+
60
+ Clients validate the type/version and field shapes, accept only a canonical HTTPS
61
+ origin, display the destination for confirmation, and then use the existing
62
+ `POST /v2/pairing/redeem`. Scanning alone must not contact the destination. Suppress
63
+ duplicate camera detections while confirmation or redemption is in progress.
64
+ The existing expiry, attempt limits and one-use transaction apply unchanged;
65
+ creating a new bundle replaces only the pending pairing, not existing devices.
66
+ The QR is a bearer enrollment secret, so it must not be retained in logs or sent
67
+ to external QR-generation services.
68
+
69
+ An expired, unknown, consumed or wrong pairing returns `invalid_code` / 401.
70
+ General HTTP limits and source/global code-verification limits return `rate_limited` / 429.
71
+ Verification quotas apply only after matching an unexpired, unlocked pending pairing
72
+ key; malformed, unknown, expired or already-locked pairing requests do not spend them.
73
+ A full device list
74
+ returns `device_limit` / 429; free a slot locally. Malformed device names return
75
+ `invalid_request` / 400 without consuming an otherwise valid pairing.
76
+
77
+ ## Native client WebSocket
78
+
79
+ Connect to `/v2/realtime` using `Authorization: Bearer <device-session-token>` in
80
+ the HTTP upgrade. Credentials are not accepted in URL query parameters. Native
81
+ clients should store the token in the platform's secure credential store.
82
+ Browser WebSocket APIs cannot set this header; browser support needs its own
83
+ designed authentication path.
84
+
85
+ On connection, the relay sends `authenticated` with the session expiry and
86
+ capability flags. Client commands use `{id, runtimeId, body}`. IDs must be unique
87
+ per connection. Supported body types are:
88
+
89
+ - `run_request`, `steer_request`, `tool_approval_response`;
90
+ - `session.list.request`, `session.snapshot.request`, `request.events.request`;
91
+ - `session.delete.request`, `session.messages.clear.request`, `session.message.delete.request`;
92
+ - `attachment.upload.request`, `attachment.fetch.request`;
93
+ - relay-local `subscribe_request` for a known request belonging to this owner.
94
+
95
+ The relay returns `relay.ack`, `relay.error`, `relay.response`, or `relay.event`
96
+ with the appropriate command ID, runtime ID, and body. An acknowledgement means
97
+ dispatched or queued, not completed. Runtime correlations belong to the relay;
98
+ the client cannot choose them. Unknown command fields and unsupported operations
99
+ are rejected rather than forwarded as a generic admin API.
100
+
101
+ Tool permission values are `ask`, `full_access`, and `full_plus`, constrained by
102
+ the runtime grant. Setup grants only `ask`. The model catalog and supported
103
+ reasoning controls come from the connected runtime, not a hardcoded model list.
104
+
105
+ ### Session mutations
106
+
107
+ Each runtime in `/v2/runtimes` and the WebSocket `authenticated` frame includes
108
+ `capabilities.sessionMutation: true` for these three relay operations. Clients must treat an
109
+ absent/false flag as unsupported and require explicit user confirmation.
110
+
111
+ | Request body | Response body (inside `relay.response`) |
112
+ | --- | --- |
113
+ | `{type:'session.delete.request', sessionId}` | `{type:'session.delete.response', ok, sessionId, deleted, deletedMessages, deletedRequests}` |
114
+ | `{type:'session.messages.clear.request', sessionId}` | `{type:'session.messages.clear.response', ok, sessionId, cleared, deletedMessages, deletedRequests, updatedAt}` |
115
+ | `{type:'session.message.delete.request', sessionId, messageId}` | `{type:'session.message.delete.response', ok, sessionId, messageId, deleted, updatedAt, deletedRequestId?, deletedRequestEvents?}` |
116
+
117
+ Use canonical message IDs from a fresh runtime snapshot, not local UI IDs.
118
+ Numeric message IDs are normalized to strings. Only `ok:true` together with
119
+ `deleted:true` / `cleared:true` confirms a change; reload canonical history after
120
+ the result. The runtime's single-message delete removes only that message row,
121
+ plus its associated request replay/context and memory checkpoints; sibling
122
+ message rows remain. Session deletion/clearing follows the existing runtime's
123
+ history and attachment cleanup semantics.
124
+
125
+ The relay rejects observed active/uncertain requests and in-flight uploads with
126
+ `session_busy`. Before forwarding a mutation it reads a complete snapshot with
127
+ `includeRequests:true`; active requests prevent deletion, and an incomplete or
128
+ unrecognized snapshot returns `session_state_unavailable`. No snapshot contents
129
+ are added to the mutation response. A pending mutation blocks another mutation,
130
+ new run or upload in that session (`session_mutation_pending`). It retains this
131
+ guard after client disconnect until the outcome or the shared 30-second timeout.
132
+ A client disconnected during the read check cannot begin deletion.
133
+
134
+ This check is not an atomic lock over independent Web UI clients. A request
135
+ started directly at the runtime after the check can race with deletion; the
136
+ runtime fences deleted generations and that request may be cancelled. Wait for
137
+ work on all clients to finish. `outcome_unknown` never authorizes an automatic
138
+ retry: reload authoritative history first. Missing sessions report
139
+ `session_not_found`; runtime failures remain failures.
140
+
141
+ ## Runtime bridge
142
+
143
+ Connect to `/agent-bridge` with the separate runtime Bearer token, then send the
144
+ existing `{type:'register', agentId, features?, modelCatalog?}` message. The
145
+ registered ID must match the credential's grant. A second connection cannot
146
+ replace an already connected runtime with the same ID. Client device credentials
147
+ cannot authenticate this role.
148
+
149
+ ## Recovery and capability limits
150
+
151
+ - A timeout or disconnect can return `outcome_unknown`; do not retry commands
152
+ automatically. Read canonical runtime history/replay to reconcile the result.
153
+ - Live `eventSequence` and replay `seqNo` are separate counters. `afterSeq` uses
154
+ the persisted replay counter only.
155
+ - Approval authority requires an observed pending approval for the correct
156
+ runtime and request. Historical replay alone does not restore that authority.
157
+ Positive approval expires after five minutes; an explicit decline can remain
158
+ possible once on that same active runtime connection and request.
159
+ - `cancel` and `durableApprovals` are advertised as false. Restart, cron,
160
+ configuration, and generic administration are not exposed.
161
+ - The relay keeps bounded temporary routing state. It is not an offline queue,
162
+ durable conversation store, or exactly-once execution service.
163
+
164
+ Use the new pairing-capable client. Email-only personal builds cannot enroll
165
+ through this authentication contract. Existing runtime adapters are unchanged.
166
+
167
+ ## Notifications
168
+
169
+ `capabilities.notifications: true` advertises the authenticated notification API.
170
+ Notifications are owned by the **paired device that initiated the request**, not
171
+ by its current WebSocket connection. Other paired devices cannot list or mark
172
+ that device's records. There are no new unsolicited WebSocket frame types;
173
+ clients poll the inbox while foregrounded and reconcile on resume or push tap.
174
+
175
+ | Method and path | Request / response |
176
+ | --- | --- |
177
+ | `GET /v2/notifications?limit=50&before=<cursor>` | Device inbox, descending time; limit 1–100; opaque optional cursor. |
178
+ | `POST /v2/notifications/read` | `{ids:[...]}` (maximum 100) → `{ok:true}`. Unknown or foreign IDs have no effect. |
179
+ | `POST /v2/notifications/presence` | `{foreground:boolean,runtimeId?,sessionId?}` → `{ok:true}`; a session requires its runtime. |
180
+ | `PUT /v2/notifications/push` | `{provider:'fcm',token}` or `{provider:'gateway',endpointId,sendToken}` → `{ok:true}`. |
181
+ | `DELETE /v2/notifications/push` | Remove this device's push registration, preview key and queued deliveries → `{ok:true}`. |
182
+ | `PUT /v2/notifications/push/preview` | Exact `{keyId,key}` → `{ok:true}`; opt in after registering push. Replaces the device's key. |
183
+ | `DELETE /v2/notifications/push/preview` | Remove this device's preview key and queued ciphertext, retaining generic deliveries → `{ok:true}`. |
184
+
185
+ The inbox response is `{version:1,items,nextCursor,unreadCount,unreadByRuntime,
186
+ unreadBySession,delivery,registration,preview}`. Each item is
187
+ `{id,runtimeId,sessionId,requestId,kind,createdAt,readAt,expiresAt}`; timestamps
188
+ are epoch milliseconds, and `readAt` is null until read. `kind` is `completed`,
189
+ `failed` or `approval_required`. `unreadByRuntime` maps runtime IDs to counts;
190
+ `unreadBySession` maps runtime IDs to session-ID/count maps. `delivery` is
191
+ `{mode:'disabled'|'fcm'|'gateway',configured:boolean}`, and `registration` is
192
+ `{enabled:boolean}`. `preview` is `{supported:true,enabled:boolean,keyId:string|null}`.
193
+ Older servers omit `preview`; clients treat that as unsupported and off. Keys
194
+ and registration credentials are never returned by this API.
195
+
196
+ Foreground presence lasts 45 seconds. Renew while active; `foreground:false`
197
+ clears it. Presence is advisory: the phone may be suspended before its background
198
+ update arrives, so presence alone never marks records read or suppresses delivery.
199
+ Only an explicit authenticated read acknowledgement consumes records and cancels
200
+ queued deliveries. Clients acknowledge fetched IDs after showing the conversation.
201
+ Counts share the same ledger; they are not runtime transcript read receipts.
202
+ Push taps resolve the notification ID through this authenticated inbox
203
+ before opening a conversation; a push payload grants no execution or approval authority.
204
+
205
+ Only validated live `completed`, `failed` and `tool.approval.required` frames for
206
+ known relay requests create records. Streaming/progress events and replay do not.
207
+ Granted/rejected/terminal events cancel pending approval deliveries. Approval
208
+ notifications expire after five minutes; other records after at most seven days
209
+ or device credential expiry. Storage is capped at 500 records per device.
210
+ Expiration, logout and revocation remove registrations and pending deliveries.
211
+ The outbox retries at most five times, with bounded backoff. Duplicate source
212
+ events are deduplicated, but provider delivery is not an exactly-once guarantee.
213
+
214
+ The inbox and queued deliveries survive relay restarts. In-flight request routing
215
+ does not: this is not a complete runtime-wide event subscription or an offline
216
+ execution queue. Web UI, schedules and autonomous SPARK events are outside this
217
+ phase. Never interpret a missing notification as a failed request or auto-retry it.
218
+
219
+ Push is disabled unless private operator configuration includes
220
+ `notifications.delivery`. A self-built client uses
221
+ `{type:'fcm',serviceAccountFile:'./private-sender.json'}`; the path is resolved
222
+ relative to the private socket configuration, and sender credentials stay on
223
+ that operator's server. The client must belong to the same configured Firebase
224
+ project. Official builds can use `{type:'gateway',gatewayOrigin:'https://<trusted-host>'}`
225
+ with a separately enrolled endpoint. Clients cannot choose a gateway URL or send
226
+ provider credentials through the registration API. A missing or mismatched
227
+ configured transport returns `409 push_unavailable`; ordinary chat and the inbox
228
+ continue to work with delivery disabled.
229
+
230
+
231
+ ### Optional encrypted answer previews
232
+
233
+ Previews are off by default and enabled per paired device, only after an active
234
+ push registration exists. The app generates a random 32-byte AES-256-GCM key and
235
+ 16-byte key ID, sends both over authenticated HTTPS to its own Socket, and enables
236
+ local display only after acknowledgement. Both fields use canonical unpadded
237
+ base64url. No email, additional identity provider, or runtime change is involved.
238
+ Provider-token rotation preserves the preview setting. Key replacement clears
239
+ pending ciphertext. Disabling previews clears the key and ciphertext while leaving
240
+ the generic queue intact; unregistering push removes the entire delivery queue.
241
+ In-flight sends are aborted where possible, but provider-accepted notifications
242
+ cannot be recalled. The client must discard its key immediately on disable/logout.
243
+
244
+ Only a nonempty final `completed.output` is eligible. The Socket takes a bounded
245
+ prefix, removes unsafe controls and bidi override characters, and truncates on a
246
+ Unicode scalar boundary to at most 1024 UTF-8 bytes, including an ellipsis when
247
+ needed. It is an excerpt, not secret redaction. Approval, failure and progress
248
+ content is never included. No plaintext preview is stored in the inbox or outbox.
249
+ The inbox remains metadata-only; encrypted content exists only in the pending
250
+ outbox and is removed on delivery, read acknowledgement, revocation or expiry.
251
+
252
+ The exact encrypted envelope is `{v:1,keyId,nonce,ciphertext,expiresAt}`. Nonce is
253
+ 12 random bytes. Ciphertext contains encrypted UTF-8 followed by the 16-byte GCM
254
+ authentication tag (at most 1040 bytes), using canonical unpadded base64url.
255
+ Authenticated additional data is UTF-8, with literal newline separators:
256
+
257
+ ```text
258
+ ABot push preview v1
259
+ <keyId>
260
+ <notificationId>
261
+ <expiresAt decimal epoch milliseconds>
262
+ ```
263
+
264
+ The expiry equals notification expiry, capped by the device credential and seven
265
+ days. The device rejects an unknown key, altered ID/expiry/content, expired data
266
+ or malformed envelope and retains a generic alert. The gateway receives neither
267
+ the key nor plaintext. Its `/v1/deliver` accepts an optional strictly validated
268
+ `preview` field alongside the existing fixed delivery fields. Upgrade the gateway
269
+ before enabling previews; older gateways do not accept this field.
270
+
271
+ Encrypted FCM sends have no top-level `notification` or `android.notification`.
272
+ Their string-valued data is `notificationId`, `abotPreview` (envelope JSON),
273
+ `abotKind` (`completed`), `abotExpiresAt` and `abotBadge`. Android renders the
274
+ notification locally. APNs retains a generic alert and sets `mutable-content:1`
275
+ for the app's notification-service extension. Generic sends keep their existing
276
+ payload. Serialized provider envelopes are bounded to 4096 UTF-8 bytes. Foreground
277
+ display and navigation continue to use the authenticated inbox; push cannot grant
278
+ access or approval authority.
@@ -0,0 +1,98 @@
1
+ # Security boundaries
2
+
3
+ ABot Socket is a single-owner relay. Each installation has its own runtime
4
+ grants, device credentials, private state and HTTPS origin. Every paired device
5
+ represents that owner; this is not tenant isolation for unrelated users.
6
+
7
+ ## Authentication and credentials
8
+
9
+ - Ordinary installations issue pairing through local operator commands. A random temporary
10
+ pairing key and six-digit code expire after five minutes or one successful use.
11
+ Five incorrect code attempts invalidate the challenge; expiry and attempts
12
+ persist across server restarts. Source and global verification limits apply only
13
+ to requests identifying an unexpired, unlocked pending pairing key. Unrelated
14
+ requests do not spend that quota; general HTTP request limits still apply.
15
+ - Generating fresh pairing details replaces only the pending challenge, not
16
+ existing devices. Consumption and device creation are atomic in SQLite.
17
+ - Device tokens are independently random. Only token digests are stored by the
18
+ server. Sessions last at most 30 days without sliding renewal, survive restarts
19
+ and can be revoked locally or by an authenticated owner device. Revocation and
20
+ expiry close existing WebSockets. At most 10 active device sessions are allowed.
21
+ - The app binds credentials to the selected HTTPS origin in iOS Keychain and
22
+ refuses redirects. The pairing key is temporary, not a permanent shared key.
23
+ - Runtime tokens are separate and bound to one runtime ID. Only their SHA-256
24
+ digest is in server configuration. Never put a runtime token into the app.
25
+ - Configuration is private; state directories are 0700 and files 0600. State
26
+ symlinks, unexpected entries and unsafe ownership/permissions are rejected.
27
+ Keep installation files outside source, and never log codes, keys or bodies.
28
+ - Setup and `pair` intentionally display new one-time details to the operator.
29
+ The ordinary Socket HTTP API does not generate or expose pairing bundles and
30
+ offers no email fallback or login bypass.
31
+ - A separately enabled [app-review portal](APP-REVIEW.md) can generate and display
32
+ bundles after reviewer authentication. It binds to a dedicated review
33
+ installation, uses a separate listener/origin and must not share production
34
+ state or runtime credentials. Protect its access details as credentials.
35
+
36
+ ## Trusted components
37
+
38
+ The relay, its local reverse proxy and runtime computers are trusted. There is no
39
+ email delivery or third-party identity provider. HTTPS/WSS protects transport;
40
+ the relay can read routed messages and attachments. This is not end-to-end
41
+ encryption against its operator or a compromised relay host.
42
+
43
+ Follow [the proxy requirements](SELF-HOSTING.md#3-configure-https-and-the-proxy-boundary).
44
+ The listener is private loopback. Proxy headers must be overwritten by a trusted
45
+ local proxy; forwarded addresses are not trustworthy simply because a header
46
+ exists. CDN chains require a separate trusted-peer policy.
47
+
48
+ ## Permission and routing limits
49
+
50
+ Runtime grants limit the permitted tool modes; `ask` is the setup default.
51
+ Requests and responses are bound to their runtime connection and private
52
+ correlation. Clients cannot use this relay to call generic runtime administration,
53
+ restart, or scheduling. Session deletion/clearing is limited to three validated
54
+ commands for an authorized runtime, with private response correlations and a
55
+ read-before-delete check for active work. See the
56
+ [mutation contract and its cross-client race limit](PROTOCOL.md#session-mutations).
57
+
58
+ The relay validates command shapes, bounds frames, connections and routing state,
59
+ and rejects unsupported operations. These controls do not make a compromised
60
+ authorized device harmless: it can act within its owner's configured grants.
61
+ Revoking that device session removes its relay access.
62
+
63
+ The relay does not automatically retry uncertain operations, restore approval
64
+ authority from historical events, or provide exactly-once execution. After a
65
+ disconnect, reconcile against runtime history before resubmission.
66
+
67
+ ## Push preview boundary
68
+
69
+ Answer previews are opt-in per paired device and off by default. The already
70
+ trusted Socket sees final answers and encrypts a bounded excerpt with a random
71
+ per-device AES-256-GCM key provisioned over the existing authenticated HTTPS API.
72
+ That shared key is stored in the restricted Socket state; it is not sent to the
73
+ publisher gateway, FCM or APNs. The gateway validates and forwards ciphertext only.
74
+ This protects preview content from delivery intermediaries; it does not protect
75
+ against a compromised Socket, device, or its operator. It is not a transcript
76
+ end-to-end encryption claim.
77
+
78
+ Keys are scoped to the paired device, and notification ID and expiry are bound
79
+ into authenticated data. Key replacement, disable, logout and revocation remove
80
+ queued ciphertext; provider token rotation retains the key. The inbox never
81
+ returns preview content or keys. Pending ciphertext is deleted after delivery,
82
+ read or expiry. SQLite deletion is logical cleanup, not forensic secure erasure;
83
+ protect state backups as credentials. The OS may retain displayed notifications,
84
+ and previews can reveal answer content on a lock screen according to user settings.
85
+ Disabling cannot recall alerts already accepted or displayed by the provider/OS.
86
+ The device must remove its local key immediately so later queued ciphertext falls
87
+ back to generic text. Unsupported, expired or invalid payloads also fail generic.
88
+
89
+ ## Validation status
90
+
91
+ Local tests use dummy principals, a mock runtime. Some
92
+ tests exercise loopback HTTP/WebSocket transport. Configuration checking itself
93
+ does not send email or open a listener. None of these checks proves a deployed
94
+ proxy, domain, or real client/runtime behavior.
95
+
96
+ Package publication is not a claim of a completed independent security
97
+ audit. When investigating a problem, use synthetic credentials and redact
98
+ configuration, tokens, codes, and conversation content from reports.
@@ -0,0 +1,154 @@
1
+ # Self-hosting ABot Socket
2
+
3
+ Use [Installation and first connection](INSTALLATION.md) for the consumer
4
+ installation, setup and QR/manual pairing flow. This companion guide covers
5
+ configuration, the proxy trust boundary and ongoing operation. Service/proxy plan
6
+ generation is covered in [Deployment](DEPLOYMENT.md). The source is MIT-licensed;
7
+ the package is distributed as `@abot-ai/socket` on npm.
8
+
9
+ ## Requirements
10
+
11
+ - Node.js 24 or later. npm installs the package's pinned dependencies, `ws` and
12
+ `qrcode-generator`, with either the registry package or a local tarball.
13
+ - A server you control and a canonical HTTPS origin such as
14
+ `https://socket.example.net`.
15
+ - A local HTTPS reverse proxy satisfying the boundary below.
16
+ - An ABot Runtime with its existing agent-bridge adapter, and ABot App with pairing.
17
+
18
+ There is no public signup. Every paired device acts as this installation's single
19
+ owner and can see all runtimes granted to that installation.
20
+
21
+ ## 1. Prepare a private installation
22
+
23
+ Follow the [setup steps](INSTALLATION.md#follow-setup) as the unprivileged account
24
+ that will run the relay. Use your own HTTPS origin and keep the configuration,
25
+ runtime connection file and state outside the installed package directory.
26
+ Setup creates files only; applying a deployment plan and configuring the runtime
27
+ remain separate operator actions.
28
+
29
+ ## 2. Check configuration and issue fresh pairing details
30
+
31
+ For an installation outside the default `~/.abot-socket` directory:
32
+
33
+ ```sh
34
+ abot-socket check-config --config /srv/abot-private/config.json
35
+ abot-socket pair --config /srv/abot-private/config.json
36
+ ```
37
+
38
+ `stateDirectory` is resolved relative to the configuration file, independent of
39
+ the process working directory. An explicit `--config` takes precedence over
40
+ environment settings. Otherwise use `ABOT_SOCKET_CONFIG`; `SOCKET_V2_CONFIG`
41
+ remains a legacy alias and conflicting environment values are rejected.
42
+ No mail credential is used.
43
+
44
+ `check-config` validates the file without opening listeners or state. `pair`
45
+ updates only local authentication state: it replaces any unused pairing details
46
+ and displays a new five-minute bundle. Already paired devices stay connected.
47
+ The running server sees these changes without a restart. Local operator commands
48
+ must run as the same operating-system user that owns the private state directory.
49
+ Generate pairing details after HTTPS is ready, then follow the
50
+ [QR/manual pairing flow](INSTALLATION.md#connect-runtime-then-pair-the-app).
51
+
52
+ ## 3. Configure HTTPS and the proxy boundary
53
+
54
+ The application listens only on `127.0.0.1` at the configured port. Keep the proxy
55
+ on the same host and the application listener private. Containers, remote proxies,
56
+ and additional proxy chains require a separately validated network arrangement;
57
+ this package does not ship a Docker deployment.
58
+
59
+ For direct Internet-to-proxy-to-relay deployment, the proxy must:
60
+
61
+ 1. Accept only the configured HTTPS authority and a valid certificate; preserve
62
+ the exact configured Host, including a non-default port if one is configured.
63
+ 2. Support WebSocket upgrades on `/v2/realtime` and `/agent-bridge`, retaining the
64
+ `Authorization` header.
65
+ 3. Remove incoming `X-Forwarded-Proto` and `X-Socket-Client-IP` values and set its
66
+ own: `https` for the verified HTTPS request and the actual client IP observed
67
+ on the proxy's connection. Never copy a client-supplied forwarding header.
68
+ 4. Preserve an incoming browser `Origin` so the relay can reject an origin that
69
+ differs from its configured public origin.
70
+ 5. Enforce connection, request-body, and timeout limits. Keep credentials, pairing
71
+ codes, request bodies, and authorization headers out of logs.
72
+
73
+ If a CDN or another proxy sits in front, deriving the client IP requires an
74
+ explicit trusted-peer policy. Trusting its claimed IP header from arbitrary
75
+ Internet peers is unsafe. The direct-proxy instructions above do not configure
76
+ that extra trust chain.
77
+
78
+ ## 4. Start and connect
79
+
80
+ Use the [foreground or Linux service instructions](INSTALLATION.md#configure-https-and-start-the-relay),
81
+ then [connect the runtime and pair the app](INSTALLATION.md#connect-runtime-then-pair-the-app).
82
+ The app binds its device credential to the exact HTTPS origin and stores it in
83
+ the platform's protected credential storage. It does not follow authentication
84
+ or WebSocket redirects. A new server requires a new pairing; credentials are
85
+ never silently copied across hosts.
86
+
87
+ ## Optional push delivery
88
+
89
+ Push is disabled by default. To enable it, the operator adds
90
+ `notifications.delivery` to the private Socket configuration and restarts the
91
+ relay deliberately:
92
+
93
+ - A self-built app uses its own Firebase project and
94
+ `{ "type": "fcm", "serviceAccountFile": "./private-sender.json" }`.
95
+ The app and sender must belong to the same Firebase project. Keep the service
96
+ account on the server, owned by the service user with mode `0600`; a relative
97
+ path is resolved from the Socket configuration file.
98
+ - A Store build uses its publisher's trusted gateway with
99
+ `{ "type": "gateway", "gatewayOrigin": "https://<trusted-host>" }`.
100
+ Obtain that origin from the publisher's instructions. The publisher operates
101
+ the gateway and Firebase credentials; each user operates their own Socket.
102
+ Do not enter provider credentials into the app or run a second gateway merely
103
+ to use the Store build.
104
+
105
+ After server configuration, enable notifications in the app and grant platform
106
+ permission. Gateway builds enroll their own endpoint before registering it with
107
+ the paired Socket. A mismatched delivery mode is rejected; the app cannot replace
108
+ the operator's gateway URL. See [the notification protocol](PROTOCOL.md#notifications)
109
+ for device ownership, retention, delivery limitations and optional encrypted
110
+ answer previews. Gateway operation is separately described in
111
+ [Deployment](DEPLOYMENT.md#optional-notification-gateway).
112
+
113
+ ## Operation and recovery
114
+
115
+ ```sh
116
+ abot-socket devices --config /srv/abot-private/config.json
117
+ abot-socket revoke <device-id> --config /srv/abot-private/config.json
118
+ ```
119
+
120
+ The device list contains names, identifiers and expiry, never tokens. Revocation
121
+ also closes an already-open client WebSocket within the next validity sweep
122
+ (approximately one second). At 10 active devices, revoke an unused device before
123
+ pairing another. The server never silently evicts an existing device.
124
+
125
+ Sessions have a fixed maximum lifetime of 30 days and survive server restarts.
126
+ After expiry, pair again. Reinstalling the app may require pairing again depending
127
+ on the platform's secure-storage lifecycle; retaining the device's credential is
128
+ not a guarantee across uninstall, signing or bundle-ID changes.
129
+
130
+ Back up the private configuration, runtime credential and state directory using
131
+ a consistent filesystem/database snapshot or with the service stopped. Losing
132
+ state requires pairing devices again. Restoring an old backup may restore old
133
+ revocation state; reapply necessary revocations. Conversation history remains in
134
+ each runtime. Temporary relay routes do not survive restart, so reconcile
135
+ interrupted requests with runtime history rather than automatically resending.
136
+
137
+ The package does not provision a public TLS endpoint or install a service.
138
+ Real-device, proxy and runtime verification remains separate from automated tests.
139
+
140
+ ## Source-checkout development
141
+
142
+ These contributor commands require the repository checkout containing
143
+ `package.json`, `package-lock.json` and the source tests. They are not additional
144
+ steps for an installed npm/tarball consumer:
145
+
146
+ ```sh
147
+ npm ci --omit=optional --ignore-scripts
148
+ npm test
149
+ ```
150
+
151
+ To prepare a separate local development installation from that checkout, use
152
+ `node bin/abot-socket.mjs setup` and the same [installation guide](INSTALLATION.md).
153
+ Keep its private files outside source control. Tests use dummy identities and
154
+ loopback fixtures; they do not establish live runtime, phone or proxy readiness.
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@abot-ai/socket",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "registry": "https://registry.npmjs.org/",
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=24"
11
+ },
12
+ "scripts": {
13
+ "start": "node bin/abot-socket.mjs start",
14
+ "setup": "node bin/abot-socket.mjs setup",
15
+ "check-config": "node bin/abot-socket.mjs check-config",
16
+ "test": "node --test test/*.test.mjs",
17
+ "pair": "node bin/abot-socket.mjs pair",
18
+ "devices": "node bin/abot-socket.mjs devices",
19
+ "revoke": "node bin/abot-socket.mjs revoke",
20
+ "doctor": "node bin/abot-socket.mjs doctor",
21
+ "deployment": "node bin/abot-socket.mjs deployment",
22
+ "check-package": "node scripts/check-package.mjs"
23
+ },
24
+ "dependencies": {
25
+ "qrcode-generator": "2.0.4",
26
+ "ws": "8.21.0"
27
+ },
28
+ "description": "Self-hosted authenticated relay for ABot clients and runtimes",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/Dorshitrit/abot-socket.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/Dorshitrit/abot-socket/issues"
36
+ },
37
+ "homepage": "https://github.com/Dorshitrit/abot-socket#readme",
38
+ "files": [
39
+ "bin/abot-socket.mjs",
40
+ "src/main.mjs",
41
+ "src/auth/*.mjs",
42
+ "src/cli/*.mjs",
43
+ "src/config/*.mjs",
44
+ "src/deployment/*.mjs",
45
+ "src/notifications/*.mjs",
46
+ "src/pairing/*.mjs",
47
+ "src/protocol/*.mjs",
48
+ "src/relay/*.mjs",
49
+ "src/review/*.mjs",
50
+ "src/setup/*.mjs",
51
+ "src/transport/*.mjs",
52
+ "README.md",
53
+ "LICENSE",
54
+ "config.example.json",
55
+ "docs/SELF-HOSTING.md",
56
+ "docs/PROTOCOL.md",
57
+ "docs/SECURITY.md",
58
+ "docs/MIGRATION.md",
59
+ "docs/DEPLOYMENT.md",
60
+ "docs/INSTALLATION.md",
61
+ "docs/APP-REVIEW.md"
62
+ ],
63
+ "bin": {
64
+ "abot-socket": "bin/abot-socket.mjs"
65
+ }
66
+ }