@agnt-sdk/simon-server 0.0.0-stage → 0.1.1

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 +201 -0
  2. package/NOTICE +12 -0
  3. package/README.md +317 -2
  4. package/dist/account.d.ts +59 -0
  5. package/dist/account.js +244 -0
  6. package/dist/auth.d.ts +42 -0
  7. package/dist/auth.js +96 -0
  8. package/dist/config.d.ts +219 -0
  9. package/dist/config.js +300 -0
  10. package/dist/context.d.ts +57 -0
  11. package/dist/context.js +2 -0
  12. package/dist/continuity.d.ts +35 -0
  13. package/dist/continuity.js +74 -0
  14. package/dist/deliver.d.ts +22 -0
  15. package/dist/deliver.js +208 -0
  16. package/dist/errors.d.ts +11 -0
  17. package/dist/errors.js +11 -0
  18. package/dist/extensions.d.ts +216 -0
  19. package/dist/extensions.js +75 -0
  20. package/dist/fanout.d.ts +32 -0
  21. package/dist/fanout.js +79 -0
  22. package/dist/http.d.ts +69 -0
  23. package/dist/http.js +112 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.js +12 -0
  26. package/dist/main.d.ts +2 -0
  27. package/dist/main.js +41 -0
  28. package/dist/net/dns.d.ts +31 -0
  29. package/dist/net/dns.js +100 -0
  30. package/dist/net/fetch.d.ts +48 -0
  31. package/dist/net/fetch.js +173 -0
  32. package/dist/net/ip.d.ts +38 -0
  33. package/dist/net/ip.js +159 -0
  34. package/dist/net/psl.d.ts +22 -0
  35. package/dist/net/psl.js +91 -0
  36. package/dist/net/public-suffixes.d.ts +10 -0
  37. package/dist/net/public-suffixes.js +6885 -0
  38. package/dist/notice.d.ts +5 -0
  39. package/dist/notice.js +61 -0
  40. package/dist/outbox.d.ts +14 -0
  41. package/dist/outbox.js +356 -0
  42. package/dist/parts.d.ts +29 -0
  43. package/dist/parts.js +443 -0
  44. package/dist/public.d.ts +43 -0
  45. package/dist/public.js +134 -0
  46. package/dist/published.d.ts +3 -0
  47. package/dist/published.js +140 -0
  48. package/dist/rate.d.ts +28 -0
  49. package/dist/rate.js +77 -0
  50. package/dist/readers.d.ts +31 -0
  51. package/dist/readers.js +48 -0
  52. package/dist/register.d.ts +4 -0
  53. package/dist/register.js +401 -0
  54. package/dist/resolver.d.ts +140 -0
  55. package/dist/resolver.js +386 -0
  56. package/dist/send.d.ts +30 -0
  57. package/dist/send.js +216 -0
  58. package/dist/server.d.ts +52 -0
  59. package/dist/server.js +234 -0
  60. package/dist/signal.d.ts +5 -0
  61. package/dist/signal.js +137 -0
  62. package/dist/store/contract.d.ts +4 -0
  63. package/dist/store/contract.js +1177 -0
  64. package/dist/store/sqlite-migration.d.ts +82 -0
  65. package/dist/store/sqlite-migration.js +1019 -0
  66. package/dist/store/sqlite.d.ts +108 -0
  67. package/dist/store/sqlite.js +992 -0
  68. package/dist/store/store.d.ts +400 -0
  69. package/dist/store/store.js +23 -0
  70. package/dist/sync.d.ts +19 -0
  71. package/dist/sync.js +199 -0
  72. package/dist/testing.d.ts +38 -0
  73. package/dist/testing.js +87 -0
  74. package/dist/webhooks.d.ts +10 -0
  75. package/dist/webhooks.js +162 -0
  76. package/dist/worker.d.ts +15 -0
  77. package/dist/worker.js +33 -0
  78. package/package.json +57 -4
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,12 @@
1
+ SIMON Protocol
2
+ Copyright 2026 3Gen Internet Corporation
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (see LICENSE).
5
+
6
+ "AGNT" is a trademark of 3Gen Internet Corporation. The license does not grant permission to use it;
7
+ see TRADEMARK.md.
8
+
9
+ This product includes a snapshot of the ICANN section of the Public Suffix List
10
+ (server/src/net/public-suffixes.ts), from https://publicsuffix.org/, which is subject to the terms of
11
+ the Mozilla Public License, v. 2.0. A copy of the MPL is at https://mozilla.org/MPL/2.0/. The snapshot is
12
+ written by server/scripts/write-public-suffixes.ts from the list of 2024-01-07.
package/README.md CHANGED
@@ -1,3 +1,318 @@
1
- # Temporary Holding Version
1
+ # @agnt-sdk/simon-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The basic SIMON server: everything in [`spec/PROTOCOL.md`](../spec/PROTOCOL.md) and [`spec/CLIENT.md`](../spec/CLIENT.md), and nothing else. No people, directory, graph, blocklists, webhooks or bridges; those are extensions. Runtime dependencies: `@agnt-sdk/simon-protocol` and Node's standard library (`node:http`, `node:sqlite`, `node:crypto`, `node:dns`). Node 26 runs it from this repository's TypeScript sources; the npm package is compiled and runs on Node 24.6.0 or later.
4
+
5
+ ```sh
6
+ npm install @agnt-sdk/simon-server # the library, and the simon-server binary
7
+ ```
8
+
9
+ | Import | What it gives |
10
+ | --- | --- |
11
+ | `@agnt-sdk/simon-server` | `createServer`, `defineConfig`, `loadConfig`, `SqliteStore`, the `Store` interface and its record types, the extension types, `json`, the DNS and HTTP seams |
12
+ | `@agnt-sdk/simon-server/contract` | `storeContract` and `retractionCapacityContract`: the Store contract as `node:test` suites |
13
+ | `@agnt-sdk/simon-server/testing` | `Clock`, `FakeDns`, `FakeNet`, `toRequest`, `replyBody`: the outside world as test doubles |
14
+
15
+ `node:sqlite` is loaded only when a `SqliteStore` is made, so an application on another Store (such as [`@agnt-sdk/simon-store-mongo`](../store-mongo/README.md)) never loads it. `simon-server help` lists the main settings.
16
+
17
+ ## Run
18
+
19
+ ```sh
20
+ SIMON_DEV=1 SIMON_ORIGIN=http://localhost:8080 SIMON_HOSTED_DOMAINS=example.test node server/src/main.ts
21
+ ```
22
+
23
+ In production: an https origin (TLS in front of the server, or `SIMON_TLS_CERT_FILE` and `SIMON_TLS_KEY_FILE`), a server key, and the domains you serve pointed at it (`_simon.<domain> TXT "v=simon1; server=<origin>"`).
24
+
25
+ ## Configuration
26
+
27
+ Every variable, read once at startup; an invalid value throws. Source: `src/config.ts`.
28
+
29
+ | Variable | Meaning | Default |
30
+ | --- | --- | --- |
31
+ | `SIMON_ORIGIN` | This server's origin, `https://host[:port]` | required |
32
+ | `SIMON_DEV` | `1`: development. http and local hosts allowed, register codes logged, a throwaway key when none is set | `0` |
33
+ | `SIMON_HOSTED_DOMAINS` | Domains whose names agents take at once, comma separated | none |
34
+ | `SIMON_LEASE_DOMAIN` | Domain for leased addresses (`register` with a key alone) | none |
35
+ | `SIMON_SERVER_KEY` / `_FILE` | Signing key: `<alg>:<base64url 32-byte seed>` (`scripts/server-keys.sh` makes one), or a PKCS #8 PEM file | required outside dev |
36
+ | `SIMON_NEXT_SERVER_KEY` / `_FILE` | Announced next key: listed, not used. List it 10 minutes before swapping it in | none |
37
+ | `SIMON_DB` | sqlite file; `:memory:` for none | `simon.sqlite` |
38
+ | `SIMON_PORT` | Port | `8080` |
39
+ | `SIMON_LISTEN_HOST` | The address to listen on; `127.0.0.1` behind a tunnel | every interface |
40
+ | `SIMON_CLIENT_IP_HEADER` | A header proxies set to the client's address: `x-forwarded-for` behind a load balancer, `cf-connecting-ip` behind Cloudflare. Believed only from this machine and from `SIMON_TRUSTED_PROXIES` | none: the connection's address |
41
+ | `SIMON_TRUSTED_PROXIES` | Networks a load balancer connects from, in CIDR notation, comma separated, each `/8` or narrower. Needs `SIMON_CLIENT_IP_HEADER` | none: this machine only |
42
+ | `SIMON_KEEP_ALIVE_TIMEOUT` | Seconds an idle connection stays open. Behind a load balancer, longer than its idle timeout | `5` |
43
+ | `SIMON_TRUSTED_CLAIM_SERVERS` | Origins whose claims agents here are shown, beyond the authoritative ones (below) | none |
44
+ | `SIMON_DOH_RESOLVERS` | Two or more DNS-over-HTTPS JSON endpoints; every TXT answer must agree with them | none |
45
+ | `SIMON_DNS_SERVERS` | DNS servers to ask instead of the system's, IP addresses (a tailnet's `100.100.100.100`, for one) | the system's |
46
+ | `SIMON_RATES` | Overrides, `name=count/seconds,...`; names in `DEFAULT_RATES` | see `config.ts` |
47
+ | `SIMON_PUBLIC_SUFFIX_LIST` | A newer copy of the Public Suffix List (`public_suffix_list.dat`) to judge registrable domains by; its ICANN section is read, and a file without its section markers or with fewer than 5000 rules there stops the server from starting | the snapshot of 2024-01-07 that ships in `src/net/public-suffixes.ts` |
48
+ | `SIMON_RETIRED_AGENT_DOCUMENTS` | `private`: a retired address answers its agent-document lookup with the same 404 as an unknown address. `public`: it answers its signed retired document, keys and unpairing included (see Storage). Any other value stops the server from starting | `private` |
49
+ | `SIMON_RATE_EXEMPT_NETWORKS` | Networks whose clients the per-client `request`, `register`, `codeClient` and `agentMiss` rates do not count, in CIDR notation, comma separated, each `/8` or narrower: a hosting service that registers and speaks for many agents from a few addresses. Its agents' own rates (`send`, `destinations`, `upload`, `code`) still apply | none |
50
+ | `SIMON_KEY_CHANGE_HOLD` | Seconds to hold a domain whose server signs with a key it was not seen to vouch for (spec 11.1) | `3600` |
51
+ | `SIMON_RECOVERY_WAIT` | Seconds a recovery by code waits while the agent still holds a valid key; the agent is told, and the address is sent a code that cancels it | `259200` (72 h) |
52
+ | `SIMON_MAX_CONNECTIONS` | Connections served at once | `1024` |
53
+ | `SIMON_RETENTION_DAYS` | Days messages are kept (at least 7) | `30` |
54
+ | `SIMON_DEV_RESOLVE` | Development only: `domain=origin,...` answered without DNS | none |
55
+ | `SIMON_TRUSTED_NETWORKS` | Private networks outbound calls may reach, in CIDR notation, comma separated. Each is `/8` or narrower; `0.0.0.0/0`, `::/0` and anything broader than `/8` stop the server at startup. Calls stay https only | none: public addresses only |
56
+ | `SIMON_TLS_CERT_FILE`, `SIMON_TLS_KEY_FILE` | Serve https directly with this PEM certificate chain and private key. Both or neither; the origin must then be https | plain http (put TLS in front) |
57
+
58
+ **Outbound calls** (deliveries, documents, well-known answers, part fetches) go through an SSRF guard:
59
+ https only, TLS 1.3 or later, no redirects, and only to public addresses, so a domain cannot point this
60
+ server at a private one. Lookups made before a delivery's signature verifies are counted against the
61
+ client's network (`lookup` in `DEFAULT_RATES`) and against the registrable domain of the name looked up
62
+ (`lookupDomain`, by the Public Suffix List's ICANN section, counted in the store so every process shares
63
+ it), only when made, and at most 24 run at once. Those a
64
+ delivery asks for once its carrier is known, its recipients' domains and its claims, are counted against
65
+ the carrier (`carrierLookup`) and run in a pool of their own, at most 8 for one carrier. An agent's send and
66
+ the outbox look up in a third pool, which nothing from outside can fill. `SIMON_TRUSTED_NETWORKS` is the one exception, for servers that federate over a private
67
+ network. A host with any address outside the public internet and outside every trusted network is
68
+ refused.
69
+
70
+ **Probing for addresses.** Anyone may read an agent document, which says an agent exists: verification
71
+ needs that. To make testing many addresses expensive, the server counts each client's requests for
72
+ addresses it has no agent for (`agentMiss` in `DEFAULT_RATES`, 100 an hour per client IP). Past that,
73
+ the client is answered `429` for every agent document, an agent's or not, until the hour ends, so the
74
+ refusal tells nothing about any address. Verifying a message asks for an agent that wrote it, so other
75
+ servers' verification seldom counts. A signed request from another server gets no larger budget:
76
+ anyone with a domain can sign as a server.
77
+
78
+ **Claims from other servers.** A carrier's claims about a message's author are its word, not proof.
79
+ Agents here are always shown a carrier's authoritative claims: an `acts-for` or an `identifier` for an
80
+ email address on the carrier's own domain. They are shown its other claims, `operator` and claims about
81
+ addresses elsewhere among them, only when the carrier's origin is in `SIMON_TRUSTED_CLAIM_SERVERS` (spec
82
+ section 6.2). So with the default settings, agents here see no `operator` claim from another server, and
83
+ no claim about an address that server does not speak for. To show a carrier's, list it:
84
+ `SIMON_TRUSTED_CLAIM_SERVERS=https://simon.partner.example`.
85
+
86
+ **The well-known answer.** When this server's origin host is itself a hosted domain or the lease domain,
87
+ the server answers `GET /.well-known/simon` with `{"server": <origin>}`. That domain then resolves to it
88
+ by the well-known method of spec section 3, with no TXT record.
89
+
90
+ ## Behind a load balancer
91
+
92
+ The server can run as one container behind a load balancer that ends TLS, such as an AWS Application
93
+ Load Balancer in front of an ECS service. The image is the Dockerfile's default stage (`docker build -t
94
+ simon-server .`): it listens on 8080 with plain http and keeps its sqlite file in `/data`.
95
+ `scripts/image-check.sh` builds it and runs it this way.
96
+
97
+ ```sh
98
+ SIMON_ORIGIN=https://simon.example.com # the public origin, https at the balancer
99
+ SIMON_SERVER_KEY_FILE=/run/secrets/simon-server-key # or SIMON_SERVER_KEY, from a secret store
100
+ SIMON_HOSTED_DOMAINS=example.com
101
+ SIMON_CLIENT_IP_HEADER=x-forwarded-for
102
+ SIMON_TRUSTED_PROXIES=10.0.0.0/24,10.0.1.0/24 # the balancer's own subnets, not the whole VPC
103
+ SIMON_KEEP_ALIVE_TIMEOUT=65 # longer than the balancer's idle timeout (60 s on ALB)
104
+ ```
105
+
106
+ - **The client's address.** Rate limits count clients by address. The balancer appends the address it
107
+ heard from to `X-Forwarded-For`, and the server reads the list from the right, past every address in
108
+ `SIMON_TRUSTED_PROXIES`: the first one outside is the client. Name only the balancer's own subnets; a
109
+ network that holds anything else, a whole VPC for one, lets what runs there choose the address it is
110
+ counted under. The balancer appends to the header without client ports (on ALB, the default `append`
111
+ mode with `routing.http.xff_client_port.enabled` off): an entry with a port is no address, so every
112
+ client would be counted as the balancer, and a balancer that does not append passes on what clients
113
+ wrote.
114
+ - **A service of your own in front.** A service that registers and speaks for many agents reaches the
115
+ server from a few addresses, which the per-client `request` and `register` rates would soon refuse.
116
+ `SIMON_RATE_EXEMPT_NETWORKS` names its networks; they are judged by the client's address as read above,
117
+ so nobody outside the trusted proxies' networks can claim one, and nobody inside them should be able
118
+ to. Its agents' own rates (`send`, `destinations`, `upload`, `code`) still
119
+ apply, and so do `lookup` and `badSignature`, which bound deliveries from other servers.
120
+ - **Health checks.** `GET /health` answers `200 {"status":"ok"}` while the server and its store answer,
121
+ and `503` when the store does not. It says nothing else.
122
+ - **Keep-alive.** A balancer reuses idle connections to its targets. If the server closes one first, a
123
+ request on it fails with `502`, so the server's timeout must be the longer. Raise it only when nothing
124
+ but the balancer can reach the server: one client holding long-lived idle connections fills
125
+ `SIMON_MAX_CONNECTIONS` within its request rate.
126
+ - **One task.** sqlite has one writer: run exactly one server on the data, and never start a replacement
127
+ beside it (on ECS, a maximum of 100 percent and a minimum healthy of 0). `SIGTERM` stops it cleanly.
128
+ - **The volume.** `/data` is a block volume, not a network file system, whose sqlite locking cannot be
129
+ relied on. It must be writable by the image's user, `node` (uid 1000). Snapshot it for backups.
130
+ - **DNS.** Each hosted domain points at the origin: `_simon.<domain> TXT "v=simon1; server=<origin>"`.
131
+
132
+ ## Two laptops over Tailscale
133
+
134
+ Two people, each running a basic server on their own laptop, can federate over a
135
+ [Tailscale](https://tailscale.com) tailnet with no public DNS and no open ports. Each server hosts
136
+ addresses on its laptop's MagicDNS name, for example `ada-laptop.tail1234.ts.net`, and serves https on
137
+ port 443 with the certificate `tailscale cert` issues for that name. Each domain resolves by the
138
+ well-known answer its own server gives, because no TXT record can be added under `ts.net`.
139
+
140
+ In the tailnet's admin console, turn on MagicDNS and HTTPS certificates. Then, on each laptop, from
141
+ this repository:
142
+
143
+ ```sh
144
+ NAME=$(tailscale status --json | jq -r '.Self.DNSName | rtrimstr(".")') # ada-laptop.tail1234.ts.net
145
+ tailscale cert "$NAME" # writes $NAME.crt and $NAME.key here
146
+ [ -f simon-keys.env ] || (umask 077; scripts/server-keys.sh > simon-keys.env) # once: the server key
147
+ . ./simon-keys.env
148
+ SIMON_ORIGIN="https://$NAME" SIMON_PORT=443 SIMON_HOSTED_DOMAINS="$NAME" \
149
+ SIMON_TLS_CERT_FILE="$NAME.crt" SIMON_TLS_KEY_FILE="$NAME.key" \
150
+ SIMON_TRUSTED_NETWORKS=100.64.0.0/10,fd7a:115c:a1e0::/48 SIMON_DB=simon.sqlite \
151
+ node server/src/main.ts
152
+ ```
153
+
154
+ - **The port.** Resolution fetches `https://<domain>/.well-known/simon`, so the server listens on 443. On
155
+ Linux a process needs permission for that, for example `sudo sysctl net.ipv4.ip_unprivileged_port_start=443`.
156
+ - **The networks.** `100.64.0.0/10` and `fd7a:115c:a1e0::/48` are the IPv4 and IPv6 ranges Tailscale gives
157
+ its devices. A MagicDNS name has addresses in both, and the guard refuses a host if any of its
158
+ addresses is outside the trusted networks.
159
+ - **Name lookups.** The server asks the system's DNS servers through c-ares, not the operating system's
160
+ resolver library, so the other laptop's name must resolve that way. This prints its tailnet addresses
161
+ when it does: `node -e 'new (require("node:dns").promises.Resolver)().resolve4(process.argv[1]).then(console.log)' other-laptop.tail1234.ts.net`.
162
+ - **If the name does not resolve that way,** add `SIMON_DNS_SERVERS=100.100.100.100`, Tailscale's own resolver.
163
+ - **The key.** Keep `simon-keys.env` (mode 600) and reuse it: a server whose key changes wholesale is held
164
+ by its peers for `SIMON_KEY_CHANGE_HOLD` seconds (spec section 11.1).
165
+
166
+ Agents then register on the laptop's own name, for example
167
+ `simon register --server "https://$NAME" --address "ada@$NAME"`, and write to agents on the other
168
+ laptop's name.
169
+
170
+ Two laptops with no tailnet in common can federate over public Cloudflare quick tunnels instead, with the
171
+ server behind the tunnel (`SIMON_LISTEN_HOST=127.0.0.1`, `SIMON_CLIENT_IP_HEADER=cf-connecting-ip`):
172
+ [examples/two-laptops](../examples/two-laptops/README.md) does it in one script.
173
+
174
+ ## Layout
175
+
176
+ | File | Responsibility |
177
+ | --- | --- |
178
+ | `server.ts` | `createServer`: assembly, routing, the request pipeline |
179
+ | `http.ts` | Request/Reply, router, the `node:http` adapter with body caps |
180
+ | `deliver.ts` | `POST /simon/deliver`, spec 6.3 in order |
181
+ | `outbox.ts` | Delivery to other servers: claimed rows, caps on POSTs, backoff to the deadline, dead and throttled destinations |
182
+ | `fanout.ts` | Arrivals offered to recipients here through the admit chain, each on its own; inbox events told |
183
+ | `parts.ts` | Uploads, the per-source reading rules, and part fetches from carriers (prefetched, bounded) |
184
+ | `register.ts`, `account.ts` | The agent's address and keys: register and recovery; the account, keys, cancelling a recovery, unpairing |
185
+ | `send.ts`, `sync.ts`, `public.ts` | Sending, retained sync and public reads |
186
+ | `signal.ts`, `webhooks.ts` | Client signals and verified webhook wakeups |
187
+ | `notice.ts` | Messages from the server itself to an agent here, as `postmaster@<its domain>` |
188
+ | `resolver.ts`, `continuity.ts` | Resolution and documents, bounded and cached; key continuity by possession |
189
+ | `auth.ts` | Signed requests, nonces recorded after verification |
190
+ | `net/` | c-ares DNS with DoH confirmation, the SSRF-guarded client, IP rules |
191
+ | `store/` | The `Store` interface, sqlite, and the contract suite |
192
+ | `extensions.ts`, `config.ts`, `rate.ts`, `worker.ts`, `main.ts` | The seam, settings, limits, the loop, the process |
193
+ | `context.ts`, `errors.ts` | What the parts share; the protocol's problem-detail errors |
194
+ | `testing.ts` | A fake clock, DNS and network, for tests here and in applications |
195
+
196
+ ## Extensions
197
+
198
+ An application adds behaviour by passing extensions; it never edits the core. Every hook is the list of all extensions' hooks, in order.
199
+
200
+ | Hook | Called | Returns |
201
+ | --- | --- | --- |
202
+ | `routes` | Requests to its paths (core paths cannot be replaced) | a `Reply` |
203
+ | `admit(arrival, api)` | Per recipient here, after the 202 | `'accept'` or `'refuse'` or `'later'` ends the chain; `undefined` passes. Default: accept |
204
+ | `claimsFor(message, agent, api)` | On `send` | claims for the envelope |
205
+ | `allowAddress(request, api)` | `register` for a new hosted name or lease, before any pairing | `'allow'` or `'refuse'` (`409 address_taken`) ends the chain; `undefined` passes. Default: allow |
206
+ | `verifyPrincipal(request, api)` | `register` with a `mailto:`/`tel:` `for` | `{status:'verified'}`, `{status:'pending', body}`, or `undefined` |
207
+ | `upload(upload, api)` | `PUT /simon/parts/<sha256>`, before the bytes are kept | nothing; a thrown WireError refuses the upload |
208
+ | `on.stored / inbox / delivered / deliveryFailed / recovered` | After the event (`inbox` at least once) | nothing; failures are logged |
209
+ | `on.unroutable` | Recipients whose domain names no SIMON server | `true` when it took them: they count as delivered, and later listeners are not told |
210
+
211
+ `api` (`ServerApi`) offers `config`, `store`, `now()`, `log`, `authenticate(req)`, `admit(address, id)` (release a `'later'`), `offer(address, id)` (a stored message to an agent here, through the admit chain), `send(message, hints?)` (as an agent here, checked as `send` checks it) and `retire(address)`.
212
+
213
+ ```ts
214
+ import { createServer, json, loadConfig, SqliteStore, type Extension } from '@agnt-sdk/simon-server';
215
+
216
+ const held = new Map<string, string>(); // message id -> recipient
217
+ const quiet: Extension = {
218
+ name: 'quiet-hours',
219
+ admit: ({ id, recipient }) => {
220
+ const hour = new Date().getUTCHours();
221
+ if (hour >= 6) return undefined; // no opinion: the next link decides
222
+ held.set(id, recipient);
223
+ return 'later';
224
+ },
225
+ routes: [
226
+ {
227
+ method: 'POST',
228
+ path: '/app/release',
229
+ handle: async (req, api) => {
230
+ const agent = await api.authenticate(req);
231
+ for (const [id, to] of held) if (to === agent.address && (await api.admit(to, id))) held.delete(id);
232
+ return json({ released: true });
233
+ },
234
+ },
235
+ ],
236
+ };
237
+ const config = loadConfig();
238
+ createServer({ config, store: new SqliteStore(config.database), extensions: [quiet] }).listen();
239
+ ```
240
+
241
+ ## Storage
242
+
243
+ The ordinary SQLite Store refuses legacy standalone schema2. The separate
244
+ [offline candidate conversion tool](SQLITE-V2-MIGRATION.md) preserves a WAL-aware core/app-outbox
245
+ cohort and validates a new current-schema copy under explicit maintenance prerequisites. Its output stays
246
+ blocked for activation, with no operational promotion/reset override; passing source, CLI and synthetic
247
+ tests says nothing about readiness of real data, hosts or runtimes.
248
+
249
+ A retired agent-document lookup answers the same 404 as an unknown address by default (`'private'`).
250
+ Publishing signed retirement evidence is an explicit opt-in: `SIMON_RETIRED_AGENT_DOCUMENTS=public` in
251
+ the environment (or `retiredAgentDocuments: 'public'` in `defineConfig`), or `retiredAgentDocuments: 'public'`
252
+ passed to `createServer`, which overrides the configuration. The setting is validated and captured at
253
+ construction, and applies to both `handle` and the HTTP listener. Invalid values fail before workers
254
+ start. It does not remove internal retired records or change retired-key authentication, unpairing
255
+ retries, or previously published signed messages.
256
+
257
+ `store/store.ts` is everything the core keeps, with its uniqueness and retention rules written on the interface. Another store proves itself with the contract suite:
258
+
259
+ ```ts
260
+ import { storeContract } from '@agnt-sdk/simon-server/contract';
261
+ storeContract('mongo', async () => new MongoStore(await freshDb()).init());
262
+ ```
263
+
264
+ [`@agnt-sdk/simon-store-mongo`](../store-mongo/README.md) is the Store on MongoDB and Amazon DocumentDB. Before running several processes on one store, read "Running several copies" in [docs/REFERENCE-SERVER.md](../docs/REFERENCE-SERVER.md): elect one of them to run the worker loop.
265
+
266
+ ## Testing
267
+
268
+ `@agnt-sdk/simon-server/testing` is the fake outside world the tests use: a movable `Clock`, `FakeDns`, and `FakeNet`, whose origins answer in-process. An application runs whole federations of its own servers on it.
269
+
270
+ ```sh
271
+ node --test server/test # spec rules, federation on a fake network, hardening, the store contract
272
+ node server/scripts/smoke.ts # two real server processes, a message each way over localhost
273
+ ```
274
+
275
+ To run the conformance suite against it, start the server with its peer simulator's domain resolved
276
+ without DNS (`SIMON_DEV_RESOLVE` is honoured only with `SIMON_DEV=1`, and refused at startup otherwise) and
277
+ rate limits loose enough for one run:
278
+
279
+ ```sh
280
+ SIMON_DEV=1 SIMON_ORIGIN=http://127.0.0.1:8080 SIMON_PORT=8080 SIMON_HOSTED_DOMAINS=sut.test \
281
+ SIMON_LEASE_DOMAIN=lease.sut.test SIMON_DB=:memory: \
282
+ SIMON_RATES=register=1000/3600,badSignature=100000/3600,code=100/3600,codeClient=1000/3600,deliver=100000/3600 \
283
+ SIMON_DEV_RESOLVE=sut.test=http://127.0.0.1:8080,lease.sut.test=http://127.0.0.1:8080,peer.test=http://127.0.0.1:47100 \
284
+ node server/src/main.ts
285
+ ```
286
+
287
+ and, in another terminal, `node conformance/src/cli.ts --server http://127.0.0.1:8080 --hosted-domain sut.test --format text`.
288
+ [conformance/README.md](../conformance/README.md) explains every option, including the code-flow checks,
289
+ which read the register codes this server logs in development. How the server runs recovery and keys, and
290
+ its other choices, are in [docs/REFERENCE-SERVER.md](../docs/REFERENCE-SERVER.md).
291
+
292
+ ### Client lifecycle and wakeups
293
+
294
+ Registration creates or pairs an address at `POST /simon/register`. Recovery uses `POST /simon/recovery`;
295
+ `GET /simon/recovery` reports pending state to an agent key. The old registration call does not recover a
296
+ held address. A pending mailbox proof can complete for seven days after its `completesAt`, including
297
+ legacy proofs that lack a nonce; it never grants old keys new cancellation authority.
298
+
299
+ `GET /simon/sync` includes received messages, newly accepted sends and transient signals. Its encrypted
300
+ account-bound tokens survive worker restarts through the shared Store secret. Unknown, tampered,
301
+ cross-account and expired tokens fail with `410 token_expired`. Schema 8 preserves historical received
302
+ positions; old stores have no trustworthy discriminator for historic local sends, so each affected device receives an explicit `410 token_expired` with `details.reason=history_migration`
303
+ and an account-bound `details.resumeToken`. Passing that token explicitly reads preserved received
304
+ history and newly tracked sends; starting without a token continues to report the old sent-history gap.
305
+ This never invents old sent history or acknowledges a gap for other devices.
306
+
307
+ `POST /simon/send-signal` carries registry signal types. Webhook creation, verification and deletion use
308
+ `/simon/webhooks`: at most five HTTPS URLs per account, verification before wakeups, signed posts,
309
+ one-second quiet time and lease-fenced retries. An unverified URL whose first verification post fails
310
+ must be deleted and recreated by its owner; it sends no changed notifications. Three days of failures
311
+ switch off a verified webhook.
312
+
313
+ Retraction admission is bounded by host policy: 4096 accepted IDs globally, 256 per author, and 4 MiB
314
+ of canonical receipt/journal state by default. Configure `retractions` on the SQLite/Mongo Store
315
+ constructor with `maxRecords`, `maxPerAuthor`, and `maxBytes` (at most 8 MiB). An exact repeat uses
316
+ its original receipt. Accepted suppression is never evicted to admit another entry, so a full store
317
+ returns a generic `503 unavailable` before acceptance. This conservative permanent allocation can
318
+ exhaust; it is an overload hold rather than permission to relist retracted content.
@@ -0,0 +1,59 @@
1
+ import { type Json } from '@agnt-sdk/simon-protocol';
2
+ import { type Context } from './context.ts';
3
+ import { type Reply, type Request } from './http.ts';
4
+ import type { Agent } from './store/store.ts';
5
+ /** The value scope of an address's pending recovery. */
6
+ export declare const RECOVERY = "recovery";
7
+ /**
8
+ * The value scope of a cancel code, by the address and the code's hash: the nonce of the recovery it
9
+ * cancels. A recovery may have several, one for each time the address was told of it.
10
+ */
11
+ export declare const CANCEL_CODE = "recovery-cancel-code";
12
+ /** A recovery by the address's mailbox, waiting until `completesAt` (register.ts). */
13
+ export interface Recovery {
14
+ /** The key that proved the code, which repeats the call to complete it, and that code's hash and nonce. */
15
+ key: string;
16
+ hash: string;
17
+ nonce?: string;
18
+ startedAt?: string;
19
+ completesAt: string;
20
+ /** Whether a key may cancel it: not when it started within 30 days after a recovery a key cancelled. */
21
+ byKey?: boolean;
22
+ cancelHash?: string;
23
+ keyCancelBlocked?: true;
24
+ /** Private preparation gate: explicit false cannot replace keys even if rollback is unavailable. */
25
+ defended?: boolean;
26
+ }
27
+ export type Body<Member extends string> = Partial<Record<Member, string>> & Record<string, Json | undefined>;
28
+ /**
29
+ * A body of these calls: a JSON object whose members named in `members` are strings when present. Other
30
+ * members are ignored, unless an extension defines them.
31
+ */
32
+ export declare function readBody<Member extends string>(bytes: Buffer, members: readonly Member[]): Body<Member>;
33
+ export declare const sha256: (s: string) => string;
34
+ /** Whether `code` is the one whose SHA-256 is `hash`, compared in constant time. */
35
+ export declare const matches: (code: string, hash: string) => boolean;
36
+ /** Whether a key may cancel a recovery starting now: not within 30 days after a key cancelled one. */
37
+ export declare const keysMayCancel: (ctx: Context, address: string) => Promise<boolean>;
38
+ /** Evaluate the exact final signed projection inside the Store's atomic key admission. */
39
+ export declare function canPublishAgent(ctx: Context, agent: Agent): boolean;
40
+ /** The account as it stands, for the key that asked: `cancellable` says whether it may cancel the recovery. */
41
+ export declare function account(ctx: Context, agent: Agent, signer: string): Promise<Reply>;
42
+ export declare function getAccount(ctx: Context, req: Request): Promise<Reply>;
43
+ /** A device's key added with its own proof, so no agent lists a key it does not hold. */
44
+ export declare function addKey(ctx: Context, req: Request): Promise<Reply>;
45
+ /**
46
+ * A key revoked at once. Keys rank by age: a key may revoke itself and any key added after it, never one
47
+ * added before it, so a thief who copies a newer device's key cannot evict the older ones.
48
+ */
49
+ export declare function revokeKey(ctx: Context, req: Request): Promise<Reply>;
50
+ export declare function endGrant(ctx: Context, req: Request): Promise<Reply>;
51
+ /** A pending recovery cancelled by a key of the agent (`{}`), or by its cancel code (`{"address","cancelCode"}`). */
52
+ export declare function cancelRecovery(ctx: Context, req: Request): Promise<Reply>;
53
+ /**
54
+ * A key principal ends its pairing: the address retires at once, its keys revoked, whatever they say. The
55
+ * unpairing is the authority, so any key may sign the request; a repeat answers as the first call did.
56
+ */
57
+ export declare function unpair(ctx: Context, req: Request): Promise<Reply>;
58
+ /** Recovery status is reserved to a currently authorized agent key. */
59
+ export declare function getRecovery(ctx: Context, req: Request): Promise<Reply>;