@abot-ai/socket 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/bin/abot-socket.mjs +15 -0
- package/config.example.json +14 -0
- package/docs/APP-REVIEW.md +184 -0
- package/docs/DEPLOYMENT.md +124 -0
- package/docs/INSTALLATION.md +177 -0
- package/docs/MIGRATION.md +84 -0
- package/docs/PROTOCOL.md +371 -0
- package/docs/SECURITY.md +116 -0
- package/docs/SELF-HOSTING.md +161 -0
- package/package.json +65 -4
- package/src/auth/auth-database.mjs +105 -0
- package/src/auth/auth-limits.mjs +17 -0
- package/src/auth/auth-pairing.mjs +95 -0
- package/src/auth/auth-sessions.mjs +119 -0
- package/src/auth/auth-storage-path.mjs +70 -0
- package/src/auth/auth.mjs +45 -0
- package/src/auth/runtime-credentials.mjs +39 -0
- package/src/cli/cli-arguments.mjs +23 -0
- package/src/cli/cli-entry.mjs +84 -0
- package/src/cli/cli-environment.mjs +31 -0
- package/src/cli/cli-help.mjs +34 -0
- package/src/cli/cli-output.mjs +35 -0
- package/src/cli/cli.mjs +58 -0
- package/src/cli/client-capabilities.mjs +14 -0
- package/src/cli/doctor.mjs +106 -0
- package/src/config/config-validation.mjs +41 -0
- package/src/config/config.mjs +57 -0
- package/src/deployment/deployment-command.mjs +42 -0
- package/src/deployment/deployment-input.mjs +76 -0
- package/src/deployment/deployment-plan.mjs +62 -0
- package/src/deployment/deployment-templates.mjs +109 -0
- package/src/main.mjs +26 -0
- package/src/notifications/notification-delivery-credentials.mjs +48 -0
- package/src/notifications/notification-delivery-fcm.mjs +100 -0
- package/src/notifications/notification-delivery-gateway.mjs +36 -0
- package/src/notifications/notification-delivery-http.mjs +53 -0
- package/src/notifications/notification-delivery-payload.mjs +45 -0
- package/src/notifications/notification-delivery.mjs +43 -0
- package/src/notifications/notification-gateway-command.mjs +48 -0
- package/src/notifications/notification-gateway-store.mjs +159 -0
- package/src/notifications/notification-gateway.mjs +181 -0
- package/src/notifications/notification-http.mjs +39 -0
- package/src/notifications/notification-input.mjs +82 -0
- package/src/notifications/notification-outbox.mjs +60 -0
- package/src/notifications/notification-preview-crypto.mjs +42 -0
- package/src/notifications/notification-preview-schema.mjs +7 -0
- package/src/notifications/notification-preview-store.mjs +63 -0
- package/src/notifications/notification-preview-text.mjs +20 -0
- package/src/notifications/notification-projection.mjs +20 -0
- package/src/notifications/notification-schema.mjs +22 -0
- package/src/notifications/notification-service.mjs +147 -0
- package/src/notifications/notification-store.mjs +132 -0
- package/src/pairing/pairing-qr-payload.mjs +32 -0
- package/src/pairing/pairing-qr.mjs +42 -0
- package/src/protocol/approval-protocol.mjs +39 -0
- package/src/protocol/protocol-validation.mjs +137 -0
- package/src/protocol/protocol.mjs +120 -0
- package/src/protocol/spark-chat-protocol.mjs +51 -0
- package/src/relay/relay-approvals.mjs +86 -0
- package/src/relay/relay-client-admission.mjs +40 -0
- package/src/relay/relay-correlations.mjs +101 -0
- package/src/relay/relay-model-catalog.mjs +69 -0
- package/src/relay/relay-request-adoption.mjs +97 -0
- package/src/relay/relay-requests.mjs +211 -0
- package/src/relay/relay-runtime-registration.mjs +49 -0
- package/src/relay/relay-session-mutations.mjs +122 -0
- package/src/relay/relay.mjs +222 -0
- package/src/relay/request-ownership-schema.mjs +18 -0
- package/src/relay/request-ownership-store.mjs +115 -0
- package/src/review/review-demo-isolation.mjs +132 -0
- package/src/review/review-demo-setup.mjs +97 -0
- package/src/review/review-portal-access.mjs +64 -0
- package/src/review/review-portal-form.mjs +24 -0
- package/src/review/review-portal-page.mjs +68 -0
- package/src/review/review-portal-server.mjs +105 -0
- package/src/setup/setup-files.mjs +37 -0
- package/src/setup/setup-questions.mjs +62 -0
- package/src/setup/setup-wizard.mjs +50 -0
- package/src/setup/setup.mjs +42 -0
- package/src/transport/http-api.mjs +32 -0
- package/src/transport/server.mjs +175 -0
- package/src/transport/transport-limits.mjs +85 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Installation and first connection
|
|
2
|
+
|
|
3
|
+
ABot Socket is MIT-licensed and distributed as `@abot-ai/socket` on npm.
|
|
4
|
+
This guide covers package installation, server preparation and the first
|
|
5
|
+
runtime/app connection.
|
|
6
|
+
|
|
7
|
+
## Install the command
|
|
8
|
+
|
|
9
|
+
Use Node.js 24 or later and a persistent npm installation owned by an unprivileged
|
|
10
|
+
account. Linux with systemd is the server deployment target; macOS can run the
|
|
11
|
+
foreground service and local development checks. The authentication store uses
|
|
12
|
+
POSIX ownership/permissions and does not support Windows server installation.
|
|
13
|
+
|
|
14
|
+
Install the command from the official npm registry:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install --global @abot-ai/socket@0.2.0
|
|
18
|
+
abot-socket --version
|
|
19
|
+
abot-socket setup
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Prefer an explicit version for server upgrades. An installed package uses the
|
|
23
|
+
`abot-socket` command; it does not require a Git checkout or `npm ci`.
|
|
24
|
+
For an existing installation, retain its configuration and follow the
|
|
25
|
+
[state backup and schema upgrade guide](MIGRATION.md#durable-request-ownership-schema-upgrade)
|
|
26
|
+
before replacing the package. `setup` prepares a new configuration; it does not
|
|
27
|
+
migrate or overwrite an existing one.
|
|
28
|
+
|
|
29
|
+
Alternatively, prepare a tarball from the public source:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
git clone https://github.com/Dorshitrit/abot-socket.git
|
|
33
|
+
cd abot-socket
|
|
34
|
+
npm ci --omit=optional --ignore-scripts
|
|
35
|
+
npm pack --ignore-scripts --json > /tmp/abot-socket-pack.json
|
|
36
|
+
npm run check-package -- /tmp/abot-socket-pack.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then install that tarball (or a verified local tarball you already have):
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install --global ./abot-ai-socket-0.2.0.tgz
|
|
43
|
+
abot-socket --version
|
|
44
|
+
abot-socket setup
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Contributor commands are separate in [Self-hosting](SELF-HOSTING.md#source-checkout-development).
|
|
48
|
+
|
|
49
|
+
Do not use an ephemeral npx installation for a persistent system service. npm
|
|
50
|
+
installation has no lifecycle install hooks and does not run the setup wizard,
|
|
51
|
+
request certificates, contact your runtime or change OS service settings.
|
|
52
|
+
|
|
53
|
+
## Follow setup
|
|
54
|
+
|
|
55
|
+
The interactive wizard asks for:
|
|
56
|
+
|
|
57
|
+
1. A new private installation directory; the default is `~/.abot-socket`.
|
|
58
|
+
2. Your canonical HTTPS origin, for example `https://socket.example.net`.
|
|
59
|
+
3. The runtime identity and a private loopback listener port (default 8788).
|
|
60
|
+
4. On Linux as a non-root user: nginx, Apache or an existing HTTPS setup. Proxy
|
|
61
|
+
templates require paths to an existing full-chain certificate and private key.
|
|
62
|
+
5. Confirmation before writing any installation files.
|
|
63
|
+
|
|
64
|
+
Setup creates a new directory with mode `0700` and private files with mode `0600`:
|
|
65
|
+
|
|
66
|
+
| Output | Purpose |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `config.json` | HTTPS origin, listener port, state location and runtime grants containing token hashes. |
|
|
69
|
+
| `runtime-connection.json` | Bridge URL, runtime identity and secret; transfer only to the runtime computer. |
|
|
70
|
+
| `state/auth.sqlite3` | Pairing state, device credentials, notification metadata and pending delivery state. |
|
|
71
|
+
| `deployment/` | Reviewable Linux service/proxy plan, when selected in the interactive wizard. |
|
|
72
|
+
|
|
73
|
+
Setup defaults to Ask permission and port `8788`. Keep the installation outside
|
|
74
|
+
the package/source directory so package updates cannot replace its state.
|
|
75
|
+
No services are installed/started and certificate contents are never read.
|
|
76
|
+
Pairing details are displayed in the terminal, not stored as a plaintext file.
|
|
77
|
+
They expire after five minutes, so request a fresh bundle when HTTPS is ready.
|
|
78
|
+
|
|
79
|
+
The optional endpoint check at the end makes one bounded public HTTPS request only
|
|
80
|
+
if you explicitly choose it. A successful check proves relay health, not a working
|
|
81
|
+
runtime or model. Cancellation before file creation leaves no installation behind.
|
|
82
|
+
|
|
83
|
+
For unattended local file preparation, use explicit arguments instead of prompts:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
abot-socket setup --output /srv/abot-private \
|
|
87
|
+
--origin https://socket.example.net --runtime-id workstation --port 8788
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The parent directory must exist, and the output directory must not exist. Run as
|
|
91
|
+
the same unprivileged account that will operate the relay. The unattended command
|
|
92
|
+
creates the configuration/state only; use `deployment` separately for service files.
|
|
93
|
+
If state initialization fails after files were written, preserve the installation,
|
|
94
|
+
correct the private-state path/permissions and use `pair` to retry. Setup never
|
|
95
|
+
overwrites existing configuration or state.
|
|
96
|
+
|
|
97
|
+
## Configure HTTPS and start the relay
|
|
98
|
+
|
|
99
|
+
Follow [Deployment](DEPLOYMENT.md) to review and explicitly apply the generated
|
|
100
|
+
systemd and proxy files. Supply DNS, a valid certificate and the required proxy
|
|
101
|
+
software yourself; neither npm nor setup obtains or installs them. Existing
|
|
102
|
+
services must be checked before applying the plan.
|
|
103
|
+
|
|
104
|
+
For foreground operation instead of a system service:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
abot-socket start
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Commands use `~/.abot-socket/config.json` by default. For a custom location:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
abot-socket start --config /srv/abot-private/config.json
|
|
114
|
+
abot-socket check-config --config /srv/abot-private/config.json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`--config` overrides environment settings. Otherwise `ABOT_SOCKET_CONFIG` and its
|
|
118
|
+
legacy `SOCKET_V2_CONFIG` alias retain their existing precedence/conflict checks.
|
|
119
|
+
`setup` takes `--output` instead; it never overwrites an existing configuration.
|
|
120
|
+
|
|
121
|
+
## Connect runtime, then pair the app
|
|
122
|
+
|
|
123
|
+
Privately transfer `runtime-connection.json` to the intended runtime computer.
|
|
124
|
+
Configure its existing bridge with the generated `agentBridgeUrl`, `runtimeId`
|
|
125
|
+
and `runtimeToken`, using that runtime version's documented configuration names.
|
|
126
|
+
The file is a credential handoff, not a replacement runtime configuration or a
|
|
127
|
+
second bridge process. This is separate from app pairing; the runtime secret
|
|
128
|
+
must never go into the app.
|
|
129
|
+
No runtime source changes are required by this installer.
|
|
130
|
+
|
|
131
|
+
Recoverable approvals and continuation of computer-created requests require a
|
|
132
|
+
compatible app and a runtime advertising `request_approvals_v1`. SPARK companion
|
|
133
|
+
chat requires `spark_chat_v1` and a configured SPARK model. The app must check
|
|
134
|
+
the selected runtime's capabilities and readiness, not just the Socket's global
|
|
135
|
+
flags. Earlier clients and runtimes retain their supported legacy chat path;
|
|
136
|
+
upgrading the Socket alone does not enable these runtime features.
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
abot-socket doctor
|
|
140
|
+
abot-socket pair
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For custom configuration locations, add `--config` to those commands as above.
|
|
144
|
+
Doctor verifies TLS and the `/health` response, refuses redirects and has a five-second
|
|
145
|
+
total timeout. Its output always includes `runtimeVerified: false`. It never sends
|
|
146
|
+
credentials or generates a device session. The app must confirm that the selected
|
|
147
|
+
runtime is online and can complete a chat request.
|
|
148
|
+
|
|
149
|
+
In a normal terminal, `abot-socket pair` also displays a private QR for a scanner-capable
|
|
150
|
+
ABot App. Scan inside the app, check the displayed server address, and confirm.
|
|
151
|
+
The QR contains the same HTTPS URL, temporary pairing key and six-digit code; it
|
|
152
|
+
expires after five minutes and can be used once. Anyone with the QR can use that
|
|
153
|
+
pending pairing, so do not publish it or send it to a public QR service.
|
|
154
|
+
|
|
155
|
+
Generate a fresh QR **after** the service and HTTPS are ready; setup does not start
|
|
156
|
+
the server, and its initial pairing may expire while deployment is being completed.
|
|
157
|
+
The QR is printed locally, not saved to disk or served at a public endpoint.
|
|
158
|
+
Piped output keeps the existing text/JSON format. `TERM=dumb` or a narrow terminal
|
|
159
|
+
uses manual details instead; widen the terminal and run `pair` again if needed.
|
|
160
|
+
When running a remote command through SSH, allocate a terminal to display the QR.
|
|
161
|
+
|
|
162
|
+
Manual entry remains available for app versions without a scanner or when camera
|
|
163
|
+
access is unavailable: enter the new code, HTTPS URL and temporary pairing key. Afterwards,
|
|
164
|
+
`abot-socket devices` lists devices and `abot-socket revoke <device-id>` revokes one.
|
|
165
|
+
Both operate on the local private state without restarting the service.
|
|
166
|
+
|
|
167
|
+
Push delivery is optional and off by default. Ordinary chat and the notification
|
|
168
|
+
inbox work without it. See [push delivery configuration](SELF-HOSTING.md#optional-push-delivery)
|
|
169
|
+
for the difference between the Store application's gateway and a self-built app's
|
|
170
|
+
own Firebase sender.
|
|
171
|
+
|
|
172
|
+
## Updates
|
|
173
|
+
|
|
174
|
+
Keep private configuration/state outside the npm package directory. Updating the
|
|
175
|
+
package must not replace that state. Regenerate the service plan if Node or the
|
|
176
|
+
package path changes, and restart the service explicitly during the approved update.
|
|
177
|
+
No automatic update, live migration or service restart is implemented here.
|
|
@@ -0,0 +1,84 @@
|
|
|
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. Socket connections and transient subscriptions still
|
|
27
|
+
disappear on restart; durable request ownership is described below. Interrupted
|
|
28
|
+
requests must be reconciled against runtime state before retry.
|
|
29
|
+
|
|
30
|
+
## Durable request ownership schema upgrade
|
|
31
|
+
|
|
32
|
+
Schema v4 adds a routing-ownership table to the existing private database.
|
|
33
|
+
Schemas v1, v2 and v3 upgrade transactionally, preserving owner identity, paired
|
|
34
|
+
devices, pending pairing, notification metadata, preview keys and delivery queues.
|
|
35
|
+
No new dependency or additional state file is required.
|
|
36
|
+
|
|
37
|
+
New durable requests record their runtime, session, request, owner and
|
|
38
|
+
initiating device identifiers, plus routing status and timestamps. A paired owner
|
|
39
|
+
device can also recover an unowned computer-created request: after a fresh
|
|
40
|
+
response from the authorized runtime verifies the exact session and request, the
|
|
41
|
+
Socket records that ownership with the adopting device. Existing ownership is
|
|
42
|
+
never reassigned to a different owner or session. There is no bulk import, and
|
|
43
|
+
historical events cannot establish ownership.
|
|
44
|
+
|
|
45
|
+
This table contains no conversation content, approval presentation, decision or
|
|
46
|
+
continuation. The runtime remains authoritative for pending approvals and decision
|
|
47
|
+
receipts. Recovery requires a compatible client and `request_approvals_v1` on the
|
|
48
|
+
selected runtime; legacy requests are not automatically converted during upgrade.
|
|
49
|
+
|
|
50
|
+
Ownership is persisted before forwarding a new request. Open or uncertain records
|
|
51
|
+
do not expire or get evicted; a full 512-record ledger rejects new admissions.
|
|
52
|
+
Only confirmed terminal records expire after 12 hours. Confirmed whole-session
|
|
53
|
+
deletion retires that session's records. Revoking the originating
|
|
54
|
+
device does not erase ownership: another paired device of the same owner can
|
|
55
|
+
still recover the request, but revoked credentials cannot connect or receive push.
|
|
56
|
+
An interrupted send is never automatically resent from this ledger.
|
|
57
|
+
|
|
58
|
+
Stop the service and preserve the private state directory and matching code before
|
|
59
|
+
upgrading. Older binaries reject schema v4. Rollback requires restoring the
|
|
60
|
+
matching pre-upgrade database snapshot and code while stopped; do not delete state
|
|
61
|
+
to make an old binary start. Reconcile device revocations and deliveries made
|
|
62
|
+
since that snapshot before resuming service. This source change alone does not
|
|
63
|
+
upgrade or restart a deployed service.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Notification preview schema upgrade
|
|
67
|
+
|
|
68
|
+
Schema v3 adds a device-scoped preview-key table and nullable encrypted-preview
|
|
69
|
+
column on the existing outbox. Both schema v1 and v2 upgrade transactionally;
|
|
70
|
+
paired-device tokens, pending pairing, owner identity, notification metadata and
|
|
71
|
+
generic delivery queues are preserved. Preview remains off until that device
|
|
72
|
+
explicitly provisions a key after push registration. No runtime changes are needed.
|
|
73
|
+
|
|
74
|
+
Stop the service and back up the private state directory and matching deployed
|
|
75
|
+
code before upgrading. Older binaries reject schema v3. Rollback requires restoring
|
|
76
|
+
the matching pre-upgrade database snapshot together with old code while stopped;
|
|
77
|
+
do not point an old binary at the migrated state or delete the database to make it
|
|
78
|
+
start. Restoring a snapshot also restores its older device/revocation/delivery state,
|
|
79
|
+
so reconcile changes made since that snapshot before resuming service.
|
|
80
|
+
|
|
81
|
+
For gateway delivery, upgrade the publisher gateway before clients enable previews.
|
|
82
|
+
Keep previews off until compatible client support is installed. Old clients and
|
|
83
|
+
opted-out devices retain the original generic push behavior. This source change
|
|
84
|
+
alone does not deploy or enable preview delivery on an existing installation.
|
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
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
|
+
- `request.approvals.request`, `request.approval.decide.request` for an owned request on a capable runtime;
|
|
91
|
+
- `session.list.request`, `session.snapshot.request`, `request.events.request`;
|
|
92
|
+
- `spark.chat.open.request` on a SPARK-capable runtime;
|
|
93
|
+
- `session.delete.request`, `session.messages.clear.request`, `session.message.delete.request`;
|
|
94
|
+
- `attachment.upload.request`, `attachment.fetch.request`;
|
|
95
|
+
- relay-local `subscribe_request` for a known request belonging to this owner.
|
|
96
|
+
|
|
97
|
+
The relay returns `relay.ack`, `relay.error`, `relay.response`, or `relay.event`
|
|
98
|
+
with the appropriate command ID, runtime ID, and body. An acknowledgement means
|
|
99
|
+
dispatched or queued, not completed. Runtime correlations belong to the relay;
|
|
100
|
+
the client cannot choose them. Unknown command fields and unsupported operations
|
|
101
|
+
are rejected rather than forwarded as a generic admin API.
|
|
102
|
+
|
|
103
|
+
Tool permission values are `ask`, `full_access`, and `full_plus`, constrained by
|
|
104
|
+
the runtime grant. Setup grants only `ask`. The model catalog and supported
|
|
105
|
+
reasoning controls come from the connected runtime, not a hardcoded model list.
|
|
106
|
+
|
|
107
|
+
### SPARK companion chat
|
|
108
|
+
|
|
109
|
+
The Socket advertises `authenticated.capabilities.sparkChat`. Each runtime in
|
|
110
|
+
`/v2/runtimes` reports this capability only when online and advertising
|
|
111
|
+
`spark_chat_v1`. Clients must check the per-runtime capability; a runtime feature
|
|
112
|
+
alone does not establish support in an older Socket.
|
|
113
|
+
|
|
114
|
+
Send `{type:'spark.chat.open.request'}` inside the usual authenticated command
|
|
115
|
+
envelope. The client supplies no session, model, permission or correlation fields.
|
|
116
|
+
The correlated `spark.chat.open.response` contains `ok`, the canonical
|
|
117
|
+
`sessionId:'spark-companion'`, `modelProfileId` (or null), `toolPermissionMode` and
|
|
118
|
+
`readiness` (`ready`, `model_required`, or `model_unavailable`). Failure responses
|
|
119
|
+
contain a bounded public error code. Other runtime fields are not forwarded.
|
|
120
|
+
|
|
121
|
+
Opening creates or reuses the marked companion without starting a model request.
|
|
122
|
+
Load history and recover waiting approvals through the existing APIs. Send chat
|
|
123
|
+
through `run_request` with the returned session and permission mode; the runtime
|
|
124
|
+
selects its configured SPARK model. Opening can report a mode above the relay's
|
|
125
|
+
grant, but does not authorize it: the existing grant check rejects that send.
|
|
126
|
+
The runtime also rejects a mode changed since open with
|
|
127
|
+
`spark_chat_permission_mismatch`; refresh state and let the user retry, never
|
|
128
|
+
silently increase permissions or automatically resubmit an uncertain request.
|
|
129
|
+
|
|
130
|
+
Unsupported runtimes reject SPARK open/run with `spark_chat_unsupported`.
|
|
131
|
+
Correlations remain bound to the requesting client and exact runtime connection,
|
|
132
|
+
are consumed once, and expire after 30 seconds. This contract does not expose
|
|
133
|
+
SPARK settings writes, collection, jobs or autonomous activity.
|
|
134
|
+
|
|
135
|
+
### Session mutations
|
|
136
|
+
|
|
137
|
+
Each runtime in `/v2/runtimes` and the WebSocket `authenticated` frame includes
|
|
138
|
+
`capabilities.sessionMutation: true` for these three relay operations. Clients must treat an
|
|
139
|
+
absent/false flag as unsupported and require explicit user confirmation.
|
|
140
|
+
|
|
141
|
+
| Request body | Response body (inside `relay.response`) |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `{type:'session.delete.request', sessionId}` | `{type:'session.delete.response', ok, sessionId, deleted, deletedMessages, deletedRequests}` |
|
|
144
|
+
| `{type:'session.messages.clear.request', sessionId}` | `{type:'session.messages.clear.response', ok, sessionId, cleared, deletedMessages, deletedRequests, updatedAt}` |
|
|
145
|
+
| `{type:'session.message.delete.request', sessionId, messageId}` | `{type:'session.message.delete.response', ok, sessionId, messageId, deleted, updatedAt, deletedRequestId?, deletedRequestEvents?}` |
|
|
146
|
+
|
|
147
|
+
Use canonical message IDs from a fresh runtime snapshot, not local UI IDs.
|
|
148
|
+
Numeric message IDs are normalized to strings. Only `ok:true` together with
|
|
149
|
+
`deleted:true` / `cleared:true` confirms a change; reload canonical history after
|
|
150
|
+
the result. The runtime's single-message delete removes only that message row,
|
|
151
|
+
plus its associated request replay/context and memory checkpoints; sibling
|
|
152
|
+
message rows remain. Session deletion/clearing follows the existing runtime's
|
|
153
|
+
history and attachment cleanup semantics.
|
|
154
|
+
|
|
155
|
+
The relay rejects observed active/uncertain requests and in-flight uploads with
|
|
156
|
+
`session_busy`. Before forwarding a mutation it reads a complete snapshot with
|
|
157
|
+
`includeRequests:true`; active requests prevent deletion, and an incomplete or
|
|
158
|
+
unrecognized snapshot returns `session_state_unavailable`. No snapshot contents
|
|
159
|
+
are added to the mutation response. A pending mutation blocks another mutation,
|
|
160
|
+
new run or upload in that session (`session_mutation_pending`). It retains this
|
|
161
|
+
guard after client disconnect until the outcome or the shared 30-second timeout.
|
|
162
|
+
A client disconnected during the read check cannot begin deletion.
|
|
163
|
+
|
|
164
|
+
This check is not an atomic lock over independent Web UI clients. A request
|
|
165
|
+
started directly at the runtime after the check can race with deletion; the
|
|
166
|
+
runtime fences deleted generations and that request may be cancelled. Wait for
|
|
167
|
+
work on all clients to finish. `outcome_unknown` never authorizes an automatic
|
|
168
|
+
retry: reload authoritative history first. Missing sessions report
|
|
169
|
+
`session_not_found`; runtime failures remain failures.
|
|
170
|
+
|
|
171
|
+
## Runtime bridge
|
|
172
|
+
|
|
173
|
+
Connect to `/agent-bridge` with the separate runtime Bearer token, then send the
|
|
174
|
+
existing `{type:'register', agentId, features?, modelCatalog?}` message. The
|
|
175
|
+
registered ID must match the credential's grant. A second connection cannot
|
|
176
|
+
replace an already connected runtime with the same ID. Client device credentials
|
|
177
|
+
cannot authenticate this role.
|
|
178
|
+
|
|
179
|
+
## Recovery and capability limits
|
|
180
|
+
|
|
181
|
+
- A timeout or disconnect can return `outcome_unknown`; do not retry commands
|
|
182
|
+
automatically. Read canonical runtime history/replay to reconcile the result;
|
|
183
|
+
the durable decision retry rules below apply only to that decision, never a run.
|
|
184
|
+
- Live `eventSequence` and replay `seqNo` are separate counters. `afterSeq` uses
|
|
185
|
+
the persisted replay counter only.
|
|
186
|
+
- Legacy `tool_approval_response` requires a live observed pending approval for
|
|
187
|
+
the correct owner, runtime and request. Positive approval expires after five
|
|
188
|
+
minutes; an explicit decline can remain possible once on that same active
|
|
189
|
+
runtime connection and request. Historical replay never creates authority.
|
|
190
|
+
- `cancel` remains false. Restart, cron, configuration, and generic administration
|
|
191
|
+
are not exposed.
|
|
192
|
+
- The relay keeps bounded routing state and persistent durable-request ownership
|
|
193
|
+
metadata. It is not an offline queue, conversation store, or exactly-once
|
|
194
|
+
execution service.
|
|
195
|
+
|
|
196
|
+
### Durable approval recovery
|
|
197
|
+
|
|
198
|
+
The WebSocket `authenticated.capabilities.durableApprovals` flag describes
|
|
199
|
+
whether the Socket has the persistent ownership store required by this API.
|
|
200
|
+
Each `/v2/runtimes` entry reports the flag separately: it is true only while that
|
|
201
|
+
runtime is online and advertises `request_approvals_v1`. Clients must check the
|
|
202
|
+
chosen runtime, not infer its support from the global flag. Older runtimes remain
|
|
203
|
+
usable through the legacy path.
|
|
204
|
+
|
|
205
|
+
To opt in, a new `run_request` includes `approvalMode:'durable_v1'`. Omitting it
|
|
206
|
+
retains legacy behavior. The relay persists its owner, originating device,
|
|
207
|
+
runtime, session and request identity before forwarding the run. It does not
|
|
208
|
+
persist the prompt, approval payload, decision or runtime continuation. An
|
|
209
|
+
unavailable capability rejects the opt-in instead of silently downgrading it.
|
|
210
|
+
|
|
211
|
+
| Request body | Response body inside `relay.response` |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `{type:'request.approvals.request', sessionId, requestId}` | `{type:'request.approvals.response', correlationId, sessionId, requestId, ok:true, approvals:[{request,wait?}], lifecycle, finalState}` |
|
|
214
|
+
| `{type:'request.approval.decide.request', sessionId, requestId, approvalId, approved, reason?, generation?, waitId?, revision?, commandId?}` | `{type:'request.approval.decide.response', correlationId, sessionId, requestId, ok:true, result:{accepted,duplicate?,reason?}}` |
|
|
215
|
+
|
|
216
|
+
Runtime failures use `ok:false,error` with the same scope. The relay assigns the
|
|
217
|
+
private correlation; clients correlate the outer command `id`. Only a response
|
|
218
|
+
from the current runtime connection with matching request, session and correlation
|
|
219
|
+
is accepted. One approval query/decision per request may be in flight at a time.
|
|
220
|
+
|
|
221
|
+
Recovery requires a durable ownership record admitted by this Socket. When an
|
|
222
|
+
authorized paired device queries approvals for a request created directly on
|
|
223
|
+
its runtime, the relay can establish that record after a fresh, privately
|
|
224
|
+
correlated snapshot from the current runtime connection confirms the exact
|
|
225
|
+
session and request. Client-provided history or a replayed
|
|
226
|
+
`tool.approval.required` event cannot establish ownership. An existing record
|
|
227
|
+
for another owner or session is never reassigned. A decision alone cannot
|
|
228
|
+
establish ownership; query the current approvals first.
|
|
229
|
+
After a Socket or runtime restart, an authenticated device belonging to the same
|
|
230
|
+
installation owner may recover the request; revoking the originating device does
|
|
231
|
+
not transfer its credential or notification inbox. Recovery also restores live
|
|
232
|
+
delivery for that request. It does not rerun it or reconstruct missed events.
|
|
233
|
+
|
|
234
|
+
The runtime remains authoritative for pending approvals and their lifecycle.
|
|
235
|
+
Durable waits do not inherit the relay's legacy five-minute approval timeout.
|
|
236
|
+
For a returned `wait`, send its exact `generation`, `waitId` and `revision` together
|
|
237
|
+
with a stable client-generated `commandId`. All four fields are required together;
|
|
238
|
+
omit all four only for a legacy approval returned by the runtime. Do not substitute
|
|
239
|
+
new wait identities when retrying an uncertain decision. Reuse the original
|
|
240
|
+
decision and `commandId` so the runtime can recognize its durable receipt, then
|
|
241
|
+
reload canonical state. Accepted duplicate decisions must not execute the action
|
|
242
|
+
again. A declined or stale decision remains a refusal, not an invitation to retry
|
|
243
|
+
against a newer approval.
|
|
244
|
+
|
|
245
|
+
Ownership storage is capped at 512 records. Open records are not age-evicted;
|
|
246
|
+
confirmed terminal records are retained for 12 hours, or removed after confirmed
|
|
247
|
+
deletion of their complete session. Capacity exhaustion rejects new admission.
|
|
248
|
+
|
|
249
|
+
Use the new pairing-capable client. Email-only personal builds cannot enroll
|
|
250
|
+
through this authentication contract. Clients and runtime adapters that omit the
|
|
251
|
+
durable approval opt-in retain legacy behavior.
|
|
252
|
+
|
|
253
|
+
## Notifications
|
|
254
|
+
|
|
255
|
+
`capabilities.notifications: true` advertises the authenticated notification API.
|
|
256
|
+
Notifications are owned by the **paired device that initiated the request**, or
|
|
257
|
+
by the **device that first adopts an unowned computer-created request** through
|
|
258
|
+
the verified recovery flow above. Ownership does not follow the current
|
|
259
|
+
WebSocket connection: later recovery by another paired device does not transfer
|
|
260
|
+
the notification inbox. Other paired devices cannot list or mark that device's
|
|
261
|
+
records. There are no new unsolicited WebSocket frame types; clients poll the
|
|
262
|
+
inbox while foregrounded and reconcile on resume or push tap.
|
|
263
|
+
|
|
264
|
+
| Method and path | Request / response |
|
|
265
|
+
| --- | --- |
|
|
266
|
+
| `GET /v2/notifications?limit=50&before=<cursor>` | Device inbox, descending time; limit 1–100; opaque optional cursor. |
|
|
267
|
+
| `POST /v2/notifications/read` | `{ids:[...]}` (maximum 100) → `{ok:true}`. Unknown or foreign IDs have no effect. |
|
|
268
|
+
| `POST /v2/notifications/presence` | `{foreground:boolean,runtimeId?,sessionId?}` → `{ok:true}`; a session requires its runtime. |
|
|
269
|
+
| `PUT /v2/notifications/push` | `{provider:'fcm',token}` or `{provider:'gateway',endpointId,sendToken}` → `{ok:true}`. |
|
|
270
|
+
| `DELETE /v2/notifications/push` | Remove this device's push registration, preview key and queued deliveries → `{ok:true}`. |
|
|
271
|
+
| `PUT /v2/notifications/push/preview` | Exact `{keyId,key}` → `{ok:true}`; opt in after registering push. Replaces the device's key. |
|
|
272
|
+
| `DELETE /v2/notifications/push/preview` | Remove this device's preview key and queued ciphertext, retaining generic deliveries → `{ok:true}`. |
|
|
273
|
+
|
|
274
|
+
The inbox response is `{version:1,items,nextCursor,unreadCount,unreadByRuntime,
|
|
275
|
+
unreadBySession,delivery,registration,preview}`. Each item is
|
|
276
|
+
`{id,runtimeId,sessionId,requestId,kind,createdAt,readAt,expiresAt}`; timestamps
|
|
277
|
+
are epoch milliseconds, and `readAt` is null until read. `kind` is `completed`,
|
|
278
|
+
`failed` or `approval_required`. `unreadByRuntime` maps runtime IDs to counts;
|
|
279
|
+
`unreadBySession` maps runtime IDs to session-ID/count maps. `delivery` is
|
|
280
|
+
`{mode:'disabled'|'fcm'|'gateway',configured:boolean}`, and `registration` is
|
|
281
|
+
`{enabled:boolean}`. `preview` is `{supported:true,enabled:boolean,keyId:string|null}`.
|
|
282
|
+
Older servers omit `preview`; clients treat that as unsupported and off. Keys
|
|
283
|
+
and registration credentials are never returned by this API.
|
|
284
|
+
|
|
285
|
+
Foreground presence lasts 45 seconds. Renew while active; `foreground:false`
|
|
286
|
+
clears it. Presence is advisory: the phone may be suspended before its background
|
|
287
|
+
update arrives, so presence alone never marks records read or suppresses delivery.
|
|
288
|
+
Only an explicit authenticated read acknowledgement consumes records and cancels
|
|
289
|
+
queued deliveries. Clients acknowledge fetched IDs after showing the conversation.
|
|
290
|
+
Counts share the same ledger; they are not runtime transcript read receipts.
|
|
291
|
+
Push taps resolve the notification ID through this authenticated inbox
|
|
292
|
+
before opening a conversation; a push payload grants no execution or approval authority.
|
|
293
|
+
|
|
294
|
+
Only validated live `completed`, `failed` and `tool.approval.required` frames for
|
|
295
|
+
known relay requests create records. Streaming/progress events and replay do not.
|
|
296
|
+
Granted/rejected/terminal events cancel pending approval deliveries. Approval
|
|
297
|
+
notifications expire after five minutes; other records after at most seven days
|
|
298
|
+
or device credential expiry. Storage is capped at 500 records per device.
|
|
299
|
+
Expiration, logout and revocation remove registrations and pending deliveries.
|
|
300
|
+
The outbox retries at most five times, with bounded backoff. Duplicate source
|
|
301
|
+
events are deduplicated, but provider delivery is not an exactly-once guarantee.
|
|
302
|
+
|
|
303
|
+
The inbox and queued deliveries survive relay restarts. Live subscriptions must
|
|
304
|
+
be restored explicitly; durable ownership permits the scoped recovery described
|
|
305
|
+
above. This is not a complete runtime-wide event subscription or an offline
|
|
306
|
+
execution queue. Work started outside the relay has no automatic notification
|
|
307
|
+
subscription. After an existing computer request is verified and adopted, later
|
|
308
|
+
validated live events for that request can create notifications for its adopting
|
|
309
|
+
device; recovery does not backfill missed notifications. Never interpret a missing
|
|
310
|
+
notification as a failed request or auto-retry it.
|
|
311
|
+
|
|
312
|
+
Push is disabled unless private operator configuration includes
|
|
313
|
+
`notifications.delivery`. A self-built client uses
|
|
314
|
+
`{type:'fcm',serviceAccountFile:'./private-sender.json'}`; the path is resolved
|
|
315
|
+
relative to the private socket configuration, and sender credentials stay on
|
|
316
|
+
that operator's server. The client must belong to the same configured Firebase
|
|
317
|
+
project. Official builds can use `{type:'gateway',gatewayOrigin:'https://<trusted-host>'}`
|
|
318
|
+
with a separately enrolled endpoint. Clients cannot choose a gateway URL or send
|
|
319
|
+
provider credentials through the registration API. A missing or mismatched
|
|
320
|
+
configured transport returns `409 push_unavailable`; ordinary chat and the inbox
|
|
321
|
+
continue to work with delivery disabled.
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
### Optional encrypted answer previews
|
|
325
|
+
|
|
326
|
+
Previews are off by default and enabled per paired device, only after an active
|
|
327
|
+
push registration exists. The app generates a random 32-byte AES-256-GCM key and
|
|
328
|
+
16-byte key ID, sends both over authenticated HTTPS to its own Socket, and enables
|
|
329
|
+
local display only after acknowledgement. Both fields use canonical unpadded
|
|
330
|
+
base64url. No email, additional identity provider, or runtime change is involved.
|
|
331
|
+
Provider-token rotation preserves the preview setting. Key replacement clears
|
|
332
|
+
pending ciphertext. Disabling previews clears the key and ciphertext while leaving
|
|
333
|
+
the generic queue intact; unregistering push removes the entire delivery queue.
|
|
334
|
+
In-flight sends are aborted where possible, but provider-accepted notifications
|
|
335
|
+
cannot be recalled. The client must discard its key immediately on disable/logout.
|
|
336
|
+
|
|
337
|
+
Only a nonempty final `completed.output` is eligible. The Socket takes a bounded
|
|
338
|
+
prefix, removes unsafe controls and bidi override characters, and truncates on a
|
|
339
|
+
Unicode scalar boundary to at most 1024 UTF-8 bytes, including an ellipsis when
|
|
340
|
+
needed. It is an excerpt, not secret redaction. Approval, failure and progress
|
|
341
|
+
content is never included. No plaintext preview is stored in the inbox or outbox.
|
|
342
|
+
The inbox remains metadata-only; encrypted content exists only in the pending
|
|
343
|
+
outbox and is removed on delivery, read acknowledgement, revocation or expiry.
|
|
344
|
+
|
|
345
|
+
The exact encrypted envelope is `{v:1,keyId,nonce,ciphertext,expiresAt}`. Nonce is
|
|
346
|
+
12 random bytes. Ciphertext contains encrypted UTF-8 followed by the 16-byte GCM
|
|
347
|
+
authentication tag (at most 1040 bytes), using canonical unpadded base64url.
|
|
348
|
+
Authenticated additional data is UTF-8, with literal newline separators:
|
|
349
|
+
|
|
350
|
+
```text
|
|
351
|
+
ABot push preview v1
|
|
352
|
+
<keyId>
|
|
353
|
+
<notificationId>
|
|
354
|
+
<expiresAt decimal epoch milliseconds>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The expiry equals notification expiry, capped by the device credential and seven
|
|
358
|
+
days. The device rejects an unknown key, altered ID/expiry/content, expired data
|
|
359
|
+
or malformed envelope and retains a generic alert. The gateway receives neither
|
|
360
|
+
the key nor plaintext. Its `/v1/deliver` accepts an optional strictly validated
|
|
361
|
+
`preview` field alongside the existing fixed delivery fields. Upgrade the gateway
|
|
362
|
+
before enabling previews; older gateways do not accept this field.
|
|
363
|
+
|
|
364
|
+
Encrypted FCM sends have no top-level `notification` or `android.notification`.
|
|
365
|
+
Their string-valued data is `notificationId`, `abotPreview` (envelope JSON),
|
|
366
|
+
`abotKind` (`completed`), `abotExpiresAt` and `abotBadge`. Android renders the
|
|
367
|
+
notification locally. APNs retains a generic alert and sets `mutable-content:1`
|
|
368
|
+
for the app's notification-service extension. Generic sends keep their existing
|
|
369
|
+
payload. Serialized provider envelopes are bounded to 4096 UTF-8 bytes. Foreground
|
|
370
|
+
display and navigation continue to use the authenticated inbox; push cannot grant
|
|
371
|
+
access or approval authority.
|