@enc-protocol/services 0.9.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 ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Zhenyu Sun, Tomoya Nagasawa, and WEAVEDB LTD.
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # @enc-protocol/services
2
+
3
+ Platform-agnostic generic protocol services that sit between per-app SDKs and platform adapters. Provides identity & enclave registration, cross-enclave profiles, peer messaging, event subscription with decryption, and dynamic enclave provisioning.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @enc-protocol/services
9
+ ```
10
+
11
+ ## API
12
+
13
+ ### Registry
14
+
15
+ Class for managing three independent registration sub-apps (identities, enclaves, nodes) that share one registry enclave. Delegates lookups to a `RegistryStrategyFn` plugin or falls back to HTTP dataview + event log traversal.
16
+
17
+ **`Registry` constructor**
18
+ ```typescript
19
+ new Registry(opts?: {
20
+ adapter?: Adapter,
21
+ identitiesAdapter?: Adapter,
22
+ enclavesAdapter?: Adapter,
23
+ nodesAdapter?: Adapter,
24
+ plugins?: ClientPluginRegistry
25
+ })
26
+ ```
27
+
28
+ **`registry.identities`** — Sub-app for identity records (pubkey → app enclaves map).
29
+ - `lookup(id_pub)` → Promise resolves record or null; caches results
30
+ - `publish(record)` → Promise submits identity event
31
+ - `list(opts?: {limit?})` → Promise resolves array of all identities
32
+ - `resolveEnclave(pubKey, appName)` → Promise resolves `{ok, enclave?, error?}`
33
+
34
+ **`registry.enclaves`** — Sub-app for enclave directory.
35
+ - `lookup(enclave_id)` → Promise resolves record or null
36
+ - `publish(record)` → Promise submits enclave event
37
+ - `list(opts?: {limit?})` → Promise resolves array of all enclaves
38
+
39
+ **`registry.nodes`** — Sub-app for node directory.
40
+ - `lookup(seq_pub)` → Promise resolves record or null
41
+ - `publish(record)` → Promise submits node event
42
+ - `list(opts?: {limit?})` → Promise resolves array of all nodes
43
+
44
+ **`registry.setAdapter(adapter)`** — Point all three sub-apps at the same adapter.
45
+
46
+ **`registry.setPlugins(plugins)`** — Wire all three sub-apps to the `RegistryStrategyFn` plugin.
47
+
48
+ ### Profiles
49
+
50
+ Class for cross-enclave profile resolution. Profiles live in each user's Personal enclave under a `Shared(profile)` slot. Delegates to `ProfileResolverFn` plugin or falls back to synchronous cache lookup + HTTP dataview fetch.
51
+
52
+ **`Profiles` constructor**
53
+ ```typescript
54
+ new Profiles(opts?: {
55
+ personalAdapter?: Adapter,
56
+ dataviewUrl?: string,
57
+ plugins?: ClientPluginRegistry
58
+ })
59
+ ```
60
+
61
+ **`profiles.get(pubKey)`** — Promise resolves profile object for the given pubkey, or null if not found. Caches results.
62
+
63
+ **`profiles.set(profile)`** — Promise submits profile to the personal adapter's `Shared(profile)` slot.
64
+
65
+ **`profiles.nameFor(pubKey)`** — Synchronous lookup of profile name from cache. Returns first of `name` or `display_name` fields, or first 8 chars of pubkey as fallback.
66
+
67
+ **`profiles.setPersonalAdapter(adapter)`** — Point at a personal enclave adapter.
68
+
69
+ **`profiles.setDataviewUrl(url)`** — Set HTTP dataview base URL for profile queries.
70
+
71
+ ### PeerMessaging
72
+
73
+ Class for submitting events into a peer's enclave rather than the user's own. Routes via `PeerRoutingFn` plugin or falls back to registry lookup + optional encryption + adapter.toEnclave.
74
+
75
+ **`PeerMessaging` constructor**
76
+ ```typescript
77
+ new PeerMessaging(opts?: {
78
+ adapter?: Adapter,
79
+ registry?: Registry,
80
+ encrypted?: string[],
81
+ eventToDataType?: {[event: string]: string},
82
+ sdk?: any,
83
+ plugins?: ClientPluginRegistry
84
+ })
85
+ ```
86
+
87
+ **`peer.submit(peerPub, targetAppName, event, content, cryptoOpts?)`** — Promise<{ok, error?, receipt?, target?}> submits an event to peer's enclave. Resolves peer's enclave_id via registry, optionally encrypts content, and routes through adapter.toEnclave.
88
+
89
+ ### Subscriber
90
+
91
+ Class wrapping adapter.subscribe with optional per-event decryption. Decrypts content for events in `encrypted` set using sdk._decrypt, sdk.crypto.decrypt, or `EnvelopeDecryptFn` plugin.
92
+
93
+ **`Subscriber` constructor**
94
+ ```typescript
95
+ new Subscriber(opts?: {
96
+ adapter?: Adapter,
97
+ encrypted?: string[],
98
+ eventToDataType?: {[event: string]: string},
99
+ sdk?: any,
100
+ plugins?: ClientPluginRegistry
101
+ })
102
+ ```
103
+
104
+ **`subscriber.subscribe(callback)`** — Returns unsubscribe function. Calls callback with each event from adapter, auto-decrypting encrypted event types. Callback receives `{type, content, from, _encrypted?, ...}`.
105
+
106
+ ### EnclaveProvisioner
107
+
108
+ Class for minting enclaves on demand with timeout. Delegates to `ProvisionerFn` plugin or falls back to adapter.createEnclave with configurable timeout.
109
+
110
+ **`EnclaveProvisioner` constructor**
111
+ ```typescript
112
+ new EnclaveProvisioner(opts?: {
113
+ adapter?: Adapter,
114
+ timeoutMs?: number,
115
+ plugins?: ClientPluginRegistry
116
+ })
117
+ ```
118
+
119
+ **`provisioner.mint(rbacManifest)`** — Promise resolves enclave creation result. Throws on timeout (default 5000ms) or if adapter has no createEnclave method.
120
+
121
+ ### Schema
122
+
123
+ **`flattenEnclaveManifest(raw, opts?)`** — Transforms authored enclave JSON manifest into normalized `{schema, states, traits, initEntries, ...}` shape expected by adapters. Delegates to `ManifestNormalizerFn` plugin (default from `@enc-protocol/plugin-client-base/normalizer`).
124
+
125
+ **`setManifestNormalizer(fn)`** — Swap the active normalizer implementation. Pass `null` to restore the default.
126
+
127
+ ### Legacy Shim
128
+
129
+ **`createLegacyShim(opts)`** — Bridges typed SDK + protocol services to legacy impl-web renderer surface. Returns an object with:
130
+ - Dynamic `submit_<event>(content, cryptoOpts?)` methods for each write event
131
+ - Dynamic `query_<table>(queryOpts?)` methods for each read table
132
+ - `submit_to_peer(peerPub, targetAppName, event, content, cryptoOpts?)` — delegates to peer.submit
133
+ - `create_enclave(rbac)` — delegates to provisioner.mint
134
+ - `subscribe(cb)` — delegates to subscriber.subscribe
135
+ - `identities`, `enclaves`, `nodes` — direct sub-app access
136
+ - `profiles` — direct profiles access
137
+ - `query_events(eventType, opts?)` — raw event query
138
+ - `_events`, `_queries`, `_encrypted` — metadata arrays for renderers
139
+
140
+ ### Composition
141
+
142
+ **`composeSdk(opts)`** — Construct typed SDK + all protocol services + legacy shim in one call.
143
+
144
+ ```typescript
145
+ composeSdk({
146
+ SdkClass: HelloSdk,
147
+ adapter: myAdapter,
148
+ crypto?: {encrypt, decrypt},
149
+ infra?: manifestWithEndpoints,
150
+ identity?: overrideFromAddress,
151
+ plugins?: ClientPluginRegistry
152
+ })
153
+ ```
154
+
155
+ Returns the legacy-shim wrapper. All SDK + services share one plugin registry, so plugin overrides apply consistently across encryption, routing, provisioning, etc. Throws if adapter is missing; returns null if SdkClass is null.
156
+
157
+ ## Example
158
+
159
+ ```javascript
160
+ import { composeSdk, Registry, Profiles, PeerMessaging } from '@enc-protocol/services'
161
+ import { HelloSdk } from '@enc-protocol/hello-sdk'
162
+ import { MemoryAdapter } from '@enc-protocol/adapters'
163
+
164
+ const adapter = new MemoryAdapter()
165
+ const registryAdapter = new MemoryAdapter()
166
+
167
+ // Option 1: Compose everything at once
168
+ const sdk = composeSdk({
169
+ SdkClass: HelloSdk,
170
+ adapter,
171
+ crypto: { encrypt, decrypt }
172
+ })
173
+
174
+ // Option 2: Assemble services separately
175
+ const registry = new Registry({ adapter: registryAdapter })
176
+ const profiles = new Profiles({ personalAdapter: adapter })
177
+ const peer = new PeerMessaging({ adapter, registry })
178
+
179
+ // Use the registry
180
+ const identity = await registry.identities.lookup('0x1234...')
181
+ await registry.identities.publish({ id_pub: '0x1234...', enclaves: { hello: 'encl_xyz' } })
182
+
183
+ // Cross-enclave profiles
184
+ const profile = await profiles.get('0x5678...')
185
+ await profiles.set({ name: 'Alice', display_name: 'alice.eth' })
186
+
187
+ // Send to peer's enclave
188
+ const result = await peer.submit('0x9abc...', 'hello', 'message', {body: 'hi'})
189
+
190
+ // Subscribe to events
191
+ const unsubscribe = sdk.subscribe(event => console.log(event))
package/compose.mjs ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * composeSdk — construct typed per-app SDK + protocol-runtime services
3
+ * + legacy shim in one call.
4
+ *
5
+ * import { composeSdk } from '@enc-protocol/services'
6
+ * const sdk = composeSdk({ SdkClass: HelloSdk, adapter, crypto, infra })
7
+ *
8
+ * Returns the legacy-shim wrapper exposing both typed methods
9
+ * (sdk.submitMessages) AND the legacy renderer surface (sdk.submit_post,
10
+ * sdk.profiles.*, sdk.identities.*, sdk.subscribe, ...).
11
+ *
12
+ * The SDK + every protocol service share one ClientPluginRegistry so
13
+ * plugin overrides apply consistently (e.g. swapping
14
+ * EnvelopeEncryptFn rebinds encryption for both Sdk._encrypt and
15
+ * PeerMessaging.submit).
16
+ */
17
+
18
+ import { Registry } from './registry.mjs'
19
+ import { Profiles } from './profiles.mjs'
20
+ import { PeerMessaging } from './peer.mjs'
21
+ import { Subscriber } from './subscriber.mjs'
22
+ import { EnclaveProvisioner } from './provisioner.mjs'
23
+ import { createLegacyShim } from './legacy-shim.mjs'
24
+
25
+ /**
26
+ * @param {object} opts
27
+ * SdkClass the per-app SDK class (HelloSdk / DmSdk / ...). If null, returns null.
28
+ * adapter the network/browser/memory adapter
29
+ * crypto? optional encrypt/decrypt hook bag (set as sdk.crypto)
30
+ * infra? optional infra manifest (passes through to shim for endpoints metadata)
31
+ * identity? optional identity override (used by AppClient for dataview "from")
32
+ * plugins? ClientPluginRegistry — if omitted, sdk.client builds its own default
33
+ */
34
+ export function composeSdk(opts = {}) {
35
+ const { SdkClass, adapter, crypto, infra, identity, plugins } = opts
36
+ if (!SdkClass) return null
37
+ if (!adapter) throw new Error('composeSdk: adapter required')
38
+
39
+ const sdk = new SdkClass({ adapter, identity, plugins })
40
+ if (crypto) sdk.crypto = crypto
41
+
42
+ // Share the SDK's plugin registry with every protocol service.
43
+ const sharedPlugins = sdk.client?.plugins || plugins || null
44
+
45
+ const registry = new Registry({ plugins: sharedPlugins })
46
+ const profiles = new Profiles({ plugins: sharedPlugins })
47
+ const peer = new PeerMessaging({ adapter, registry, sdk, plugins: sharedPlugins })
48
+ const subscriber = new Subscriber({ adapter, sdk, plugins: sharedPlugins })
49
+ const provisioner = new EnclaveProvisioner({ adapter, plugins: sharedPlugins })
50
+
51
+ return createLegacyShim({
52
+ sdk, registry, profiles, peer, subscriber, provisioner,
53
+ infra: infra?.manifest || infra,
54
+ })
55
+ }
package/index.mjs ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @enc-protocol/services — generic protocol services
3
+ *
4
+ * Platform-agnostic classes every ENC client uses on top of a per-app
5
+ * SDK (@enc-protocol/<app>-cli):
6
+ *
7
+ * Registry — identities/enclaves/nodes sub-apps
8
+ * Profiles — cross-enclave profile resolution
9
+ * PeerMessaging — submit_to_peer routing
10
+ * Subscriber — adapter.subscribe wrapper with decrypt
11
+ * EnclaveProvisioner — mint enclaves on demand
12
+ *
13
+ * Plus:
14
+ *
15
+ * flattenEnclaveManifest — turn authored enclave JSON into the
16
+ * { schema, states, traits, initEntries, ... }
17
+ * shape PureAdapter + cf-mint expect
18
+ *
19
+ * createLegacyShim — bridge to expose legacy submit_<event> /
20
+ * query_<table> shape over typed SDK +
21
+ * services. impl-web's app-runtime.ts uses
22
+ * the shim unchanged.
23
+ */
24
+
25
+ export { Registry } from './registry.mjs'
26
+ export { Profiles } from './profiles.mjs'
27
+ export { PeerMessaging } from './peer.mjs'
28
+ export { Subscriber } from './subscriber.mjs'
29
+ export { EnclaveProvisioner } from './provisioner.mjs'
30
+ export { flattenEnclaveManifest } from './schema.mjs'
31
+ export { createLegacyShim } from './legacy-shim.mjs'
32
+ export { composeSdk } from './compose.mjs'
@@ -0,0 +1,219 @@
1
+ /**
2
+ * createLegacyShim — exposes the legacy impl-web SDK surface
3
+ * (submit_<event>, query_<table>, sdk.profiles, sdk.identities, ...)
4
+ * over a typed per-app SDK + the platform-agnostic protocol-runtime
5
+ * services.
6
+ *
7
+ * This is the bridge that keeps impl-web's `app-runtime.ts` working
8
+ * unchanged while making the per-app SDK + protocol-runtime the source
9
+ * of truth.
10
+ *
11
+ * const sdk = new HelloSdk({ adapter })
12
+ * const registry = new Registry({ adapter: registryAdapter })
13
+ * const profiles = new Profiles({ personalAdapter, dataviewUrl })
14
+ * const legacy = createLegacyShim({ sdk, registry, profiles, peer, subscriber, provisioner })
15
+ *
16
+ * // legacy.submit_post(content) → sdk.submit('post', content)
17
+ * // legacy.query_messages() → sdk.query('messages')
18
+ * // legacy.identities.lookup → registry.identities.lookup
19
+ * // legacy.profiles.get → profiles.get
20
+ * // legacy.subscribe(cb) → subscriber.subscribe(cb)
21
+ * // legacy.submit_to_peer(...) → peer.submit(...)
22
+ * // legacy.create_enclave(...) → provisioner.mint(...)
23
+ *
24
+ * Inserts metadata `_events`, `_queries`, `_encrypted` arrays that
25
+ * impl-web's renderer reads.
26
+ */
27
+
28
+ export function createLegacyShim({ sdk, registry, profiles, peer, subscriber, provisioner, infra }) {
29
+ const schema = sdk.schema || {}
30
+ const tableMap = schema.tableMap || {}
31
+ // `schema.encrypt` is the data_type→plugin map (e.g. {messages:'ratchet-pair'});
32
+ // encryptSet is the SET of encrypted data_types. Take its keys. Tolerate the
33
+ // legacy array form ([data_type,...]) too. `new Set(object)` would throw
34
+ // "object is not iterable" — the `|| []` only guards null/undefined.
35
+ const encryptSet = new Set(Array.isArray(schema.encrypt) ? schema.encrypt : Object.keys(schema.encrypt || {}))
36
+
37
+ // Reverse tableMap: event → first matching data_type (for encryption lookup).
38
+ const eventToTables = {}
39
+ for (const [table, event] of Object.entries(tableMap)) {
40
+ ;(eventToTables[event] ||= []).push(table)
41
+ }
42
+
43
+ // Derive write events from schema.data_types (same rule as flow.json):
44
+ const writeTables = Object.keys(schema.data_types || {})
45
+ const writeEvents = new Set()
46
+ for (const table of writeTables) {
47
+ const event = tableMap[table] || table.replace(/s$/, '')
48
+ writeEvents.add(event)
49
+ }
50
+
51
+ const readTables = Object.keys(schema.reads || {}).filter(t => !t.startsWith('_'))
52
+
53
+ // The shim itself — a plain object that delegates to the typed SDK +
54
+ // runtime services. Lifecycle: any mutation of the underlying SDK's
55
+ // adapter propagates through transparently because every method reads
56
+ // `sdk.client` / `sdk.opts` at call time.
57
+ const shim = {
58
+ _sdk: sdk,
59
+ _registry: registry,
60
+ _profiles: profiles,
61
+ _peer: peer,
62
+ _subscriber: subscriber,
63
+ _provisioner: provisioner,
64
+
65
+ // Metadata for app-runtime.ts:
66
+ _events: [...writeEvents],
67
+ _queries: readTables.length > 0
68
+ ? readTables
69
+ : (infra?.endpoints || []).filter(e => !e.name.startsWith('_')).map(e => e.name),
70
+ _encrypted: [...encryptSet],
71
+ _appId: sdk.appId,
72
+ _adapter: getPrimaryAdapter(sdk),
73
+
74
+ // Generic services
75
+ identities: registry?.identities,
76
+ enclaves: registry?.enclaves,
77
+ nodes: registry?.nodes,
78
+ profiles,
79
+
80
+ // Back-compat alias used by older callers (ui-kit + emulator).
81
+ registry: {
82
+ setAdapter(a) {
83
+ registry?.identities.setAdapter(a)
84
+ registry?.enclaves.setAdapter(a)
85
+ registry?.nodes.setAdapter(a)
86
+ },
87
+ publish: (...args) => registry?.identities.publish(...args),
88
+ lookup: (...args) => registry?.identities.lookup(...args),
89
+ },
90
+
91
+ // Crypto hook passthrough — apps wire encrypt/decrypt at the SDK
92
+ // class level via _encrypt; legacy callers expect `sdk.crypto`.
93
+ get crypto() { return sdk.crypto || null },
94
+ set crypto(c) { sdk.crypto = c },
95
+
96
+ // Generic primitives
97
+ submit: (name, args, opts) => sdk.submit(name, args, opts),
98
+ query: (name) => sdk.query(name),
99
+
100
+ query_events: async (eventType, opts = {}) => {
101
+ const adapter = getPrimaryAdapter(sdk)
102
+ return adapter?.query(eventType, opts) || []
103
+ },
104
+
105
+ subscribe: (cb) => subscriber?.subscribe(cb) || (() => {}),
106
+
107
+ create_enclave: async (rbac) => provisioner?.mint(rbac),
108
+
109
+ submit_to_peer: async (peerPub, targetAppName, event, content, cryptoOpts) =>
110
+ peer?.submit(peerPub, targetAppName, event, content, cryptoOpts)
111
+ || { ok: false, error: 'no_peer_messaging' },
112
+ }
113
+
114
+ // Dynamic submit_<event> methods — one per write event the schema declares.
115
+ for (const event of writeEvents) {
116
+ shim['submit_' + event] = async (content, cryptoOpts) => {
117
+ let payload = content
118
+ const dataTypes = eventToTables[event] || [event]
119
+ const isEncrypted = dataTypes.some(t => encryptSet.has(t))
120
+ if (isEncrypted && sdk._encrypt) {
121
+ const dt = dataTypes.find(t => encryptSet.has(t)) || event
122
+ payload = await sdk._encrypt(dt, content, cryptoOpts)
123
+ }
124
+ const adapter = getPrimaryAdapter(sdk)
125
+ const str = typeof payload === 'string' ? payload : JSON.stringify(payload)
126
+ if (typeof globalThis !== 'undefined' && globalThis.__encDebugShim) {
127
+ console.log(`[shim] submit_${event} adapter.enclaveId=${adapter?.enclaveId || 'NULL'} ctor=${adapter?.constructor?.name}`)
128
+ }
129
+ const r = await adapter.submit(event, str)
130
+ if (typeof globalThis !== 'undefined' && globalThis.__encDebugShim) {
131
+ console.log(`[shim] submit_${event}(${str.slice(0, 60)}) → ${JSON.stringify(r)}`)
132
+ }
133
+ return r
134
+ }
135
+ }
136
+
137
+ // Dynamic query_<table> methods — one per read.
138
+ // The renderer expects FLATTENED rows: each row's content fields lifted
139
+ // to the row level, plus from/_event_id/_timestamp. Matches the legacy
140
+ // createSDK shape so `#list[0].body` resolves from the parsed `body`
141
+ // field, not from a nested `parsed.body`.
142
+ const flattenEvents = async (events, eventName, encrypted) => {
143
+ if (!Array.isArray(events)) return []
144
+ const decrypt = sdk._decrypt || sdk.crypto?.decrypt
145
+ return Promise.all(events.map(async ev => {
146
+ let c = ev.parsed
147
+ if (c == null) {
148
+ try { c = typeof ev.content === 'string' ? JSON.parse(ev.content) : (ev.content || {}) } catch { c = {} }
149
+ }
150
+ if (encrypted && decrypt && (c?.encrypted || c?.ciphertext)) {
151
+ try { c = await decrypt.call(sdk, eventName, c, ev.from) } catch { /* keep raw */ }
152
+ }
153
+ return { ...c, from: ev.from, _event_id: ev.id, _timestamp: ev.timestamp }
154
+ }))
155
+ }
156
+
157
+ for (const table of readTables) {
158
+ const eventName = tableMap[table] || table.replace(/s$/, '')
159
+ const isEncrypted = (eventToTables[eventName] || [table]).some(t => encryptSet.has(t))
160
+ shim['query_' + table] = async (queryOpts = {}) => {
161
+ // Cross-enclave dataview reads return already-flattened rows;
162
+ // pass through. Otherwise enclave event-walk + flatten.
163
+ const read = schema.reads?.[table]
164
+ if (read?.cross_enclave) {
165
+ try { return await sdk.query(table) } catch { return [] }
166
+ }
167
+ // Multi-enclave routing: if this event lives in a non-primary
168
+ // enclave (e.g. Super's moments → Personal.public when primary
169
+ // is DM), the primary adapter doesn't have a path to it. Look up
170
+ // the secondary's dataview URL via globalThis.__encConfig and
171
+ // override the adapter call with that URL. The MemoryAdapter
172
+ // ignores this override (its queryEndpoint walks all enclaves
173
+ // anyway); the BrowserAdapter respects it via the new
174
+ // `_dataviewUrl` opt.
175
+ const targetEnclave = sdk._eventToEnclave?.get?.(eventName)
176
+ const adapter = getPrimaryAdapter(sdk)
177
+ let dvOverride = null
178
+ if (targetEnclave && typeof globalThis !== 'undefined') {
179
+ const cfg = globalThis.__encConfig?.dataviews
180
+ if (cfg && cfg[targetEnclave]) dvOverride = cfg[targetEnclave]
181
+ }
182
+ if (adapter?.queryEndpoint && (adapter?.dataviewUrl || dvOverride)) {
183
+ return adapter.queryEndpoint('/' + table, { ...queryOpts, _eventName: eventName, _dataviewUrl: dvOverride })
184
+ }
185
+ const events = await (adapter?.query(eventName, queryOpts) || [])
186
+ const rows = await flattenEvents(events, eventName, isEncrypted)
187
+ if (typeof globalThis !== 'undefined' && globalThis.__encDebugShim) {
188
+ console.log(`[shim] query_${table}(${eventName}) → ${rows.length} rows; first=${JSON.stringify(rows[0]).slice(0, 80)}`)
189
+ }
190
+ return rows
191
+ }
192
+ }
193
+
194
+ // Endpoint fallbacks (apps without a dataview).
195
+ if (readTables.length === 0) {
196
+ for (const ep of (infra?.endpoints || []).filter(e => !e.name.startsWith('_'))) {
197
+ shim['query_' + ep.name] = async (queryOpts = {}) => {
198
+ const adapter = getPrimaryAdapter(sdk)
199
+ if (adapter?.dataviewUrl && adapter.queryEndpoint) return adapter.queryEndpoint(ep.path, queryOpts)
200
+ const eventName = [...writeEvents][0]
201
+ if (!eventName) return []
202
+ const events = await (adapter?.query(eventName, queryOpts) || [])
203
+ return flattenEvents(events, eventName, false)
204
+ }
205
+ }
206
+ }
207
+
208
+ return shim
209
+ }
210
+
211
+ function getPrimaryAdapter(sdk) {
212
+ // The "primary" adapter is the first enclave's adapter — used for
213
+ // event subscribe, query_events, dynamic submit_<event>. Matches the
214
+ // impl-web sdk-gen convention.
215
+ const enclaves = sdk.client?.enclaves
216
+ if (!enclaves) return null
217
+ for (const [, e] of enclaves) return e.adapter
218
+ return null
219
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@enc-protocol/services",
3
+ "version": "0.9.0",
4
+ "type": "module",
5
+ "description": "Platform-agnostic generic protocol services that sit between per-app SDKs (@enc-protocol/<app>-sdk) and platform adapters: Registry, Profiles, PeerMessaging, Subscriber, EnclaveProvisioner, plus a legacy-shim + composeSdk one-liner that wires everything together.",
6
+ "main": "./index.mjs",
7
+ "files": [
8
+ "index.mjs",
9
+ "registry.mjs",
10
+ "profiles.mjs",
11
+ "peer.mjs",
12
+ "subscriber.mjs",
13
+ "provisioner.mjs",
14
+ "schema.mjs",
15
+ "legacy-shim.mjs",
16
+ "compose.mjs",
17
+ "README.md"
18
+ ],
19
+ "exports": {
20
+ ".": "./index.mjs",
21
+ "./registry": "./registry.mjs",
22
+ "./profiles": "./profiles.mjs",
23
+ "./peer": "./peer.mjs",
24
+ "./subscriber": "./subscriber.mjs",
25
+ "./provisioner": "./provisioner.mjs",
26
+ "./schema": "./schema.mjs",
27
+ "./legacy-shim": "./legacy-shim.mjs",
28
+ "./compose": "./compose.mjs"
29
+ },
30
+ "keywords": [
31
+ "enc-protocol",
32
+ "runtime",
33
+ "registry",
34
+ "profiles",
35
+ "platform-agnostic",
36
+ "core",
37
+ "public"
38
+ ],
39
+ "dependencies": {
40
+ "@enc-protocol/plugin-runtime": "^0.9.0",
41
+ "@enc-protocol/plugin-client-base": "^0.9.0"
42
+ },
43
+ "license": "Apache-2.0",
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org/",
46
+ "access": "public"
47
+ }
48
+ }
package/peer.mjs ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * PeerMessaging — submit an event into a peer's enclave rather than the
3
+ * user's own.
4
+ *
5
+ * Delegates to the `PeerRoutingFn` plugin from the ClientPluginRegistry.
6
+ * Default impl preserves legacy behavior: resolve peer enclave via
7
+ * registry, optionally encrypt via the SDK's _encrypt hook, dispatch
8
+ * through adapter.toEnclave.
9
+ */
10
+
11
+ export class PeerMessaging {
12
+ constructor(opts = {}) {
13
+ this.adapter = opts.adapter
14
+ this.registry = opts.registry
15
+ this.encrypted = new Set(opts.encrypted || [])
16
+ this.eventToDataType = opts.eventToDataType || {}
17
+ this._sdk = opts.sdk || null
18
+ this._plugins = opts.plugins || this._sdk?.client?.plugins || null
19
+ }
20
+
21
+ /** Per-event encrypt hook lookup. Plugin-first, SDK override fallback. */
22
+ async _maybeEncrypt(event, content, cryptoOpts) {
23
+ if (!this.encrypted.has(event)) return content
24
+ const dataType = this.eventToDataType[event] || event
25
+ if (this._sdk?._encrypt) {
26
+ return this._sdk._encrypt(dataType, content, cryptoOpts)
27
+ }
28
+ if (this._plugins?.has?.('EnvelopeEncryptFn')) {
29
+ return this._plugins.invoke('EnvelopeEncryptFn', dataType, content, cryptoOpts)
30
+ }
31
+ return content
32
+ }
33
+
34
+ /**
35
+ * Submit to peer's enclave.
36
+ * @param {string} peerPub target pubkey
37
+ * @param {string} targetAppName app name (used to resolve which of peer's enclaves)
38
+ * @param {string} event enclave event name
39
+ * @param {object|string} content payload
40
+ * @param {object} [cryptoOpts] extra opts for _encrypt
41
+ * @returns {Promise<{ok, error?, receipt?, target?}>}
42
+ */
43
+ async submit(peerPub, targetAppName, event, content, cryptoOpts) {
44
+ const routingFn = this._plugins?.get?.('PeerRoutingFn')
45
+ const maybeEncrypt = (ev, c, opts) => this._maybeEncrypt(ev, c, opts)
46
+ if (routingFn) {
47
+ return routingFn({
48
+ peerPub, targetAppName, event, content,
49
+ adapter: this.adapter, registry: this.registry,
50
+ cryptoOpts, maybeEncrypt,
51
+ })
52
+ }
53
+
54
+ // Fallback path (no plugin bound) — legacy inline behavior.
55
+ if (!this.registry?.identities?.resolveEnclave) {
56
+ return { ok: false, error: 'no_registry' }
57
+ }
58
+ let resolved
59
+ try {
60
+ resolved = await this.registry.identities.resolveEnclave(peerPub, targetAppName)
61
+ } catch (e) {
62
+ return { ok: false, error: 'resolve_threw', cause: e?.message || e }
63
+ }
64
+ if (!resolved.ok) return { ok: false, error: resolved.reason, ...resolved }
65
+ if (!this.adapter?.toEnclave) return { ok: false, error: 'adapter_no_retarget' }
66
+ const payload = await maybeEncrypt(event, content, { ...cryptoOpts, recipientPub: peerPub })
67
+ // strict-wire {__withTags, content, tags}: the runtime's pre-encrypt step
68
+ // (buildCrypto dm:invite / personal:notice) hands us a wrapper object. Submit
69
+ // the BARE content (base64) — the receiver's strict-string decrypt gate rejects
70
+ // anything starting with '{', so shipping the JSON wrapper means the peer never
71
+ // decrypts the invite/notice. Tags are routing fallbacks (to/enclave_id/epoch);
72
+ // the decrypted content carries the app-level fields.
73
+ const str =
74
+ payload && typeof payload === 'object' && payload.__withTags === true
75
+ ? typeof payload.content === 'string'
76
+ ? payload.content
77
+ : JSON.stringify(payload.content)
78
+ : typeof payload === 'string'
79
+ ? payload
80
+ : JSON.stringify(payload)
81
+ const peerAdapter = this.adapter.toEnclave(resolved.enclave)
82
+ const res = await peerAdapter.submit(event, str)
83
+ if (res?.error || res?.type === 'Error') {
84
+ return { ok: false, error: res.error || res.code || 'submit_failed', details: res }
85
+ }
86
+ return { ok: true, receipt: res, target: resolved.enclave }
87
+ }
88
+ }
package/profiles.mjs ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Profiles — cross-enclave profile resolution.
3
+ *
4
+ * Profiles live in each user's Personal enclave under a `Shared(profile)`
5
+ * slot. Personal is OWNER-read, so other apps read via the Personal
6
+ * dataview which aggregates profile pushes from every user's Personal
7
+ * enclave keyed by sender pubkey.
8
+ *
9
+ * Delegates to the `ProfileResolverFn` plugin from the
10
+ * ClientPluginRegistry. Default impl preserves legacy behavior:
11
+ * synchronous mem-tier getProfile first, fall back to HTTP dataview.
12
+ */
13
+
14
+ export class Profiles {
15
+ constructor(opts = {}) {
16
+ this._personalAdapter = opts.personalAdapter || null
17
+ this._dataviewUrl = opts.dataviewUrl || null
18
+ this._cache = new Map()
19
+ this._plugins = opts.plugins || null
20
+ }
21
+
22
+ setPersonalAdapter(adapter) { this._personalAdapter = adapter }
23
+ setDataviewUrl(url) { this._dataviewUrl = url }
24
+
25
+ async set(profile) {
26
+ if (!this._personalAdapter) return null
27
+ return this._personalAdapter.submit('Shared(profile)', JSON.stringify({ value: profile }))
28
+ }
29
+
30
+ async get(pubKey) {
31
+ const norm = (pubKey || '').toLowerCase()
32
+ if (this._cache.has(norm)) return this._cache.get(norm)
33
+ if (!pubKey) return null
34
+
35
+ const resolveFn = this._plugins?.get?.('ProfileResolverFn')
36
+ if (resolveFn) {
37
+ const row = await resolveFn(this._personalAdapter, this._dataviewUrl, pubKey)
38
+ if (row) { this._cache.set(norm, row); return row }
39
+ return null
40
+ }
41
+
42
+ // Fallback inline behavior preserved.
43
+ if (typeof this._personalAdapter?.getProfile === 'function') {
44
+ const row = this._personalAdapter.getProfile(pubKey)
45
+ if (row) { this._cache.set(norm, row); return row }
46
+ return null
47
+ }
48
+ if (!this._dataviewUrl) return null
49
+ try {
50
+ const url = this._dataviewUrl.replace(/\/+$/, '') + '/profiles?id_pub=' + encodeURIComponent(pubKey)
51
+ const res = await fetch(url)
52
+ if (!res.ok) return null
53
+ const body = await res.json()
54
+ const row = (body.profiles || [])[0]
55
+ if (row) { this._cache.set(norm, row); return row }
56
+ return null
57
+ } catch { return null }
58
+ }
59
+
60
+ nameFor(pubKey) {
61
+ const hit = this._cache.get((pubKey || '').toLowerCase())
62
+ return hit?.name || hit?.display_name || pubKey?.slice(0, 8) || ''
63
+ }
64
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * EnclaveProvisioner — mint enclaves on demand + rebind active adapter.
3
+ *
4
+ * Delegates to the `ProvisionerFn` plugin from the ClientPluginRegistry.
5
+ * Default impl preserves legacy behavior: calls adapter.createEnclave
6
+ * with a timeout.
7
+ */
8
+
9
+ export class EnclaveProvisioner {
10
+ constructor(opts = {}) {
11
+ this.adapter = opts.adapter
12
+ this.timeoutMs = opts.timeoutMs || 5000
13
+ this._plugins = opts.plugins || null
14
+ }
15
+
16
+ async mint(rbacManifest) {
17
+ const provisionFn = this._plugins?.get?.('ProvisionerFn')
18
+ if (provisionFn) {
19
+ return provisionFn(this.adapter, rbacManifest, { timeoutMs: this.timeoutMs })
20
+ }
21
+ // Fallback inline behavior (preserved).
22
+ if (!this.adapter?.createEnclave) throw new Error('provisioner: adapter has no createEnclave')
23
+ const result = await Promise.race([
24
+ this.adapter.createEnclave(rbacManifest),
25
+ new Promise((_, rej) => setTimeout(() => rej(new Error('createEnclave timeout')), this.timeoutMs)),
26
+ ])
27
+ return result
28
+ }
29
+ }
package/registry.mjs ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Registry — three independent registration sub-apps that share one
3
+ * enclave. Each sub-app is keyed by a different field:
4
+ *
5
+ * identities → key=id_pub (pubkey → { enclaves: {<app>: <id>, ...} })
6
+ * enclaves → key=enclave_id (enclave directory)
7
+ * nodes → key=seq_pub (node directory)
8
+ *
9
+ * Lookup is delegated to the `RegistryStrategyFn` plugin from the
10
+ * ClientPluginRegistry. The default impl is identical to the legacy
11
+ * inline behavior: dataview HTTP GET first, fall back to event log
12
+ * filtered by signer.
13
+ */
14
+
15
+ function makeRegistrySubApp(eventName, { keyField, table } = {}) {
16
+ return {
17
+ _adapter: null,
18
+ _cache: new Map(),
19
+ _strategyFn: null,
20
+ setAdapter(adapter) { this._adapter = adapter },
21
+ setStrategy(strategyFn) { this._strategyFn = strategyFn },
22
+
23
+ async publish(record) {
24
+ if (!this._adapter) return null
25
+ return this._adapter.submit(eventName, JSON.stringify(record))
26
+ },
27
+
28
+ async lookup(key) {
29
+ if (this._strategyFn) {
30
+ return this._strategyFn({
31
+ adapter: this._adapter,
32
+ table,
33
+ keyField,
34
+ key,
35
+ cache: this._cache,
36
+ })
37
+ }
38
+ // Fallback inline behavior (kept verbatim in case no plugin bound).
39
+ const norm = (key || '').toLowerCase()
40
+ if (!this._adapter || !key) return this._cache.get(norm) || null
41
+ if (this._adapter.queryEndpoint && table && keyField) {
42
+ try {
43
+ const rows = await this._adapter.queryEndpoint(`/${table}?${keyField}=${encodeURIComponent(key)}`)
44
+ if (Array.isArray(rows) && rows.length > 0) {
45
+ const row = { ...rows[0] }
46
+ for (const [k, v] of Object.entries(row)) {
47
+ if (typeof v === 'string' && v.length > 1 && (v[0] === '{' || v[0] === '[')) {
48
+ try { row[k] = JSON.parse(v) } catch {}
49
+ }
50
+ }
51
+ this._cache.set(norm, row)
52
+ return row
53
+ }
54
+ } catch {}
55
+ }
56
+ try {
57
+ const events = await this._adapter.query(eventName, { from: key, limit: 20 })
58
+ if (events.length === 0) return null
59
+ const c = typeof events[0].content === 'string' ? JSON.parse(events[0].content) : events[0].content
60
+ this._cache.set(norm, c)
61
+ return c
62
+ } catch { return this._cache.get(norm) || null }
63
+ },
64
+
65
+ async list(opts = {}) {
66
+ if (!this._adapter) return []
67
+ try {
68
+ const events = await this._adapter.query(eventName, { limit: opts.limit || 500 })
69
+ return events.map(ev => {
70
+ const c = typeof ev.content === 'string' ? JSON.parse(ev.content) : (ev.content || {})
71
+ return { ...c, _from: ev.from }
72
+ })
73
+ } catch { return [] }
74
+ },
75
+ }
76
+ }
77
+
78
+ export class Registry {
79
+ /**
80
+ * @param {object} [opts]
81
+ * @param {object} [opts.adapter] shared adapter for all three sub-apps
82
+ * @param {object} [opts.identitiesAdapter] per-sub-app adapter override
83
+ * @param {object} [opts.enclavesAdapter]
84
+ * @param {object} [opts.nodesAdapter]
85
+ * @param {object} [opts.plugins] ClientPluginRegistry to source RegistryStrategyFn from
86
+ */
87
+ constructor(opts = {}) {
88
+ this.identities = makeRegistrySubApp('reg_identity', { keyField: 'id_pub', table: 'reg_identities' })
89
+ this.enclaves = makeRegistrySubApp('reg_enclave', { keyField: 'enclave_id', table: 'reg_enclaves' })
90
+ this.nodes = makeRegistrySubApp('reg_node', { keyField: 'seq_pub', table: 'reg_nodes' })
91
+
92
+ // resolveEnclave: pubKey + appName → enclave_id (per Reg_Identity.enclaves map).
93
+ this.identities.resolveEnclave = async function (pubKey, appName) {
94
+ const entry = await this.lookup(pubKey)
95
+ if (!entry) return { ok: false, reason: 'unregistered', pubKey }
96
+ const enclaveId = entry.enclaves?.[String(appName).toLowerCase()]
97
+ if (!enclaveId) return { ok: false, reason: 'app_not_enabled', pubKey, appName }
98
+ return { ok: true, enclave: enclaveId, entry }
99
+ }
100
+
101
+ if (opts.adapter) this.setAdapter(opts.adapter)
102
+ if (opts.identitiesAdapter) this.identities.setAdapter(opts.identitiesAdapter)
103
+ if (opts.enclavesAdapter) this.enclaves.setAdapter(opts.enclavesAdapter)
104
+ if (opts.nodesAdapter) this.nodes.setAdapter(opts.nodesAdapter)
105
+
106
+ if (opts.plugins) this.setPlugins(opts.plugins)
107
+ }
108
+
109
+ /** Wire all three sub-apps to the registry's RegistryStrategyFn plugin. */
110
+ setPlugins(plugins) {
111
+ const strategy = plugins?.get?.('RegistryStrategyFn')
112
+ if (!strategy) return
113
+ this.identities.setStrategy(strategy)
114
+ this.enclaves.setStrategy(strategy)
115
+ this.nodes.setStrategy(strategy)
116
+ }
117
+
118
+ /** Point all three sub-apps at the same adapter (single shared Registry enclave). */
119
+ setAdapter(adapter) {
120
+ this.identities.setAdapter(adapter)
121
+ this.enclaves.setAdapter(adapter)
122
+ this.nodes.setAdapter(adapter)
123
+ }
124
+ }
package/schema.mjs ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Schema flatteners — delegate to the ManifestNormalizerFn plugin.
3
+ *
4
+ * The actual implementation lives in
5
+ * `@enc-protocol/plugin-client-base/normalizer`. This file is kept
6
+ * as a thin re-export so existing callers (`flattenEnclaveManifest`)
7
+ * keep working, and so per-app SDKs can dependency-inject a different
8
+ * normalizer if they author a non-default manifest DSL.
9
+ */
10
+
11
+ import defaultFlatten from '@enc-protocol/plugin-client-base/normalizer'
12
+
13
+ /** Bound at module load to the default plugin impl. */
14
+ let _impl = defaultFlatten
15
+
16
+ /**
17
+ * Swap the active impl (e.g. from a ClientPluginRegistry binding).
18
+ * Pass `null` to restore the default.
19
+ */
20
+ export function setManifestNormalizer(fn) {
21
+ _impl = fn || defaultFlatten
22
+ }
23
+
24
+ export function flattenEnclaveManifest(raw, opts) {
25
+ return _impl(raw, opts)
26
+ }
package/subscriber.mjs ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Subscriber — wraps adapter.subscribe with optional per-event decrypt.
3
+ *
4
+ * The adapter delivers raw events; Subscriber decrypts content for
5
+ * events in `encrypted` (set of event names) using one of:
6
+ * 1. opts.sdk._decrypt (per-app SDK override — backward compat)
7
+ * 2. opts.sdk.crypto.decrypt (legacy override path)
8
+ * 3. opts.plugins.invoke('EnvelopeDecryptFn', ...) (protocol-level slot)
9
+ */
10
+
11
+ export class Subscriber {
12
+ constructor(opts = {}) {
13
+ this.adapter = opts.adapter
14
+ this.encrypted = new Set(opts.encrypted || [])
15
+ this.eventToDataType = opts.eventToDataType || {}
16
+ this._sdk = opts.sdk || null
17
+ this._plugins = opts.plugins || this._sdk?.client?.plugins || null
18
+ }
19
+
20
+ _resolveDecrypt() {
21
+ if (typeof this._sdk?._decrypt === 'function') {
22
+ return (et, c, f) => this._sdk._decrypt(et, c, f)
23
+ }
24
+ if (typeof this._sdk?.crypto?.decrypt === 'function') {
25
+ return (et, c, f) => this._sdk.crypto.decrypt(et, c, f)
26
+ }
27
+ if (this._plugins?.has?.('EnvelopeDecryptFn')) {
28
+ return (et, c, f) => this._plugins.invoke('EnvelopeDecryptFn', et, c, f)
29
+ }
30
+ return null
31
+ }
32
+
33
+ subscribe(callback) {
34
+ if (!this.adapter?.subscribe) return () => {}
35
+ return this.adapter.subscribe(async (event) => {
36
+ const eventType = event.type || event.event
37
+ const isEnc = this.encrypted.has(eventType)
38
+ const decrypt = this._resolveDecrypt()
39
+ if (isEnc && decrypt && event.content) {
40
+ try {
41
+ const content = typeof event.content === 'string'
42
+ ? JSON.parse(event.content) : event.content
43
+ const decrypted = await decrypt(eventType, content, event.from)
44
+ callback({ ...event, content: decrypted, _encrypted: true })
45
+ return
46
+ } catch { /* fall through to raw */ }
47
+ }
48
+ callback(event)
49
+ })
50
+ }
51
+ }