@noy-db/in-relay 0.8.0-pre.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/LICENSE +202 -0
- package/NOTICE +1 -0
- package/README.md +64 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 vLannaAi
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
noy-db — Copyright 2026 vLannaAi
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @noy-db/in-relay
|
|
2
|
+
|
|
3
|
+
> The relay server half for [noy-db](https://github.com/noy-db/core) — a frame dispatcher over a **narrowed** store profile.
|
|
4
|
+
|
|
5
|
+
A relay hosts vaults it cannot read and must not be able to overwrite. This package serves the granular store methods and **structurally omits two**:
|
|
6
|
+
|
|
7
|
+
| omitted | why |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `saveAll` | whole-vault replace — a rollback superweapon aimed at the hosts trusting the relay |
|
|
10
|
+
| `listVaults` | enumeration is an existence leak; a relay serves vaults the caller already names |
|
|
11
|
+
|
|
12
|
+
## Why a narrowed TYPE rather than a runtime allowlist
|
|
13
|
+
|
|
14
|
+
`@noy-db/in-rest` types its `store` as a full `NoydbStore` and gates dispatch with an `allow` set. That is right for `in-rest`, whose job is to serve a whole store. Building a relay that way means handing the handler an object that **carries** `saveAll` and trusting a `Set` not to call it — structural absence downgraded to a runtime allowlist.
|
|
15
|
+
|
|
16
|
+
Here the store is a `NoydbRelayStore` (`@noy-db/hub/to`), so the excluded members are not on the type and no frame can reach them.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createRelayHandler } from '@noy-db/in-relay'
|
|
20
|
+
import type { NoydbRelayStore } from '@noy-db/hub/to'
|
|
21
|
+
|
|
22
|
+
const handle = createRelayHandler({ store: myStore satisfies NoydbRelayStore })
|
|
23
|
+
const result = await handle({ id: '1', method: 'get', args: ['vault', 'invoices', 'inv-1'] })
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Passing an ordinary full store is fine — it satisfies the narrowed type structurally, and the handler still cannot call what it does not name.
|
|
27
|
+
|
|
28
|
+
## Unknown, not forbidden
|
|
29
|
+
|
|
30
|
+
An excluded method is refused as **unknown** (`400`), never as forbidden (`403`). A 403 would confirm the method exists and is merely disallowed here, naming the excluded surface to anyone probing. An excluded name and a name that never existed are indistinguishable in the response.
|
|
31
|
+
|
|
32
|
+
## Transport-neutral
|
|
33
|
+
|
|
34
|
+
`createRelayHandler` takes a decoded frame and returns a decoded result, so it can be served over HTTP, a WebSocket, or QUIC without this package knowing which.
|
|
35
|
+
|
|
36
|
+
## The notify frame — server push, beside the request frame
|
|
37
|
+
|
|
38
|
+
Every transport in this family is client-pull; a hosted store that knows a record changed had no way to say so. The relay's one server-initiated sentence is a **notify frame**, and it is deliberately small:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createRelayHandler, createRelayNotifier } from '@noy-db/in-relay'
|
|
42
|
+
import type { NoydbRelayStore, EncryptedEnvelope } from '@noy-db/hub/to'
|
|
43
|
+
|
|
44
|
+
const notify = createRelayNotifier()
|
|
45
|
+
const handle = createRelayHandler({ store: myStore satisfies NoydbRelayStore, notify })
|
|
46
|
+
|
|
47
|
+
// Your transport, when an AUTHENTICATED session asks to watch one vault:
|
|
48
|
+
const off = notify.subscribe('vault', (frame) => session.send(JSON.stringify(frame)))
|
|
49
|
+
// frame: { t: 'notify', seq: 1, vault: 'vault', collection: 'invoices', id: 'inv-1', op: 'put', ts: '…' }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| question | answer |
|
|
53
|
+
|---|---|
|
|
54
|
+
| a third encoding of the store contract? | no — a notification is not a method call; the wire carries requests (`method`), results (`ok`) and this (`t: 'notify'`) |
|
|
55
|
+
| what is `seq`? | a **per-subscription** sequence, contiguous from 1 — a gap is detectable; a late joiner starts at 1 |
|
|
56
|
+
| what does it carry? | the **address** (`vault/collection/id`), the op, and the envelope's `_ts` (relay clock for a delete). **Never the envelope** — a push is not a write path |
|
|
57
|
+
| delivery guarantee? | at-most-once, in order per subscription. On a gap, reconcile with `listSince(vault, collection, lastTs)` — the frame makes polling unnecessary; it is not the source of truth |
|
|
58
|
+
| auth? | a subscription names **one vault** (no enumeration by the back door); binding it to an authenticated session is the transport's job |
|
|
59
|
+
|
|
60
|
+
A failed mutation publishes nothing; a `tx` publishes one frame per op only after the whole batch committed; a subscriber that throws is skipped for that frame and never breaks the write.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT © vLannaAi
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { NoydbRelayStore } from '@noy-db/hub/to';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* **@noy-db/in-relay** — the relay server half (#1237).
|
|
5
|
+
*
|
|
6
|
+
* A frame dispatcher over a store whose profile EXCLUDES whole-vault replace
|
|
7
|
+
* and vault enumeration. The exclusions are structural, not configured.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is not `in-rest` with a narrower `allow` set
|
|
10
|
+
*
|
|
11
|
+
* `in-rest` types its `store` as a full `NoydbStore` and gates dispatch with a
|
|
12
|
+
* runtime `Set`. That is correct for `in-rest`, whose job is to serve a whole
|
|
13
|
+
* store. Building a relay that way means handing the handler an object that
|
|
14
|
+
* CARRIES `saveAll` and trusting the `Set` not to call it — structural absence
|
|
15
|
+
* downgraded to a runtime allowlist, which is the one property a relay exists
|
|
16
|
+
* to have. Here the store is a {@link NoydbRelayStore}, so the excluded members
|
|
17
|
+
* cannot be called because they are not on the type.
|
|
18
|
+
*
|
|
19
|
+
* ## Two exclusions, two different reasons
|
|
20
|
+
*
|
|
21
|
+
* - `saveAll` — whole-vault replace. A relay that can be asked to overwrite a
|
|
22
|
+
* vault wholesale is a rollback superweapon aimed at the hosts trusting it.
|
|
23
|
+
* - `listVaults` — enumeration is an existence leak. A relay serves vaults the
|
|
24
|
+
* caller already names; it never answers "what else is here".
|
|
25
|
+
*
|
|
26
|
+
* ## Unknown-method, not forbidden-method
|
|
27
|
+
*
|
|
28
|
+
* An excluded method is refused as **unknown** (400), never as forbidden (403).
|
|
29
|
+
* A 403 would confirm the method exists and is merely disallowed here, which
|
|
30
|
+
* names the excluded surface to anyone probing. Matching `@doi-db/daemon`,
|
|
31
|
+
* which reached the same conclusion independently in its native implementation.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The relay's method vocabulary.
|
|
38
|
+
*
|
|
39
|
+
* `saveAll` and `listVaults` are absent because they are absent from
|
|
40
|
+
* {@link NoydbRelayStore} — adding either here would not compile a call.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ That is NOT sufficient on its own, and the first version of this comment
|
|
43
|
+
* claimed it was: it said the list "cannot drift away from the type it
|
|
44
|
+
* dispatches to", and it had already drifted by FOUR members
|
|
45
|
+
* (`listPage`, `getStoreTime`, `presencePublish`, `presenceSubscribe`).
|
|
46
|
+
* Omission is silent in the direction the type cannot catch — a member present
|
|
47
|
+
* on the type and missing here is simply unreachable, and `listPage` being
|
|
48
|
+
* unreachable means clients fall back to `loadAll`, which is the regression
|
|
49
|
+
* pagination exists to prevent.
|
|
50
|
+
*
|
|
51
|
+
* So the invariant is now ENFORCED rather than asserted: {@link NOT_RELAYED}
|
|
52
|
+
* names every deliberate exclusion, and a compile-time check below fails if
|
|
53
|
+
* some method of {@link NoydbRelayStore} appears in neither list. Adding a
|
|
54
|
+
* method to the store contract forces a decision here instead of a silent drop.
|
|
55
|
+
*/
|
|
56
|
+
declare const RELAY_METHODS: readonly ["get", "put", "delete", "list", "loadAll", "ping", "listSince", "listPage", "getStoreTime", "presencePublish", "estimateUsage", "tx"];
|
|
57
|
+
type RelayMethod = (typeof RELAY_METHODS)[number];
|
|
58
|
+
/**
|
|
59
|
+
* Members of {@link NoydbRelayStore} deliberately NOT dispatched, with reasons.
|
|
60
|
+
*
|
|
61
|
+
* - `presenceSubscribe` — returns an unsubscribe FUNCTION. A frame carries
|
|
62
|
+
* JSON, so this is not merely unimplemented, it is unrepresentable in a
|
|
63
|
+
* request/response shape. Server-initiated delivery is what the notify frame
|
|
64
|
+
* is for ({@link RelayNotifyFrame}, #1238), not a return value.
|
|
65
|
+
* - `presignUrl` — hands the caller a time-limited URL that fetches the
|
|
66
|
+
* envelope **directly from the backing object store**, around this relay.
|
|
67
|
+
* That defeats the reason a relay exists: it is the mediating point, so a
|
|
68
|
+
* presigned URL survives revocation, escapes metering, and is unobservable
|
|
69
|
+
* here. Excluded on the security argument, not on serialisability — it would
|
|
70
|
+
* marshal perfectly, which is what makes it worth stating.
|
|
71
|
+
*/
|
|
72
|
+
declare const NOT_RELAYED: readonly ["presenceSubscribe", "presignUrl"];
|
|
73
|
+
/** One request frame. Identical shape to `in-rest`'s, deliberately — see the module docs. */
|
|
74
|
+
interface RelayFrame {
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly method: string;
|
|
77
|
+
readonly args: readonly unknown[];
|
|
78
|
+
}
|
|
79
|
+
/** A frame result: either a value, or an error with a status a transport can map. */
|
|
80
|
+
type RelayResult = {
|
|
81
|
+
readonly ok: true;
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly value: unknown;
|
|
84
|
+
} | {
|
|
85
|
+
readonly ok: false;
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly status: 400 | 500 | 501;
|
|
88
|
+
readonly error: {
|
|
89
|
+
readonly name: string;
|
|
90
|
+
readonly message: string;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
interface RelayHandlerOptions {
|
|
94
|
+
/**
|
|
95
|
+
* The store this relay serves. Typed as the NARROWED profile: a store may
|
|
96
|
+
* carry `saveAll`, but this handler structurally cannot reach it.
|
|
97
|
+
*/
|
|
98
|
+
readonly store: NoydbRelayStore;
|
|
99
|
+
/**
|
|
100
|
+
* Where successful mutations are announced (#1238). Omit it and the handler
|
|
101
|
+
* behaves exactly as before — pull-only.
|
|
102
|
+
*/
|
|
103
|
+
readonly notify?: RelayNotifier;
|
|
104
|
+
}
|
|
105
|
+
/** Server-push frame: one successful mutation, by address. See the block above. */
|
|
106
|
+
interface RelayNotifyFrame {
|
|
107
|
+
readonly t: 'notify';
|
|
108
|
+
/** Per-subscription, contiguous from 1. A gap means frames were missed: reconcile with `listSince`. */
|
|
109
|
+
readonly seq: number;
|
|
110
|
+
readonly vault: string;
|
|
111
|
+
readonly collection: string;
|
|
112
|
+
readonly id: string;
|
|
113
|
+
readonly op: 'put' | 'delete';
|
|
114
|
+
/** The envelope's `_ts` for a put; the relay's clock for a delete. Feed it to `listSince`. */
|
|
115
|
+
readonly ts: string;
|
|
116
|
+
}
|
|
117
|
+
/** A change as the handler reports it, before any subscriber's `seq` is stamped. */
|
|
118
|
+
type RelayChange = Omit<RelayNotifyFrame, 't' | 'seq'>;
|
|
119
|
+
interface RelayNotifier {
|
|
120
|
+
/**
|
|
121
|
+
* Deliver every subsequent change in `vault` to `deliver`, each stamped with
|
|
122
|
+
* this subscription's own `seq`. Returns an unsubscribe. Late joiners start
|
|
123
|
+
* at 1 — the sequence is the subscription's, not the server's history.
|
|
124
|
+
*/
|
|
125
|
+
subscribe(vault: string, deliver: (frame: RelayNotifyFrame) => void): () => void;
|
|
126
|
+
/** Called by the handler after a mutation LANDED. Not for transports to call. */
|
|
127
|
+
publish(change: RelayChange): void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The in-process fan-out. One per relay; a transport calls `subscribe` when an
|
|
131
|
+
* authenticated session asks to watch a vault and forwards each frame on that
|
|
132
|
+
* session's wire.
|
|
133
|
+
*/
|
|
134
|
+
declare function createRelayNotifier(): RelayNotifier;
|
|
135
|
+
/** Thrown for a method this relay does not implement — including excluded ones. */
|
|
136
|
+
declare class UnknownRelayMethodError extends Error {
|
|
137
|
+
constructor(method: string);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Thrown when the relay KNOWS a method but the backing store does not
|
|
141
|
+
* implement it (the optional members of the store contract).
|
|
142
|
+
*
|
|
143
|
+
* ⚠️ Deliberately distinct from {@link UnknownRelayMethodError}, and the first
|
|
144
|
+
* version of this package collapsed them: an absent optional method threw
|
|
145
|
+
* `UnknownRelayMethodError` from inside dispatch, which the catch-all reported
|
|
146
|
+
* as **500**. That misattributes a store CAPABILITY GAP as a server fault —
|
|
147
|
+
* two states warranting opposite responses (the client should degrade
|
|
148
|
+
* gracefully vs. the operator should investigate) rendered identically.
|
|
149
|
+
*
|
|
150
|
+
* `501` matches `@noy-db/in-rest`, which maps its own `UnsupportedMethodError`
|
|
151
|
+
* to `501 NotImplemented`, and `@doi-db/daemon`, which reached 501
|
|
152
|
+
* independently. It leaks nothing about the relay's EXCLUSIONS: those are
|
|
153
|
+
* refused as 400-unknown before dispatch is reached.
|
|
154
|
+
*/
|
|
155
|
+
declare class UnsupportedRelayMethodError extends Error {
|
|
156
|
+
constructor(method: string);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build a frame handler over a narrowed store.
|
|
160
|
+
*
|
|
161
|
+
* Transport-neutral by design: it takes a decoded frame and returns a decoded
|
|
162
|
+
* result, so a caller may serve it over HTTP, a WebSocket, or QUIC without this
|
|
163
|
+
* package knowing which.
|
|
164
|
+
*/
|
|
165
|
+
declare function createRelayHandler(options: RelayHandlerOptions): (frame: RelayFrame) => Promise<RelayResult>;
|
|
166
|
+
|
|
167
|
+
export { NOT_RELAYED, RELAY_METHODS, type RelayChange, type RelayFrame, type RelayHandlerOptions, type RelayMethod, type RelayNotifier, type RelayNotifyFrame, type RelayResult, UnknownRelayMethodError, UnsupportedRelayMethodError, createRelayHandler, createRelayNotifier };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var RELAY_METHODS = [
|
|
3
|
+
"get",
|
|
4
|
+
"put",
|
|
5
|
+
"delete",
|
|
6
|
+
"list",
|
|
7
|
+
"loadAll",
|
|
8
|
+
"ping",
|
|
9
|
+
"listSince",
|
|
10
|
+
"listPage",
|
|
11
|
+
"getStoreTime",
|
|
12
|
+
"presencePublish",
|
|
13
|
+
"estimateUsage",
|
|
14
|
+
"tx"
|
|
15
|
+
];
|
|
16
|
+
var NOT_RELAYED = ["presenceSubscribe", "presignUrl"];
|
|
17
|
+
function createRelayNotifier() {
|
|
18
|
+
const byVault = /* @__PURE__ */ new Map();
|
|
19
|
+
return {
|
|
20
|
+
subscribe(vault, deliver) {
|
|
21
|
+
const sub = { deliver, seq: 0 };
|
|
22
|
+
let subs = byVault.get(vault);
|
|
23
|
+
if (!subs) byVault.set(vault, subs = /* @__PURE__ */ new Set());
|
|
24
|
+
subs.add(sub);
|
|
25
|
+
return () => {
|
|
26
|
+
subs.delete(sub);
|
|
27
|
+
if (subs.size === 0) byVault.delete(vault);
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
publish(change) {
|
|
31
|
+
const subs = byVault.get(change.vault);
|
|
32
|
+
if (!subs) return;
|
|
33
|
+
for (const sub of [...subs]) {
|
|
34
|
+
const frame = { t: "notify", seq: ++sub.seq, ...change };
|
|
35
|
+
try {
|
|
36
|
+
sub.deliver(frame);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
var UnknownRelayMethodError = class extends Error {
|
|
44
|
+
constructor(method) {
|
|
45
|
+
super(`Unknown method: ${method}`);
|
|
46
|
+
this.name = "UnknownRelayMethodError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var UnsupportedRelayMethodError = class extends Error {
|
|
50
|
+
constructor(method) {
|
|
51
|
+
super(`Method not supported by this store: ${method}`);
|
|
52
|
+
this.name = "UnsupportedRelayMethodError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function isRelayMethod(m) {
|
|
56
|
+
return RELAY_METHODS.includes(m);
|
|
57
|
+
}
|
|
58
|
+
async function dispatch(store, method, args) {
|
|
59
|
+
switch (method) {
|
|
60
|
+
case "get": {
|
|
61
|
+
const [vault, collection, id] = args;
|
|
62
|
+
return store.get(vault, collection, id);
|
|
63
|
+
}
|
|
64
|
+
case "put": {
|
|
65
|
+
const [vault, collection, id, envelope, expectedVersion] = args;
|
|
66
|
+
return store.put(vault, collection, id, envelope, expectedVersion);
|
|
67
|
+
}
|
|
68
|
+
case "delete": {
|
|
69
|
+
const [vault, collection, id] = args;
|
|
70
|
+
return store.delete(vault, collection, id);
|
|
71
|
+
}
|
|
72
|
+
case "list": {
|
|
73
|
+
const [vault, collection] = args;
|
|
74
|
+
return store.list(vault, collection);
|
|
75
|
+
}
|
|
76
|
+
case "loadAll": {
|
|
77
|
+
const [vault] = args;
|
|
78
|
+
return store.loadAll(vault);
|
|
79
|
+
}
|
|
80
|
+
case "ping": {
|
|
81
|
+
return store.ping?.() ?? true;
|
|
82
|
+
}
|
|
83
|
+
case "listSince": {
|
|
84
|
+
const [vault, collection, since] = args;
|
|
85
|
+
if (store.listSince === void 0) throw new UnsupportedRelayMethodError("listSince");
|
|
86
|
+
return store.listSince(vault, collection, since);
|
|
87
|
+
}
|
|
88
|
+
case "listPage": {
|
|
89
|
+
const [vault, collection, cursor, limit] = args;
|
|
90
|
+
if (store.listPage === void 0) throw new UnsupportedRelayMethodError("listPage");
|
|
91
|
+
return store.listPage(vault, collection, cursor, limit);
|
|
92
|
+
}
|
|
93
|
+
case "getStoreTime": {
|
|
94
|
+
if (store.getStoreTime === void 0) throw new UnsupportedRelayMethodError("getStoreTime");
|
|
95
|
+
return store.getStoreTime();
|
|
96
|
+
}
|
|
97
|
+
case "presencePublish": {
|
|
98
|
+
const [channel, payload] = args;
|
|
99
|
+
if (store.presencePublish === void 0) throw new UnsupportedRelayMethodError("presencePublish");
|
|
100
|
+
return store.presencePublish(channel, payload);
|
|
101
|
+
}
|
|
102
|
+
case "estimateUsage": {
|
|
103
|
+
if (store.estimateUsage === void 0) throw new UnsupportedRelayMethodError("estimateUsage");
|
|
104
|
+
return store.estimateUsage();
|
|
105
|
+
}
|
|
106
|
+
case "tx": {
|
|
107
|
+
const [ops] = args;
|
|
108
|
+
if (store.tx === void 0) throw new UnsupportedRelayMethodError("tx");
|
|
109
|
+
return store.tx(ops);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function changesOf(method, args) {
|
|
114
|
+
switch (method) {
|
|
115
|
+
case "put": {
|
|
116
|
+
const [vault, collection, id, envelope] = args;
|
|
117
|
+
return [{ vault, collection, id, op: "put", ts: envelope._ts }];
|
|
118
|
+
}
|
|
119
|
+
case "delete": {
|
|
120
|
+
const [vault, collection, id] = args;
|
|
121
|
+
return [{ vault, collection, id, op: "delete", ts: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
122
|
+
}
|
|
123
|
+
case "tx": {
|
|
124
|
+
const [ops] = args;
|
|
125
|
+
return ops.map((op) => op.type === "put" ? { vault: op.vault, collection: op.collection, id: op.id, op: "put", ts: op.envelope._ts } : { vault: op.vault, collection: op.collection, id: op.id, op: "delete", ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function createRelayHandler(options) {
|
|
132
|
+
const { store, notify } = options;
|
|
133
|
+
return async (frame) => {
|
|
134
|
+
if (!isRelayMethod(frame.method)) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
id: frame.id,
|
|
138
|
+
status: 400,
|
|
139
|
+
error: { name: "UnknownRelayMethodError", message: `Unknown method: ${frame.method}` }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const value = await dispatch(store, frame.method, frame.args);
|
|
144
|
+
if (notify) for (const change of changesOf(frame.method, frame.args)) notify.publish(change);
|
|
145
|
+
return { ok: true, id: frame.id, value };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const e = err;
|
|
148
|
+
const status = e instanceof UnsupportedRelayMethodError ? 501 : 500;
|
|
149
|
+
return { ok: false, id: frame.id, status, error: { name: e.name, message: e.message } };
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
NOT_RELAYED,
|
|
155
|
+
RELAY_METHODS,
|
|
156
|
+
UnknownRelayMethodError,
|
|
157
|
+
UnsupportedRelayMethodError,
|
|
158
|
+
createRelayHandler,
|
|
159
|
+
createRelayNotifier
|
|
160
|
+
};
|
|
161
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/in-relay** — the relay server half (#1237).\n *\n * A frame dispatcher over a store whose profile EXCLUDES whole-vault replace\n * and vault enumeration. The exclusions are structural, not configured.\n *\n * ## Why this is not `in-rest` with a narrower `allow` set\n *\n * `in-rest` types its `store` as a full `NoydbStore` and gates dispatch with a\n * runtime `Set`. That is correct for `in-rest`, whose job is to serve a whole\n * store. Building a relay that way means handing the handler an object that\n * CARRIES `saveAll` and trusting the `Set` not to call it — structural absence\n * downgraded to a runtime allowlist, which is the one property a relay exists\n * to have. Here the store is a {@link NoydbRelayStore}, so the excluded members\n * cannot be called because they are not on the type.\n *\n * ## Two exclusions, two different reasons\n *\n * - `saveAll` — whole-vault replace. A relay that can be asked to overwrite a\n * vault wholesale is a rollback superweapon aimed at the hosts trusting it.\n * - `listVaults` — enumeration is an existence leak. A relay serves vaults the\n * caller already names; it never answers \"what else is here\".\n *\n * ## Unknown-method, not forbidden-method\n *\n * An excluded method is refused as **unknown** (400), never as forbidden (403).\n * A 403 would confirm the method exists and is merely disallowed here, which\n * names the excluded surface to anyone probing. Matching `@doi-db/daemon`,\n * which reached the same conclusion independently in its native implementation.\n *\n * @module\n */\nimport type { NoydbRelayStore, EncryptedEnvelope, TxOp } from '@noy-db/hub/to'\n\n/**\n * The relay's method vocabulary.\n *\n * `saveAll` and `listVaults` are absent because they are absent from\n * {@link NoydbRelayStore} — adding either here would not compile a call.\n *\n * ⚠️ That is NOT sufficient on its own, and the first version of this comment\n * claimed it was: it said the list \"cannot drift away from the type it\n * dispatches to\", and it had already drifted by FOUR members\n * (`listPage`, `getStoreTime`, `presencePublish`, `presenceSubscribe`).\n * Omission is silent in the direction the type cannot catch — a member present\n * on the type and missing here is simply unreachable, and `listPage` being\n * unreachable means clients fall back to `loadAll`, which is the regression\n * pagination exists to prevent.\n *\n * So the invariant is now ENFORCED rather than asserted: {@link NOT_RELAYED}\n * names every deliberate exclusion, and a compile-time check below fails if\n * some method of {@link NoydbRelayStore} appears in neither list. Adding a\n * method to the store contract forces a decision here instead of a silent drop.\n */\nexport const RELAY_METHODS = [\n 'get', 'put', 'delete', 'list', 'loadAll',\n 'ping', 'listSince', 'listPage', 'getStoreTime', 'presencePublish',\n 'estimateUsage', 'tx',\n] as const\n\nexport type RelayMethod = (typeof RELAY_METHODS)[number]\n\n/**\n * Members of {@link NoydbRelayStore} deliberately NOT dispatched, with reasons.\n *\n * - `presenceSubscribe` — returns an unsubscribe FUNCTION. A frame carries\n * JSON, so this is not merely unimplemented, it is unrepresentable in a\n * request/response shape. Server-initiated delivery is what the notify frame\n * is for ({@link RelayNotifyFrame}, #1238), not a return value.\n * - `presignUrl` — hands the caller a time-limited URL that fetches the\n * envelope **directly from the backing object store**, around this relay.\n * That defeats the reason a relay exists: it is the mediating point, so a\n * presigned URL survives revocation, escapes metering, and is unobservable\n * here. Excluded on the security argument, not on serialisability — it would\n * marshal perfectly, which is what makes it worth stating.\n */\nexport const NOT_RELAYED = ['presenceSubscribe', 'presignUrl'] as const\n\n/**\n * Compile-time completeness check (#1237 follow-up). If a method exists on the\n * relay store type and is in neither list, `_exhaustive` errors — the drop is\n * caught at build time rather than becoming an unreachable method in a\n * published package.\n */\ntype StoreMethodNames = {\n [K in keyof NoydbRelayStore]-?: NonNullable<NoydbRelayStore[K]> extends (...args: never[]) => unknown ? K : never\n}[keyof NoydbRelayStore]\ntype Accounted = RelayMethod | (typeof NOT_RELAYED)[number]\ntype Unaccounted = Exclude<StoreMethodNames, Accounted>\nconst _exhaustive: Unaccounted extends never ? true : ['unaccounted store methods', Unaccounted] = true\nvoid _exhaustive\n\n/** One request frame. Identical shape to `in-rest`'s, deliberately — see the module docs. */\nexport interface RelayFrame {\n readonly id: string\n readonly method: string\n readonly args: readonly unknown[]\n}\n\n/** A frame result: either a value, or an error with a status a transport can map. */\nexport type RelayResult =\n | { readonly ok: true; readonly id: string; readonly value: unknown }\n | { readonly ok: false; readonly id: string; readonly status: 400 | 500 | 501; readonly error: { readonly name: string; readonly message: string } }\n\nexport interface RelayHandlerOptions {\n /**\n * The store this relay serves. Typed as the NARROWED profile: a store may\n * carry `saveAll`, but this handler structurally cannot reach it.\n */\n readonly store: NoydbRelayStore\n /**\n * Where successful mutations are announced (#1238). Omit it and the handler\n * behaves exactly as before — pull-only.\n */\n readonly notify?: RelayNotifier\n}\n\n// ─── The notify frame — BESIDE /rpc (#1238) ──────────────────────────────\n//\n// Every transport in this family is client-pull. A hosted store that knows a\n// record changed had no way to say so, so a client polled or learned on its\n// next read. This frame is the server's one sentence, and it is deliberately a\n// SMALL one. The shape questions the issue asked, answered here rather than\n// assumed:\n//\n// - **Not a third encoding of the store contract.** A request is still\n// `{ id, method, args }`; a notification is not a method call and shares no\n// vocabulary with one. There are exactly two things on a relay wire besides\n// results: requests, discriminated by `method`, and this, discriminated by\n// `t: 'notify'`.\n// - **Correlation id semantics.** `seq` is a per-SUBSCRIPTION sequence,\n// contiguous from 1 — not a request correlation (there is no request) and\n// not a subscription handle (that is the transport's business). A sequence\n// lets a client detect a GAP; a handle would not. The client's recovery from\n// a gap is `listSince(vault, collection, <last ts it applied>)`, which\n// already exists and is the reconciliation path: the frame is a hint that\n// makes polling unnecessary, never the source of truth.\n// - **What it carries.** The ADDRESS of the change (`vault/collection/id`),\n// the op, and the envelope's own `_ts` (a delete has none, so the relay's\n// clock stands in). **Never the envelope.** Carrying it would make the push a\n// write path, with everything that implies for a fail-closed auth check and\n// for a store that is untrusted by construction; the client fetches through\n// the authenticated request path it already has.\n// - **Delivery guarantee: at-most-once, in order per subscription.** A dropped\n// frame is recoverable by the gap + `listSince` rule above. A subscriber\n// that throws is dropped for that frame and never breaks the write or its\n// neighbours — the mutation already landed.\n// - **Auth.** A subscription is scoped to the ONE vault it named — a client\n// never hears about a vault it did not already know, which keeps the\n// `listVaults` exclusion honest. Binding a subscription to an authenticated\n// session is the transport's job (this package is transport-neutral); the\n// frame carries no secret and no content, so the failure mode of a mistake\n// there is an existence leak, not a data leak — still a leak, still the\n// transport's to close.\n\n/** Server-push frame: one successful mutation, by address. See the block above. */\nexport interface RelayNotifyFrame {\n readonly t: 'notify'\n /** Per-subscription, contiguous from 1. A gap means frames were missed: reconcile with `listSince`. */\n readonly seq: number\n readonly vault: string\n readonly collection: string\n readonly id: string\n readonly op: 'put' | 'delete'\n /** The envelope's `_ts` for a put; the relay's clock for a delete. Feed it to `listSince`. */\n readonly ts: string\n}\n\n/** A change as the handler reports it, before any subscriber's `seq` is stamped. */\nexport type RelayChange = Omit<RelayNotifyFrame, 't' | 'seq'>\n\nexport interface RelayNotifier {\n /**\n * Deliver every subsequent change in `vault` to `deliver`, each stamped with\n * this subscription's own `seq`. Returns an unsubscribe. Late joiners start\n * at 1 — the sequence is the subscription's, not the server's history.\n */\n subscribe(vault: string, deliver: (frame: RelayNotifyFrame) => void): () => void\n /** Called by the handler after a mutation LANDED. Not for transports to call. */\n publish(change: RelayChange): void\n}\n\n/**\n * The in-process fan-out. One per relay; a transport calls `subscribe` when an\n * authenticated session asks to watch a vault and forwards each frame on that\n * session's wire.\n */\nexport function createRelayNotifier(): RelayNotifier {\n type Sub = { deliver: (frame: RelayNotifyFrame) => void; seq: number }\n const byVault = new Map<string, Set<Sub>>()\n return {\n subscribe(vault, deliver) {\n const sub: Sub = { deliver, seq: 0 }\n let subs = byVault.get(vault)\n if (!subs) byVault.set(vault, (subs = new Set()))\n subs.add(sub)\n return () => {\n subs.delete(sub)\n if (subs.size === 0) byVault.delete(vault)\n }\n },\n publish(change) {\n const subs = byVault.get(change.vault)\n if (!subs) return\n for (const sub of [...subs]) {\n const frame: RelayNotifyFrame = { t: 'notify', seq: ++sub.seq, ...change }\n try {\n sub.deliver(frame)\n } catch {\n // The write landed; a subscriber that cannot take the frame does not\n // get to un-land it or starve the others. Its seq advanced, so it\n // will see the gap and reconcile.\n }\n }\n },\n }\n}\n\n/** Thrown for a method this relay does not implement — including excluded ones. */\nexport class UnknownRelayMethodError extends Error {\n constructor(method: string) {\n super(`Unknown method: ${method}`)\n this.name = 'UnknownRelayMethodError'\n }\n}\n\n/**\n * Thrown when the relay KNOWS a method but the backing store does not\n * implement it (the optional members of the store contract).\n *\n * ⚠️ Deliberately distinct from {@link UnknownRelayMethodError}, and the first\n * version of this package collapsed them: an absent optional method threw\n * `UnknownRelayMethodError` from inside dispatch, which the catch-all reported\n * as **500**. That misattributes a store CAPABILITY GAP as a server fault —\n * two states warranting opposite responses (the client should degrade\n * gracefully vs. the operator should investigate) rendered identically.\n *\n * `501` matches `@noy-db/in-rest`, which maps its own `UnsupportedMethodError`\n * to `501 NotImplemented`, and `@doi-db/daemon`, which reached 501\n * independently. It leaks nothing about the relay's EXCLUSIONS: those are\n * refused as 400-unknown before dispatch is reached.\n */\nexport class UnsupportedRelayMethodError extends Error {\n constructor(method: string) {\n super(`Method not supported by this store: ${method}`)\n this.name = 'UnsupportedRelayMethodError'\n }\n}\n\nfunction isRelayMethod(m: string): m is RelayMethod {\n return (RELAY_METHODS as readonly string[]).includes(m)\n}\n\nasync function dispatch(store: NoydbRelayStore, method: RelayMethod, args: readonly unknown[]): Promise<unknown> {\n switch (method) {\n case 'get': {\n const [vault, collection, id] = args as [string, string, string]\n return store.get(vault, collection, id)\n }\n case 'put': {\n const [vault, collection, id, envelope, expectedVersion] = args as\n [string, string, string, EncryptedEnvelope, number | undefined]\n return store.put(vault, collection, id, envelope, expectedVersion)\n }\n case 'delete': {\n const [vault, collection, id] = args as [string, string, string]\n return store.delete(vault, collection, id)\n }\n case 'list': {\n const [vault, collection] = args as [string, string]\n return store.list(vault, collection)\n }\n case 'loadAll': {\n const [vault] = args as [string]\n return store.loadAll(vault)\n }\n case 'ping': {\n return store.ping?.() ?? true\n }\n case 'listSince': {\n const [vault, collection, since] = args as [string, string, string]\n if (store.listSince === undefined) throw new UnsupportedRelayMethodError('listSince')\n return store.listSince(vault, collection, since)\n }\n case 'listPage': {\n // Absent from the first published vocabulary. Its absence is not neutral:\n // a client with no pagination falls back to `loadAll`, which is the\n // regression `listPage` exists to prevent.\n const [vault, collection, cursor, limit] = args as [string, string, string | undefined, number | undefined]\n if (store.listPage === undefined) throw new UnsupportedRelayMethodError('listPage')\n return store.listPage(vault, collection, cursor, limit)\n }\n case 'getStoreTime': {\n if (store.getStoreTime === undefined) throw new UnsupportedRelayMethodError('getStoreTime')\n return store.getStoreTime()\n }\n case 'presencePublish': {\n const [channel, payload] = args as [string, string]\n if (store.presencePublish === undefined) throw new UnsupportedRelayMethodError('presencePublish')\n return store.presencePublish(channel, payload)\n }\n case 'estimateUsage': {\n if (store.estimateUsage === undefined) throw new UnsupportedRelayMethodError('estimateUsage')\n return store.estimateUsage()\n }\n case 'tx': {\n // Relayed: a native transaction is the store's own atomicity, and\n // withholding it would silently downgrade a relayed vault to per-op\n // writes — the CAS guarantees callers rely on would quietly weaken.\n const [ops] = args as [readonly TxOp[]]\n if (store.tx === undefined) throw new UnsupportedRelayMethodError('tx')\n return store.tx(ops)\n }\n }\n}\n\n/** The changes a successful mutating frame implies; empty for a read. */\nfunction changesOf(method: RelayMethod, args: readonly unknown[]): RelayChange[] {\n switch (method) {\n case 'put': {\n const [vault, collection, id, envelope] = args as [string, string, string, EncryptedEnvelope]\n return [{ vault, collection, id, op: 'put', ts: envelope._ts }]\n }\n case 'delete': {\n const [vault, collection, id] = args as [string, string, string]\n return [{ vault, collection, id, op: 'delete', ts: new Date().toISOString() }]\n }\n case 'tx': {\n const [ops] = args as [readonly TxOp[]]\n return ops.map((op) => op.type === 'put'\n ? { vault: op.vault, collection: op.collection, id: op.id, op: 'put' as const, ts: op.envelope!._ts }\n : { vault: op.vault, collection: op.collection, id: op.id, op: 'delete' as const, ts: new Date().toISOString() })\n }\n default:\n return []\n }\n}\n\n/**\n * Build a frame handler over a narrowed store.\n *\n * Transport-neutral by design: it takes a decoded frame and returns a decoded\n * result, so a caller may serve it over HTTP, a WebSocket, or QUIC without this\n * package knowing which.\n */\nexport function createRelayHandler(options: RelayHandlerOptions): (frame: RelayFrame) => Promise<RelayResult> {\n const { store, notify } = options\n return async (frame: RelayFrame): Promise<RelayResult> => {\n if (!isRelayMethod(frame.method)) {\n // 400 unknown-method, NOT 403. Distinguishing would name the excluded\n // members to a prober; to this relay they simply do not exist.\n return {\n ok: false, id: frame.id, status: 400,\n error: { name: 'UnknownRelayMethodError', message: `Unknown method: ${frame.method}` },\n }\n }\n try {\n const value = await dispatch(store, frame.method, frame.args)\n // Announce AFTER the store returned: a frame reports what landed, so a\n // failed mutation publishes nothing and a tx publishes only once the\n // whole batch committed (#1238).\n if (notify) for (const change of changesOf(frame.method, frame.args)) notify.publish(change)\n return { ok: true, id: frame.id, value }\n } catch (err) {\n const e = err as Error\n // A store that does not implement an OPTIONAL method is a capability gap,\n // not a server fault: 501, so a client can degrade rather than retry.\n // Matches in-rest's UnsupportedMethodError -> 501 and @doi-db/daemon.\n const status = e instanceof UnsupportedRelayMethodError ? 501 : 500\n // The error's own name is forwarded so a client can re-hydrate a\n // ConflictError by name — the seam contract in-rest and to-rest settled\n // in #1218. Never `instanceof` across this boundary.\n return { ok: false, id: frame.id, status, error: { name: e.name, message: e.message } }\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/in-relay",
|
|
3
|
+
"version": "0.8.0-pre.0",
|
|
4
|
+
"description": "Relay server half for noy-db — a frame dispatcher over a NARROWED store profile, where saveAll and listVaults are absent by construction",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@noy-db/hub": "^0.8.0-pre.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@noy-db/hub": "0.8.0-pre.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/noy-db/core.git",
|
|
30
|
+
"directory": "packages/in-relay"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"tag": "latest"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"noy-db",
|
|
42
|
+
"in-rest",
|
|
43
|
+
"rest",
|
|
44
|
+
"http",
|
|
45
|
+
"api"
|
|
46
|
+
],
|
|
47
|
+
"homepage": "https://github.com/noy-db/core/tree/main/packages/in-relay#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/noy-db/core/issues"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"lint": "eslint src/",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|