@statewalker/httpeers-hub 0.1.3
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 +21 -0
- package/README.md +108 -0
- package/dist/admin.d.ts +35 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +39 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +42 -0
- package/dist/create-hub.d.ts +90 -0
- package/dist/create-hub.d.ts.map +1 -0
- package/dist/create-hub.js +132 -0
- package/dist/endpoints.d.ts +137 -0
- package/dist/endpoints.d.ts.map +1 -0
- package/dist/endpoints.js +305 -0
- package/dist/hub-state.d.ts +114 -0
- package/dist/hub-state.d.ts.map +1 -0
- package/dist/hub-state.js +118 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/mesh-view.d.ts +44 -0
- package/dist/mesh-view.d.ts.map +1 -0
- package/dist/mesh-view.js +23 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +39 -0
- package/dist/registries.d.ts +45 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/registries.js +133 -0
- package/dist/storage.d.ts +53 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +113 -0
- package/package.json +66 -0
- package/src/admin.ts +50 -0
- package/src/browser.ts +73 -0
- package/src/create-hub.ts +200 -0
- package/src/endpoints.ts +476 -0
- package/src/hub-state.ts +199 -0
- package/src/index.ts +33 -0
- package/src/mesh-view.ts +83 -0
- package/src/node.ts +40 -0
- package/src/registries.ts +150 -0
- package/src/storage.ts +136 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mikhail Kotelnikov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @statewalker/httpeers-hub
|
|
2
|
+
|
|
3
|
+
Minting membership, holding the registries, and saying who is in the mesh.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createHub, memoryStorage } from "@statewalker/httpeers-hub";
|
|
7
|
+
|
|
8
|
+
const hub = await createHub({
|
|
9
|
+
selfPeerId, // this hub's peerId IS the mesh's name
|
|
10
|
+
policies: rules,
|
|
11
|
+
storage: memoryStorage(),
|
|
12
|
+
mintToken: async (sub, roles) => mintToken({ signer, sub, roles, ttlMs: 60_000 }),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const invite = await hub.invitations.create(["member"], 60_000);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`hub.mounts` is a mount table. Serving it on a wire is somebody else's job.
|
|
19
|
+
|
|
20
|
+
## A hub has no transport, and that is what lets it run in a tab
|
|
21
|
+
|
|
22
|
+
This package dials nothing and listens on nothing. `servePeer`
|
|
23
|
+
(`@statewalker/httpeers-libp2p`) is what puts the mount table on a wire, and
|
|
24
|
+
the dependency list here is `core`, `access` and `hono` — no libp2p, asserted
|
|
25
|
+
by `tests/boundary.test.ts`.
|
|
26
|
+
|
|
27
|
+
That is not tidiness. It is the property that makes a **hub in a browser tab**
|
|
28
|
+
possible at all, and it is why the hub's *lifecycle* is deliberately not in
|
|
29
|
+
this package: composing a node, a relay reservation, `createHub` and an edge is
|
|
30
|
+
application wiring, and both the Node hub and the hub page do it themselves.
|
|
31
|
+
`httpeers-conformance` compiles that composition as a consumer, so if the
|
|
32
|
+
packages ever stop being sufficient for it, a test says so.
|
|
33
|
+
|
|
34
|
+
## Invitations are bearer credentials, and are treated as such
|
|
35
|
+
|
|
36
|
+
An invitation id is **128 bits from the platform CSPRNG**, never `Math.random`:
|
|
37
|
+
whoever holds one can become a member. The hub generates it **by default**, so
|
|
38
|
+
nobody has to invent unguessable strings — `create(roles, ttlMs)` is the whole
|
|
39
|
+
call. A caller may name the id (`{ id }`) for a deployment migrating existing
|
|
40
|
+
codes or a test wanting a constant, and that is the only way to get a weak one.
|
|
41
|
+
|
|
42
|
+
Every mutator resolves when the write is **durable**. A caller is never told
|
|
43
|
+
"created" before it is, because an invitation handed out over the phone and
|
|
44
|
+
then lost to a crash a millisecond later is an invitation somebody is standing
|
|
45
|
+
there holding.
|
|
46
|
+
|
|
47
|
+
Unredeemed invitations **survive a restart** — they are in the snapshot, not in
|
|
48
|
+
memory — and `pending()` lists them so an operator can see what is outstanding.
|
|
49
|
+
|
|
50
|
+
## The snapshot is synchronous and the storage is not
|
|
51
|
+
|
|
52
|
+
`SnapshotStore.write` must stay synchronous: `createHubState` calls it and
|
|
53
|
+
returns, so an async write would open a window where a caller has been told a
|
|
54
|
+
member was added while the snapshot still says otherwise. There is no way to
|
|
55
|
+
block on a promise in a browser, so the shape is forced:
|
|
56
|
+
|
|
57
|
+
**the in-memory copy is authoritative, and the backend trails it.** `write`
|
|
58
|
+
replaces `current` and returns; the flush runs behind it on one promise chain.
|
|
59
|
+
Reads answer from `current`, so the hub always sees what it last wrote.
|
|
60
|
+
|
|
61
|
+
The chain matters: writes reach storage **in order**. An unordered
|
|
62
|
+
`void put(...)` per write would let a slow earlier write land on top of a later
|
|
63
|
+
one and **resurrect a spent invitation id**, which is a membership bypass.
|
|
64
|
+
`asyncSnapshotStore` is that wrapper over any `KeyValueStorage`, and
|
|
65
|
+
`flushed()` is how a test waits for durability.
|
|
66
|
+
|
|
67
|
+
Honestly stated: a tab closed between a write and its flush loses that write.
|
|
68
|
+
This is a page, not a database.
|
|
69
|
+
|
|
70
|
+
| Import | Storage |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `.` | `memoryStorage()`, `filesStorage(files, dir?)` over a `FilesApi` |
|
|
73
|
+
| `./node` | `fileStorage(path)` |
|
|
74
|
+
| `./browser` | `idbStorage(name?, store?)` — IndexedDB, which is transactional |
|
|
75
|
+
|
|
76
|
+
`filesStorage` **writes-then-moves**. The prototype's Node persistence wrote in
|
|
77
|
+
place, so a crash mid-write left a half-written snapshot where the whole state
|
|
78
|
+
lives; this adapter does not inherit that.
|
|
79
|
+
|
|
80
|
+
## Roles come off the rules, not a second list
|
|
81
|
+
|
|
82
|
+
There is no separate vocabulary document. A role exists for this mesh exactly
|
|
83
|
+
when some rule fires on it, so `roleNames(rules)` is the registry, and both
|
|
84
|
+
`members.setRoles` and `invitations.create` validate against it. A role added
|
|
85
|
+
to the policy appears everywhere without anyone updating a second place.
|
|
86
|
+
|
|
87
|
+
## Changing roles takes two writes, or it is a lie
|
|
88
|
+
|
|
89
|
+
`MemberStore.setRoles` rewrites the record — but the peer is carrying a token
|
|
90
|
+
that already states its **old** roles and stays valid until it expires, so a
|
|
91
|
+
demoted admin keeps admin for the life of that token. `revocations.changeRoles`
|
|
92
|
+
records the change so tokens minted before it stop verifying, which is what
|
|
93
|
+
makes the new roles take effect on the peer's next heartbeat.
|
|
94
|
+
|
|
95
|
+
The store does not do this for you. Both halves, or the change is cosmetic.
|
|
96
|
+
|
|
97
|
+
## Presence is a heartbeat
|
|
98
|
+
|
|
99
|
+
A member that stops beating is swept from the mesh view within one TTL, and its
|
|
100
|
+
advertisements go with it. `sweep()` is callable directly — which is what a
|
|
101
|
+
test with a controlled clock wants — or driven on a timer with
|
|
102
|
+
`sweepIntervalMs`.
|
|
103
|
+
|
|
104
|
+
## Tests
|
|
105
|
+
|
|
106
|
+
**43, plus 4 skipped** (the platform-entry exemptions in the boundary test).
|
|
107
|
+
The hub is also exercised for real in `@statewalker/httpeers-conformance`,
|
|
108
|
+
where it mints an invitation that a live member redeems over a relay.
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DELETE /admin/members/{peerId}` — closes the gap the design record has
|
|
3
|
+
* carried since Task 1: `MemberStore.remove` has existed as a store method
|
|
4
|
+
* with no endpoint and no authorisation the whole time, "exactly the kind
|
|
5
|
+
* of thing that must not be reachable by an ordinary member" (design
|
|
6
|
+
* record §5.2). Capability gating (`std:mesh.admin`) is `policy.ts`'s
|
|
7
|
+
* `/admin` policy in `policy.ts`, not this handler's job — this file only
|
|
8
|
+
* does the removal, same split as every other handler in this app.
|
|
9
|
+
*
|
|
10
|
+
* TWO CALLS, NOT ONE, BECAUSE THIS PACKAGE SPLIT WHAT THE ARCHIVE KEPT AS
|
|
11
|
+
* ONE STORE. `hub.memberStore.remove(peerId)` drops membership;
|
|
12
|
+
* `hub.revocations.revoke(peerId)` records the change so a provider's
|
|
13
|
+
* pulled deny-list cache can see it — see `tests/support/mesh.ts`'s own
|
|
14
|
+
* header comment for the archived one-call precedent this splits in two.
|
|
15
|
+
* `RevocationRegistry.revoke` bumps `policyVersion()` internally (see
|
|
16
|
+
* `revocation.ts`): there is no separate "bump the version" step to
|
|
17
|
+
* forget — calling `revoke` at all IS the bump, which is what makes a
|
|
18
|
+
* provider's next heartbeat see the change.
|
|
19
|
+
*
|
|
20
|
+
* Removal (and revocation) is unconditional and idempotent: `MemberStore
|
|
21
|
+
* .remove` on a peerId that is not currently a member is a harmless no-op
|
|
22
|
+
* (`Map.delete` on a missing key), and revoking a peerId with no prior
|
|
23
|
+
* membership is a legitimate "make sure this identity can never redeem an
|
|
24
|
+
* old token" operation, not an error condition — so this handler does not
|
|
25
|
+
* 404 on an unknown peerId.
|
|
26
|
+
*/
|
|
27
|
+
import type { RevocationRegistry } from "@statewalker/httpeers-access/issuer";
|
|
28
|
+
import type { FetchHandler, MemberStore } from "@statewalker/httpeers-core";
|
|
29
|
+
export interface AdminEndpointsInit {
|
|
30
|
+
memberStore: MemberStore;
|
|
31
|
+
revocations: RevocationRegistry;
|
|
32
|
+
}
|
|
33
|
+
/** `DELETE /admin/members/{peerId}`. Mounted at `/admin/members` — see `hub/endpoints.ts`. */
|
|
34
|
+
export declare function createAdminEndpoints(init: AdminEndpointsInit): FetchHandler;
|
|
35
|
+
//# sourceMappingURL=admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAI5E,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,YAAY,CAW3E"}
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DELETE /admin/members/{peerId}` — closes the gap the design record has
|
|
3
|
+
* carried since Task 1: `MemberStore.remove` has existed as a store method
|
|
4
|
+
* with no endpoint and no authorisation the whole time, "exactly the kind
|
|
5
|
+
* of thing that must not be reachable by an ordinary member" (design
|
|
6
|
+
* record §5.2). Capability gating (`std:mesh.admin`) is `policy.ts`'s
|
|
7
|
+
* `/admin` policy in `policy.ts`, not this handler's job — this file only
|
|
8
|
+
* does the removal, same split as every other handler in this app.
|
|
9
|
+
*
|
|
10
|
+
* TWO CALLS, NOT ONE, BECAUSE THIS PACKAGE SPLIT WHAT THE ARCHIVE KEPT AS
|
|
11
|
+
* ONE STORE. `hub.memberStore.remove(peerId)` drops membership;
|
|
12
|
+
* `hub.revocations.revoke(peerId)` records the change so a provider's
|
|
13
|
+
* pulled deny-list cache can see it — see `tests/support/mesh.ts`'s own
|
|
14
|
+
* header comment for the archived one-call precedent this splits in two.
|
|
15
|
+
* `RevocationRegistry.revoke` bumps `policyVersion()` internally (see
|
|
16
|
+
* `revocation.ts`): there is no separate "bump the version" step to
|
|
17
|
+
* forget — calling `revoke` at all IS the bump, which is what makes a
|
|
18
|
+
* provider's next heartbeat see the change.
|
|
19
|
+
*
|
|
20
|
+
* Removal (and revocation) is unconditional and idempotent: `MemberStore
|
|
21
|
+
* .remove` on a peerId that is not currently a member is a harmless no-op
|
|
22
|
+
* (`Map.delete` on a missing key), and revoking a peerId with no prior
|
|
23
|
+
* membership is a legitimate "make sure this identity can never redeem an
|
|
24
|
+
* old token" operation, not an error condition — so this handler does not
|
|
25
|
+
* 404 on an unknown peerId.
|
|
26
|
+
*/
|
|
27
|
+
import { json } from "@statewalker/httpeers-core";
|
|
28
|
+
import { Hono } from "hono";
|
|
29
|
+
/** `DELETE /admin/members/{peerId}`. Mounted at `/admin/members` — see `hub/endpoints.ts`. */
|
|
30
|
+
export function createAdminEndpoints(init) {
|
|
31
|
+
const app = new Hono();
|
|
32
|
+
app.delete("/admin/members/:peerId", (c) => {
|
|
33
|
+
const peerId = c.req.param("peerId");
|
|
34
|
+
init.memberStore.remove(peerId);
|
|
35
|
+
init.revocations.revoke(peerId);
|
|
36
|
+
return json({ ok: true, removed: peerId, policyVersion: init.revocations.policyVersion() });
|
|
37
|
+
});
|
|
38
|
+
return app.fetch;
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser profile: hub state in IndexedDB.
|
|
3
|
+
*
|
|
4
|
+
* A hub in a tab is a real hub — it mints, it holds the registries, and its
|
|
5
|
+
* members expect to still be members after a reload. `localStorage` would do
|
|
6
|
+
* for strings, but IndexedDB is transactional, which matters when two writes
|
|
7
|
+
* race a reload.
|
|
8
|
+
*/
|
|
9
|
+
import type { KeyValueStorage } from "./storage.js";
|
|
10
|
+
export declare function idbStorage(name?: string, store?: string): KeyValueStorage;
|
|
11
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAyBpD,wBAAgB,UAAU,CAAC,IAAI,SAAiB,EAAE,KAAK,SAAU,GAAG,eAAe,CAsClF"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser profile: hub state in IndexedDB.
|
|
3
|
+
*
|
|
4
|
+
* A hub in a tab is a real hub — it mints, it holds the registries, and its
|
|
5
|
+
* members expect to still be members after a reload. `localStorage` would do
|
|
6
|
+
* for strings, but IndexedDB is transactional, which matters when two writes
|
|
7
|
+
* race a reload.
|
|
8
|
+
*/
|
|
9
|
+
export function idbStorage(name = "httpeers-hub", store = "state") {
|
|
10
|
+
const open = () => new Promise((resolve, reject) => {
|
|
11
|
+
const request = indexedDB.open(name, 1);
|
|
12
|
+
request.onupgradeneeded = () => {
|
|
13
|
+
if (!request.result.objectStoreNames.contains(store)) {
|
|
14
|
+
request.result.createObjectStore(store);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
request.onsuccess = () => resolve(request.result);
|
|
18
|
+
request.onerror = () => reject(request.error);
|
|
19
|
+
});
|
|
20
|
+
const run = async (mode, act) => {
|
|
21
|
+
const db = await open();
|
|
22
|
+
try {
|
|
23
|
+
return await new Promise((resolve, reject) => {
|
|
24
|
+
const request = act(db.transaction(store, mode).objectStore(store));
|
|
25
|
+
request.onsuccess = () => resolve(request.result);
|
|
26
|
+
request.onerror = () => reject(request.error);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
db.close();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
get: (key) => run("readonly", (s) => s.get(key)),
|
|
35
|
+
set: async (key, value) => {
|
|
36
|
+
await run("readwrite", (s) => s.put(value, key));
|
|
37
|
+
},
|
|
38
|
+
delete: async (key) => {
|
|
39
|
+
await run("readwrite", (s) => s.delete(key));
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One constructor that returns the parts.
|
|
3
|
+
*
|
|
4
|
+
* THE API SPEC SAID THERE WOULD NOT BE A `createHub`, and the prototype proved
|
|
5
|
+
* it wrong by writing the assembly three times — the Node hub, the browser hub
|
|
6
|
+
* and the test hub. A hub is not a mount table: it is a mount table *plus* a
|
|
7
|
+
* snapshot store, a clock, a revocation registry, an invitation store, two
|
|
8
|
+
* registries with different consistency needs, and a sweep timer. Anything that
|
|
9
|
+
* hands back only the mounts leaves the other six to every caller.
|
|
10
|
+
*
|
|
11
|
+
* `mounts` is still separately usable, so nothing is walled off — the point is
|
|
12
|
+
* to stop each assembly re-deriving the wiring, not to hide it.
|
|
13
|
+
*
|
|
14
|
+
* THE ORDERING CYCLE, closed here because it cannot be closed by the caller.
|
|
15
|
+
* A hub's node must exist before `createHub` (the node needs a key; this needs
|
|
16
|
+
* `selfPeerId`), and the node's connection gater needs `hub.isMember`. So the
|
|
17
|
+
* documented order is:
|
|
18
|
+
*
|
|
19
|
+
* key -> node with a LATE-BOUND gater -> createHub -> servePeer
|
|
20
|
+
* -> point the gater's thunk at hub.isMember
|
|
21
|
+
*
|
|
22
|
+
* `membershipGater` takes a thunk for exactly this reason; see its comment in
|
|
23
|
+
* `httpeers-libp2p`.
|
|
24
|
+
*/
|
|
25
|
+
import type { RuleSet } from "@statewalker/httpeers-access";
|
|
26
|
+
import { RevocationRegistry } from "@statewalker/httpeers-access/issuer";
|
|
27
|
+
import type { FetchHandler, MeshView, Mounts, PeerIdStr } from "@statewalker/httpeers-core";
|
|
28
|
+
import { type HubEndpointsInit } from "./endpoints.js";
|
|
29
|
+
import { type InvitationRecord, type InvitationRedemption, type PersistentHub } from "./hub-state.js";
|
|
30
|
+
import { type KeyValueStorage } from "./storage.js";
|
|
31
|
+
export interface CreateHubInit {
|
|
32
|
+
/** This hub's own peerId — the mesh identity every token restates. */
|
|
33
|
+
selfPeerId: PeerIdStr;
|
|
34
|
+
/** Mint a membership token that self-certifies as this mesh. */
|
|
35
|
+
mintToken: HubEndpointsInit["mintToken"];
|
|
36
|
+
/** Where members and spent invitation ids are persisted. Defaults to memory. */
|
|
37
|
+
storage?: KeyValueStorage;
|
|
38
|
+
/** This mesh's Datalog rules. Injected, never global — there is no default rule set. */
|
|
39
|
+
policies: RuleSet;
|
|
40
|
+
now?: () => number;
|
|
41
|
+
presenceTtlMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Expire stale presence on a timer. Omit to drive `sweep()` yourself, which
|
|
44
|
+
* is what a test with a controlled clock wants.
|
|
45
|
+
*/
|
|
46
|
+
sweepIntervalMs?: number;
|
|
47
|
+
extraMounts?: Record<string, FetchHandler>;
|
|
48
|
+
ownAdvertisements?: Array<{
|
|
49
|
+
id: string;
|
|
50
|
+
kind: string;
|
|
51
|
+
title: string;
|
|
52
|
+
}>;
|
|
53
|
+
advertisementAccess?: Record<string, string>;
|
|
54
|
+
/** Longest life of any token this hub mints. Sets the revocation pruning horizon. */
|
|
55
|
+
maxTokenTtlMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface Hub {
|
|
58
|
+
/** The `.well-known` surface and `/admin`, plus whatever `extraMounts` added. */
|
|
59
|
+
mounts: Mounts;
|
|
60
|
+
/** Expire stale presence and the advertisements that rode along with it. */
|
|
61
|
+
sweep(): void;
|
|
62
|
+
/** The very view `GET /.well-known/mesh` serves, read in-process. */
|
|
63
|
+
meshView(caller?: PeerIdStr): MeshView;
|
|
64
|
+
/**
|
|
65
|
+
* The operator-facing shape (spec §9), which differs from the store's:
|
|
66
|
+
* the hub generates the id, and every mutator resolves when the write is
|
|
67
|
+
* DURABLE — so a caller is never told "created" before it is, and an
|
|
68
|
+
* invitation handed out over the phone cannot be lost to a crash a
|
|
69
|
+
* millisecond later.
|
|
70
|
+
*/
|
|
71
|
+
invitations: {
|
|
72
|
+
create(roles: string[], ttlMs: number, opts?: {
|
|
73
|
+
id?: string;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
id: string;
|
|
76
|
+
expiresAt: number;
|
|
77
|
+
}>;
|
|
78
|
+
redeem(id: string): Promise<InvitationRedemption>;
|
|
79
|
+
status(id: string): "unspent" | "redeemed" | "expired" | "unknown";
|
|
80
|
+
pending(): InvitationRecord[];
|
|
81
|
+
};
|
|
82
|
+
members: PersistentHub["memberStore"];
|
|
83
|
+
isMember(peerId: PeerIdStr): boolean;
|
|
84
|
+
revocations: RevocationRegistry;
|
|
85
|
+
rules: RuleSet;
|
|
86
|
+
reset(): Promise<void>;
|
|
87
|
+
stop(): Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
export declare function createHub(init: CreateHubInit): Promise<Hub>;
|
|
90
|
+
//# sourceMappingURL=create-hub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-hub.d.ts","sourceRoot":"","sources":["../src/create-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAsB,KAAK,eAAe,EAAiB,MAAM,cAAc,CAAC;AAEvF,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,UAAU,EAAE,SAAS,CAAC;IACtB,gEAAgE;IAChE,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,gFAAgF;IAChF,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,wFAAwF;IACxF,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,GAAG;IAClB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,KAAK,IAAI,IAAI,CAAC;IACd,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACvC;;;;;;OAMG;IACH,WAAW,EAAE;QACX,MAAM,CACJ,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;YAAE,EAAE,CAAC,EAAE,MAAM,CAAA;SAAE,GACrB,OAAO,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC9C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAClD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;QACnE,OAAO,IAAI,gBAAgB,EAAE,CAAC;KAC/B,CAAC;IACF,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IACrC,WAAW,EAAE,kBAAkB,CAAC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAeD,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CA6FjE"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One constructor that returns the parts.
|
|
3
|
+
*
|
|
4
|
+
* THE API SPEC SAID THERE WOULD NOT BE A `createHub`, and the prototype proved
|
|
5
|
+
* it wrong by writing the assembly three times — the Node hub, the browser hub
|
|
6
|
+
* and the test hub. A hub is not a mount table: it is a mount table *plus* a
|
|
7
|
+
* snapshot store, a clock, a revocation registry, an invitation store, two
|
|
8
|
+
* registries with different consistency needs, and a sweep timer. Anything that
|
|
9
|
+
* hands back only the mounts leaves the other six to every caller.
|
|
10
|
+
*
|
|
11
|
+
* `mounts` is still separately usable, so nothing is walled off — the point is
|
|
12
|
+
* to stop each assembly re-deriving the wiring, not to hide it.
|
|
13
|
+
*
|
|
14
|
+
* THE ORDERING CYCLE, closed here because it cannot be closed by the caller.
|
|
15
|
+
* A hub's node must exist before `createHub` (the node needs a key; this needs
|
|
16
|
+
* `selfPeerId`), and the node's connection gater needs `hub.isMember`. So the
|
|
17
|
+
* documented order is:
|
|
18
|
+
*
|
|
19
|
+
* key -> node with a LATE-BOUND gater -> createHub -> servePeer
|
|
20
|
+
* -> point the gater's thunk at hub.isMember
|
|
21
|
+
*
|
|
22
|
+
* `membershipGater` takes a thunk for exactly this reason; see its comment in
|
|
23
|
+
* `httpeers-libp2p`.
|
|
24
|
+
*/
|
|
25
|
+
import { RevocationRegistry } from "@statewalker/httpeers-access/issuer";
|
|
26
|
+
import { createHubEndpoints } from "./endpoints.js";
|
|
27
|
+
import { createHubState, } from "./hub-state.js";
|
|
28
|
+
import { createMemberStore } from "./registries.js";
|
|
29
|
+
import { asyncSnapshotStore, memoryStorage } from "./storage.js";
|
|
30
|
+
/**
|
|
31
|
+
* An invitation id.
|
|
32
|
+
*
|
|
33
|
+
* 128 bits from the platform CSPRNG, base36. An invitation is a bearer
|
|
34
|
+
* credential for membership, so a guessable id is a way into the mesh —
|
|
35
|
+
* `Math.random` is not acceptable here and the crypto global is available on
|
|
36
|
+
* every runtime this package targets.
|
|
37
|
+
*/
|
|
38
|
+
function newInvitationId() {
|
|
39
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
40
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
41
|
+
}
|
|
42
|
+
export async function createHub(init) {
|
|
43
|
+
const now = init.now ?? Date.now;
|
|
44
|
+
const storage = init.storage ?? memoryStorage();
|
|
45
|
+
// Storage is async; the snapshot seam is not. An in-memory authoritative
|
|
46
|
+
// copy with serialised flushes behind it is what lets `SnapshotStore.write`
|
|
47
|
+
// stay synchronous while the durable write is still awaited by whoever
|
|
48
|
+
// mutated — so a caller is never told "added" before it is.
|
|
49
|
+
const snapshots = await asyncSnapshotStore(storage);
|
|
50
|
+
const state = createHubState({
|
|
51
|
+
store: snapshots,
|
|
52
|
+
rules: init.policies,
|
|
53
|
+
now,
|
|
54
|
+
createMemberStore,
|
|
55
|
+
});
|
|
56
|
+
const revocations = new RevocationRegistry({
|
|
57
|
+
maxTokenTtlMs: init.maxTokenTtlMs ?? 24 * 60 * 60 * 1000,
|
|
58
|
+
now,
|
|
59
|
+
});
|
|
60
|
+
const endpoints = createHubEndpoints({
|
|
61
|
+
selfPeerId: init.selfPeerId,
|
|
62
|
+
mintToken: init.mintToken,
|
|
63
|
+
memberStore: state.memberStore,
|
|
64
|
+
invitations: state.invitations,
|
|
65
|
+
rules: init.policies,
|
|
66
|
+
revocations,
|
|
67
|
+
advertisementAccess: init.advertisementAccess,
|
|
68
|
+
extraMounts: init.extraMounts,
|
|
69
|
+
ownAdvertisements: init.ownAdvertisements,
|
|
70
|
+
presenceTtlMs: init.presenceTtlMs,
|
|
71
|
+
now,
|
|
72
|
+
});
|
|
73
|
+
// The timer is optional so a test can drive `sweep()` with a controlled
|
|
74
|
+
// clock. A hub that also sweeps on a real timer would race that test.
|
|
75
|
+
let timer;
|
|
76
|
+
if (init.sweepIntervalMs != null) {
|
|
77
|
+
timer = setInterval(() => endpoints.sweep(), init.sweepIntervalMs);
|
|
78
|
+
// Never hold a process open for a sweep. A CLI that finished its work
|
|
79
|
+
// should exit, and an interval that keeps it alive is a hang nobody
|
|
80
|
+
// attributes to the hub.
|
|
81
|
+
timer.unref?.();
|
|
82
|
+
}
|
|
83
|
+
let stopped = false;
|
|
84
|
+
return {
|
|
85
|
+
mounts: endpoints.mounts,
|
|
86
|
+
sweep: endpoints.sweep,
|
|
87
|
+
meshView: endpoints.meshView,
|
|
88
|
+
invitations: {
|
|
89
|
+
async create(roles, ttlMs, opts) {
|
|
90
|
+
// A caller may name the id — a deployment migrating existing codes, or
|
|
91
|
+
// a test wanting a constant — but the hub generates one by default so
|
|
92
|
+
// nobody has to invent unguessable strings.
|
|
93
|
+
const id = opts?.id ?? newInvitationId();
|
|
94
|
+
const record = state.invitations.create(id, roles, ttlMs);
|
|
95
|
+
await snapshots.flushed();
|
|
96
|
+
return { id: record.id, expiresAt: record.expiresAt };
|
|
97
|
+
},
|
|
98
|
+
async redeem(id) {
|
|
99
|
+
const result = state.invitations.redeem(id);
|
|
100
|
+
// Flush even on refusal: a redemption that FAILED still may have
|
|
101
|
+
// consumed nothing, but one that succeeded must be durable before the
|
|
102
|
+
// caller is told it is a member.
|
|
103
|
+
await snapshots.flushed();
|
|
104
|
+
return result;
|
|
105
|
+
},
|
|
106
|
+
status: (id) => state.invitations.status(id),
|
|
107
|
+
pending: () => state.invitations.pending(),
|
|
108
|
+
},
|
|
109
|
+
members: state.memberStore,
|
|
110
|
+
isMember: (peerId) => state.memberStore.get(peerId) !== undefined,
|
|
111
|
+
revocations,
|
|
112
|
+
rules: init.policies,
|
|
113
|
+
async reset() {
|
|
114
|
+
// Forget every member and every spent invitation id. The next start
|
|
115
|
+
// founds a fresh mesh under the SAME peerId — which is the one thing
|
|
116
|
+
// this cannot undo, and why it is a separate call rather than something
|
|
117
|
+
// `stop()` does.
|
|
118
|
+
for (const record of state.memberStore.list())
|
|
119
|
+
state.memberStore.remove(record.peerId);
|
|
120
|
+
await snapshots.flushed();
|
|
121
|
+
},
|
|
122
|
+
async stop() {
|
|
123
|
+
// Idempotent: an assembly that tears down twice — a page unloading while
|
|
124
|
+
// a test also cleans up — must not see the second call throw.
|
|
125
|
+
if (stopped)
|
|
126
|
+
return;
|
|
127
|
+
stopped = true;
|
|
128
|
+
if (timer != null)
|
|
129
|
+
clearInterval(timer);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hub's `.well-known` surface: registries, invitations, presence and the
|
|
3
|
+
* mesh view. The hub is an ordinary peer (note 03) — nothing here imports
|
|
4
|
+
* libp2p; it is handed a `mintToken` capability already scoped to this
|
|
5
|
+
* peer's own signing key (see `createPeer`'s `mounts` factory in
|
|
6
|
+
* `@statewalker/httpeers-core`), never the key itself.
|
|
7
|
+
*
|
|
8
|
+
* Built on Hono. `app.fetch` is already `(Request) => Promise<Response>`, so
|
|
9
|
+
* it drops straight into the `FetchHandler` contract with no adapter — and
|
|
10
|
+
* `c.req.raw` is the very `Request` the transport (or, in a test, the
|
|
11
|
+
* caller) registered a peer binding on, which is what makes `lookupPeer`
|
|
12
|
+
* and `lookupClaims` work at all from inside a handler.
|
|
13
|
+
*
|
|
14
|
+
* NO QUERY PARAMETERS. Conditional reads use `ETag` / `If-None-Match`, not
|
|
15
|
+
* `?since=`: some transports drop `url.search` before the wire, and ETag is
|
|
16
|
+
* better HTTP regardless.
|
|
17
|
+
*/
|
|
18
|
+
import type { RuleSet } from "@statewalker/httpeers-access";
|
|
19
|
+
import type { RevocationRegistry } from "@statewalker/httpeers-access/issuer";
|
|
20
|
+
import type { FetchHandler, MemberStore, Mounts, PeerIdStr, UsesTransportIdentity } from "@statewalker/httpeers-core";
|
|
21
|
+
import type { InvitationStore } from "./hub-state.js";
|
|
22
|
+
import type { MeshView } from "./mesh-view.js";
|
|
23
|
+
/** The capability that grants admin visibility — sees `hidden` members and gates `/admin/*` (Task 8's `DELETE /admin/members/{peerId}` included). */
|
|
24
|
+
export declare const ADMIN_CAPABILITY = "std:mesh.admin";
|
|
25
|
+
/** Presence TTL: an entry not refreshed within this window is swept. */
|
|
26
|
+
export declare const DEFAULT_PRESENCE_TTL_MS = 15000;
|
|
27
|
+
/** Exactly two handlers, method-aware: a presence WRITE is bootstrap, a read is not. */
|
|
28
|
+
export declare function usesTransportIdentity(): UsesTransportIdentity;
|
|
29
|
+
/**
|
|
30
|
+
* The diagnostic `/test` surface: `/test/whoami` and `/test/echo`, per the
|
|
31
|
+
* archived `12-httpeers-prototype-validated/src/endpoints.ts` (lines 128
|
|
32
|
+
* and 138). NOT hub-specific — every peer in that archive served this
|
|
33
|
+
* unconditionally, hub or not, and Task 7b's promoted E2E suites need the
|
|
34
|
+
* same: their "provider" peer (an ordinary member, never a hub) must answer
|
|
35
|
+
* `/test/whoami` with this rich shape too.
|
|
36
|
+
*
|
|
37
|
+
* This is deliberately NOT `httpeers.core`'s own default diagnostic mount
|
|
38
|
+
* (`peer.ts`'s `defaultMounts`, also `/test/whoami`) — that one answers
|
|
39
|
+
* `{ peer, sub }` from the transport/binding alone and knows nothing about
|
|
40
|
+
* a minted token's `roles`/`mesh`. The archived suites assert `body.you`,
|
|
41
|
+
* `body.roles`, `body.mesh` — fields the core's default does not have and
|
|
42
|
+
* is not meant to grow, since the core must stay hub-agnostic. Any peer
|
|
43
|
+
* that wants this richer surface mounts this handler explicitly, the same
|
|
44
|
+
* way `createHubEndpoints` below does for the hub itself.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createTestSurfaceHandler(selfPeerId: PeerIdStr): FetchHandler;
|
|
47
|
+
export interface HubEndpointsInit {
|
|
48
|
+
selfPeerId: PeerIdStr;
|
|
49
|
+
/**
|
|
50
|
+
* Mint a membership token that self-certifies as this mesh. Structurally
|
|
51
|
+
* `MountsFactoryContext["mintToken"]` from `httpeers.core`, restated here
|
|
52
|
+
* rather than imported so this file stays a plain description of what the
|
|
53
|
+
* hub needs -- including the audience option ADR-0020 added, which nothing
|
|
54
|
+
* in this file passes yet: the invitation and presence protocols mint
|
|
55
|
+
* unrestricted tokens, and narrowing them is a protocol change, not a
|
|
56
|
+
* signature change.
|
|
57
|
+
*/
|
|
58
|
+
mintToken: (sub: string, roles: string[], options?: {
|
|
59
|
+
ttlMs?: number;
|
|
60
|
+
audience?: readonly PeerIdStr[];
|
|
61
|
+
}) => Promise<string>;
|
|
62
|
+
memberStore: MemberStore;
|
|
63
|
+
invitations: InvitationStore;
|
|
64
|
+
/** This mesh's Datalog rules and policies (ADR-0019) — published read-only, and the source of the caller's capabilities in the mesh view. */
|
|
65
|
+
rules: RuleSet;
|
|
66
|
+
revocations: RevocationRegistry;
|
|
67
|
+
/** `kind` -> capability required to see advertisements of that kind. Defaults to `{}` (no kind gated). */
|
|
68
|
+
advertisementAccess?: Record<string, string>;
|
|
69
|
+
presenceTtlMs?: number;
|
|
70
|
+
now?: () => number;
|
|
71
|
+
/**
|
|
72
|
+
* Application mounts served alongside the hub's own `.well-known` surface
|
|
73
|
+
* and `/admin`, behind the same access checks.
|
|
74
|
+
*
|
|
75
|
+
* The prototype hard-mounted a demo `/search` endpoint here, which made the
|
|
76
|
+
* library import the application. A hub is membership and the bulletin
|
|
77
|
+
* board; what else a particular deployment serves is not its business.
|
|
78
|
+
*/
|
|
79
|
+
extraMounts?: Record<string, FetchHandler>;
|
|
80
|
+
/**
|
|
81
|
+
* What this hub advertises about ITSELF on the bulletin board.
|
|
82
|
+
*
|
|
83
|
+
* Posted once at construction, because the hub is the one peer that never
|
|
84
|
+
* sends a heartbeat — and a heartbeat's `advertisements` array is otherwise
|
|
85
|
+
* the only way anything reaches that store. The prototype learned this the
|
|
86
|
+
* hard way: its `/search` mount was served and gated but INVISIBLE, so a
|
|
87
|
+
* consumer filtering `/.well-known/mesh` by `kind` found the image peer and
|
|
88
|
+
* nothing else.
|
|
89
|
+
*
|
|
90
|
+
* A hub that serves a route while withholding its advertisement is a
|
|
91
|
+
* half-wired state, so a deployment passing `extraMounts` almost always
|
|
92
|
+
* wants a matching entry here. These never need withdrawing: `sweep`
|
|
93
|
+
* withdraws only for peers whose PRESENCE expired, and a hub has no presence
|
|
94
|
+
* record of its own.
|
|
95
|
+
*/
|
|
96
|
+
ownAdvertisements?: Array<{
|
|
97
|
+
id: string;
|
|
98
|
+
kind: string;
|
|
99
|
+
title: string;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
export interface HubEndpoints {
|
|
103
|
+
mounts: Mounts;
|
|
104
|
+
/** Expire stale presence and the advertisements that rode along with it; bumps the mesh version only if something actually left. Call on a timer (production) or directly with a controlled clock (tests). */
|
|
105
|
+
sweep: () => void;
|
|
106
|
+
/**
|
|
107
|
+
* The very view `GET /.well-known/mesh` serves, read in-process.
|
|
108
|
+
*
|
|
109
|
+
* FOR A UI THAT IS ALREADY INSIDE THE HUB, and for nothing else. The
|
|
110
|
+
* browser hub page (`../pages/hub/`) renders the member list from this;
|
|
111
|
+
* going through the HTTP endpoint for it would mean the hub minting
|
|
112
|
+
* itself a token and dispatching a request to itself, purely so it could
|
|
113
|
+
* read three registries it is holding two references away. The HTTP
|
|
114
|
+
* endpoint remains the ONLY way any other peer sees this — this accessor
|
|
115
|
+
* adds no route, no capability, and no way in from the network.
|
|
116
|
+
*
|
|
117
|
+
* IT IS `buildMeshView`, NOT A SECOND OPINION. "Saved" (a member record,
|
|
118
|
+
* from the persisted snapshot) and "active" (`online`, from the TTL'd
|
|
119
|
+
* presence store) are two different facts, and `mesh-view.ts` already
|
|
120
|
+
* computes the second per member. A UI that re-derived `online` by
|
|
121
|
+
* joining a member list against a presence list would be inventing a
|
|
122
|
+
* parallel notion that could disagree with the one every remote peer
|
|
123
|
+
* reads — the exact kind of divergence that turns into a debugging trap.
|
|
124
|
+
*
|
|
125
|
+
* THE HUB'S OWN VIEW SEES EVERYTHING: every capability the rules can
|
|
126
|
+
* derive is passed as the caller's, so `hidden` members and every gated
|
|
127
|
+
* advertisement are included. There is nobody to hide from — this is the
|
|
128
|
+
* machine that holds the list, and an operator shown a filtered version
|
|
129
|
+
* of their own mesh would be misled about what they are administering.
|
|
130
|
+
*
|
|
131
|
+
* Live, not a snapshot: a swept peer is `online: false` on the next call,
|
|
132
|
+
* and a removed member is gone from it, which is what a polling UI wants.
|
|
133
|
+
*/
|
|
134
|
+
meshView: () => MeshView;
|
|
135
|
+
}
|
|
136
|
+
export declare function createHubEndpoints(init: HubEndpointsInit): HubEndpoints;
|
|
137
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../src/endpoints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,KAAK,EAGV,YAAY,EACZ,WAAW,EACX,MAAM,EACN,SAAS,EAET,qBAAqB,EACtB,MAAM,4BAA4B,CAAC;AAepC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAwB,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAIrE,qJAAqJ;AACrJ,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AAEjD,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,wFAAwF;AACxF,wBAAgB,qBAAqB,IAAI,qBAAqB,CAM7D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,SAAS,GAAG,YAAY,CAgB5E;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;;;OAQG;IACH,SAAS,EAAE,CACT,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,SAAS,EAAE,CAAA;KAAE,KAC1D,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,eAAe,CAAC;IAC7B,6IAA6I;IAC7I,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,EAAE,kBAAkB,CAAC;IAChC,0GAA0G;IAC1G,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C;;;;;;;;;;;;;;;OAeG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,8MAA8M;IAC9M,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,QAAQ,EAAE,MAAM,QAAQ,CAAC;CAC1B;AA0BD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAuQvE"}
|