openfused 0.4.5 → 0.5.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/README.md +12 -0
- package/dist/cli.js +257 -11
- package/dist/crypto.d.ts +3 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +10 -4
- package/dist/wasm-core.d.ts +3 -0
- package/package.json +1 -1
- package/wasm/openfused-core.wasm +0 -0
package/README.md
CHANGED
|
@@ -313,6 +313,18 @@ openfused serve --store ./my-context --token "$OPENFUSE_TOKEN" --gc-days 7
|
|
|
313
313
|
|
|
314
314
|
Rate limiting, IP filtering, and TLS belong at the reverse proxy layer (nginx, Caddy, cloudflared). The daemon focuses on application logic.
|
|
315
315
|
|
|
316
|
+
**Isolation:** Run the daemon as a dedicated non-root user with access only to the store directory. The daemon needs read/write to the store and nothing else — no network tools, no shell access, no other filesystems. In Docker this is automatic (container isolation). On bare metal:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Create isolated user
|
|
320
|
+
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/openfused openfused
|
|
321
|
+
sudo mkdir -p /var/lib/openfused/store
|
|
322
|
+
sudo chown -R openfused: /var/lib/openfused
|
|
323
|
+
|
|
324
|
+
# Run as that user
|
|
325
|
+
sudo -u openfused openfused serve --store /var/lib/openfused/store --public --token "$TOKEN"
|
|
326
|
+
```
|
|
327
|
+
|
|
316
328
|
Endpoints:
|
|
317
329
|
|
|
318
330
|
| Endpoint | Method | Auth | Purpose |
|
package/dist/cli.js
CHANGED
|
@@ -118,7 +118,38 @@ program
|
|
|
118
118
|
const store = new ContextStore(resolve(opts.dir));
|
|
119
119
|
if (opts.set) {
|
|
120
120
|
await store.writeProfile(opts.set);
|
|
121
|
-
console.log("Profile updated.");
|
|
121
|
+
console.log("Profile updated locally.");
|
|
122
|
+
// Push to remote endpoint if agent has an HTTP peer for itself
|
|
123
|
+
const config = await store.readConfig();
|
|
124
|
+
const endpoint = config.peers?.find((p) => p.name === config.name && p.url?.startsWith("http"))?.url;
|
|
125
|
+
if (endpoint) {
|
|
126
|
+
try {
|
|
127
|
+
const { signChallenge } = await import("./crypto.js");
|
|
128
|
+
const timestamp = new Date().toISOString();
|
|
129
|
+
const challenge = `PROFILE:${config.name}:${timestamp}`;
|
|
130
|
+
const { signature, publicKey } = await signChallenge(store.root, challenge);
|
|
131
|
+
const resp = await fetch(`${endpoint}/profile/${encodeURIComponent(config.name)}`, {
|
|
132
|
+
method: "PUT",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "text/plain",
|
|
135
|
+
"X-OpenFuse-PublicKey": publicKey,
|
|
136
|
+
"X-OpenFuse-Signature": signature,
|
|
137
|
+
"X-OpenFuse-Timestamp": timestamp,
|
|
138
|
+
},
|
|
139
|
+
body: opts.set,
|
|
140
|
+
});
|
|
141
|
+
if (resp.ok) {
|
|
142
|
+
console.log("Profile synced to hosted mailbox.");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const err = await resp.text();
|
|
146
|
+
console.error(`Failed to sync profile to mailbox: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.error(`Failed to sync profile: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
else {
|
|
124
155
|
console.log(await store.readProfile());
|
|
@@ -131,19 +162,64 @@ inbox
|
|
|
131
162
|
.description("List inbox messages")
|
|
132
163
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
133
164
|
.option("--raw", "Show raw content instead of wrapped")
|
|
165
|
+
.option("--all", "Show all messages including unverified (default: verified + subscribed)")
|
|
166
|
+
.option("--trusted", "Show only trusted messages")
|
|
167
|
+
.option("--no-sync", "Skip pulling from remote peers before listing")
|
|
134
168
|
.action(async (opts) => {
|
|
135
169
|
const store = new ContextStore(resolve(opts.dir));
|
|
136
|
-
|
|
137
|
-
if (
|
|
170
|
+
// Auto-sync: pull new messages from remote peers before listing
|
|
171
|
+
if (opts.sync !== false) {
|
|
172
|
+
try {
|
|
173
|
+
const config = await store.readConfig();
|
|
174
|
+
if (config.peers.length > 0) {
|
|
175
|
+
const { syncAll } = await import("./sync.js");
|
|
176
|
+
await syncAll(store);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
180
|
+
}
|
|
181
|
+
const allMessages = await store.readInbox();
|
|
182
|
+
// Default: show verified (trusted + subscribed). Use --all for everything.
|
|
183
|
+
let messages;
|
|
184
|
+
if (opts.all) {
|
|
185
|
+
messages = allMessages;
|
|
186
|
+
}
|
|
187
|
+
else if (opts.trusted) {
|
|
188
|
+
messages = allMessages.filter((m) => m.verified && m.trusted);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
messages = allMessages.filter((m) => m.verified && (m.trusted || m.subscribed));
|
|
192
|
+
}
|
|
193
|
+
const hidden = allMessages.length - messages.length;
|
|
194
|
+
if (messages.length === 0 && hidden === 0) {
|
|
138
195
|
console.log("Inbox is empty.");
|
|
139
196
|
return;
|
|
140
197
|
}
|
|
198
|
+
if (messages.length === 0 && hidden > 0) {
|
|
199
|
+
console.log(`Inbox has ${hidden} message(s) from unsubscribed/untrusted senders.`);
|
|
200
|
+
console.log(`Run with --all to see them, or: openfuse key trust <name> / openfuse subscribe <name>`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
141
203
|
for (const msg of messages) {
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
204
|
+
// Build badge: [VERIFIED] [TRUSTED] [INTERNAL] etc.
|
|
205
|
+
const badges = [];
|
|
206
|
+
badges.push(msg.verified ? "[VERIFIED]" : "[UNVERIFIED]");
|
|
207
|
+
if (msg.trusted)
|
|
208
|
+
badges.push("[TRUSTED]");
|
|
209
|
+
if (msg.subscribed)
|
|
210
|
+
badges.push("[SUBSCRIBED]");
|
|
211
|
+
if (msg.relationship)
|
|
212
|
+
badges.push(`[${msg.relationship.toUpperCase()}]`);
|
|
213
|
+
if (msg.encrypted)
|
|
214
|
+
badges.push("[ENCRYPTED]");
|
|
215
|
+
const badgeStr = badges.join(" ");
|
|
216
|
+
const noteStr = msg.note ? ` (${msg.note})` : "";
|
|
217
|
+
console.log(`\n--- ${badgeStr} From: ${msg.from}${noteStr} | ${msg.time} ---`);
|
|
145
218
|
console.log(opts.raw ? msg.content : msg.wrappedContent);
|
|
146
219
|
}
|
|
220
|
+
if (hidden > 0) {
|
|
221
|
+
console.log(`\n(${hidden} message(s) hidden — use --all to show)`);
|
|
222
|
+
}
|
|
147
223
|
});
|
|
148
224
|
inbox
|
|
149
225
|
.command("archive [file]")
|
|
@@ -506,13 +582,25 @@ key
|
|
|
506
582
|
.command("trust <query>")
|
|
507
583
|
.description("Trust a key in the keyring (name, name:fingerprint, or fingerprint)")
|
|
508
584
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
585
|
+
.option("--internal", "Mark as internal (same org/team)")
|
|
586
|
+
.option("--external", "Mark as external (partner/vendor)")
|
|
587
|
+
.option("--note <text>", "Private note about this peer")
|
|
509
588
|
.action(async (query, opts) => {
|
|
510
589
|
const store = new ContextStore(resolve(opts.dir));
|
|
511
590
|
const config = await store.readConfig();
|
|
512
591
|
const entry = resolveKeyring(config.keyring, query);
|
|
513
592
|
entry.trusted = true;
|
|
593
|
+
if (opts.internal)
|
|
594
|
+
entry.relationship = "internal";
|
|
595
|
+
else if (opts.external)
|
|
596
|
+
entry.relationship = "external";
|
|
597
|
+
if (opts.note)
|
|
598
|
+
entry.note = opts.note;
|
|
514
599
|
await store.writeConfig(config);
|
|
515
|
-
|
|
600
|
+
const rel = entry.relationship ? ` [${entry.relationship.toUpperCase()}]` : "";
|
|
601
|
+
console.log(`Trusted: ${entry.name} (${entry.fingerprint})${rel}`);
|
|
602
|
+
if (entry.note)
|
|
603
|
+
console.log(` Note: ${entry.note}`);
|
|
516
604
|
});
|
|
517
605
|
key
|
|
518
606
|
.command("untrust <query>")
|
|
@@ -538,6 +626,141 @@ key
|
|
|
538
626
|
console.log(`signing:${config.publicKey}`);
|
|
539
627
|
console.log(`encryption:${config.encryptionKey}`);
|
|
540
628
|
});
|
|
629
|
+
// --- subscribe / unsubscribe / broadcast ---
|
|
630
|
+
program
|
|
631
|
+
.command("subscribe <name>")
|
|
632
|
+
.description("Subscribe to an agent's broadcasts")
|
|
633
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
634
|
+
.option("-r, --registry <url>", "Registry URL")
|
|
635
|
+
.option("--note <text>", "Private note about this agent")
|
|
636
|
+
.action(async (name, opts) => {
|
|
637
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
638
|
+
const reg = registry.resolveRegistry(opts.registry);
|
|
639
|
+
const config = await store.readConfig();
|
|
640
|
+
let entry = config.keyring.find((e) => e.name === name);
|
|
641
|
+
if (!entry) {
|
|
642
|
+
// Auto-import from registry
|
|
643
|
+
try {
|
|
644
|
+
const manifest = await registry.discover(name, reg);
|
|
645
|
+
entry = {
|
|
646
|
+
name: manifest.name,
|
|
647
|
+
address: `${manifest.name}@registry`,
|
|
648
|
+
signingKey: manifest.publicKey,
|
|
649
|
+
encryptionKey: manifest.encryptionKey,
|
|
650
|
+
fingerprint: manifest.fingerprint,
|
|
651
|
+
trusted: false,
|
|
652
|
+
subscribed: true,
|
|
653
|
+
relationship: null,
|
|
654
|
+
note: opts.note ?? null,
|
|
655
|
+
added: new Date().toISOString(),
|
|
656
|
+
};
|
|
657
|
+
config.keyring.push(entry);
|
|
658
|
+
console.log(`Imported key for ${manifest.name} from registry`);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
console.error(`Agent '${name}' not found in keyring or registry.`);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
entry.subscribed = true;
|
|
667
|
+
if (opts.note)
|
|
668
|
+
entry.note = opts.note;
|
|
669
|
+
}
|
|
670
|
+
await store.writeConfig(config);
|
|
671
|
+
console.log(`Subscribed to: ${entry.name} (${entry.fingerprint})`);
|
|
672
|
+
console.log(`Their broadcasts will appear in your inbox.`);
|
|
673
|
+
});
|
|
674
|
+
program
|
|
675
|
+
.command("unsubscribe <name>")
|
|
676
|
+
.description("Unsubscribe from an agent's broadcasts")
|
|
677
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
678
|
+
.action(async (name, opts) => {
|
|
679
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
680
|
+
const config = await store.readConfig();
|
|
681
|
+
const entry = config.keyring.find((e) => e.name === name);
|
|
682
|
+
if (!entry) {
|
|
683
|
+
console.error(`Agent '${name}' not in keyring.`);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
entry.subscribed = false;
|
|
687
|
+
await store.writeConfig(config);
|
|
688
|
+
console.log(`Unsubscribed from: ${entry.name}`);
|
|
689
|
+
});
|
|
690
|
+
program
|
|
691
|
+
.command("broadcast <message>")
|
|
692
|
+
.description("Send a message to all trusted + subscribed agents")
|
|
693
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
694
|
+
.option("--internal", "Only send to internal agents")
|
|
695
|
+
.option("--external", "Only send to external agents")
|
|
696
|
+
.option("--subscribers", "Only send to subscribers (not trusted)")
|
|
697
|
+
.option("--trusted-only", "Only send to trusted agents (skip subscribed-but-untrusted)")
|
|
698
|
+
.action(async (message, opts) => {
|
|
699
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
700
|
+
const config = await store.readConfig();
|
|
701
|
+
// Build recipient list
|
|
702
|
+
let recipients = config.keyring.filter((e) => {
|
|
703
|
+
if (opts.trustedOnly)
|
|
704
|
+
return e.trusted;
|
|
705
|
+
if (opts.subscribers)
|
|
706
|
+
return e.subscribed;
|
|
707
|
+
return e.trusted || e.subscribed;
|
|
708
|
+
});
|
|
709
|
+
if (opts.internal) {
|
|
710
|
+
recipients = recipients.filter((e) => e.relationship === "internal");
|
|
711
|
+
}
|
|
712
|
+
else if (opts.external) {
|
|
713
|
+
recipients = recipients.filter((e) => e.relationship === "external");
|
|
714
|
+
}
|
|
715
|
+
if (recipients.length === 0) {
|
|
716
|
+
console.log("No recipients. Trust or subscribe to agents first.");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// Warn about untrusted recipients
|
|
720
|
+
const untrusted = recipients.filter((e) => !e.trusted);
|
|
721
|
+
if (untrusted.length > 0) {
|
|
722
|
+
console.log(`Warning: ${untrusted.length} recipient(s) are subscribed but NOT trusted:`);
|
|
723
|
+
for (const u of untrusted) {
|
|
724
|
+
console.log(` ${u.name} (${u.fingerprint})`);
|
|
725
|
+
}
|
|
726
|
+
console.log(`Their keys have not been verified. Use --trusted-only to skip them.\n`);
|
|
727
|
+
}
|
|
728
|
+
console.log(`Broadcasting to ${recipients.length} agent(s)...`);
|
|
729
|
+
let delivered = 0;
|
|
730
|
+
let queued = 0;
|
|
731
|
+
for (const recipient of recipients) {
|
|
732
|
+
try {
|
|
733
|
+
await store.sendInbox(recipient.name, message);
|
|
734
|
+
// Try HTTP delivery
|
|
735
|
+
const peer = config.peers.find((p) => p.name === recipient.name && p.url?.startsWith("http"));
|
|
736
|
+
if (peer) {
|
|
737
|
+
const outboxFile = findNewestOutboxFile(store.root, recipient.name);
|
|
738
|
+
if (outboxFile) {
|
|
739
|
+
const { checkSsrf } = await import("./sync.js");
|
|
740
|
+
await checkSsrf(peer.url);
|
|
741
|
+
const body = await readFile(join(store.root, "outbox", outboxFile), "utf-8");
|
|
742
|
+
const inboxUrl = `${peer.url.replace(/\/$/, "")}/inbox/${encodeURIComponent(recipient.name)}`;
|
|
743
|
+
const r = await fetch(inboxUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body });
|
|
744
|
+
if (r.ok) {
|
|
745
|
+
const { mkdir, rename } = await import("node:fs/promises");
|
|
746
|
+
const filePath = join(store.root, "outbox", outboxFile);
|
|
747
|
+
const sentDir = join(filePath, "..", ".sent");
|
|
748
|
+
const baseName = outboxFile.split("/").pop();
|
|
749
|
+
await mkdir(sentDir, { recursive: true });
|
|
750
|
+
await rename(filePath, join(sentDir, baseName));
|
|
751
|
+
delivered++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
queued++;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
queued++;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
console.log(`Done. ${delivered} delivered, ${queued} queued for sync.`);
|
|
763
|
+
});
|
|
541
764
|
// --- sync ---
|
|
542
765
|
program
|
|
543
766
|
.command("sync [peer]")
|
|
@@ -580,6 +803,20 @@ program
|
|
|
580
803
|
const config = await store.readConfig();
|
|
581
804
|
const agentName = opts.name || `${config.name}.openfused.net`;
|
|
582
805
|
const manifest = await registry.register(store, opts.endpoint || "", reg, agentName);
|
|
806
|
+
// Auto-add endpoint as a peer so sync/inbox list can pull from it
|
|
807
|
+
if (manifest.endpoint?.startsWith("http")) {
|
|
808
|
+
const config2 = await store.readConfig();
|
|
809
|
+
const selfName = config2.name;
|
|
810
|
+
if (!config2.peers.some((p) => p.url === manifest.endpoint && p.name === selfName)) {
|
|
811
|
+
config2.peers.push({
|
|
812
|
+
id: (await import("nanoid")).nanoid(12),
|
|
813
|
+
name: selfName,
|
|
814
|
+
url: manifest.endpoint,
|
|
815
|
+
access: "read",
|
|
816
|
+
});
|
|
817
|
+
await store.writeConfig(config2);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
583
820
|
console.log(`Registered: ${manifest.name} [SIGNED]`);
|
|
584
821
|
if (manifest.endpoint)
|
|
585
822
|
console.log(` Endpoint: ${manifest.endpoint}`);
|
|
@@ -595,8 +832,8 @@ program
|
|
|
595
832
|
// --- discover ---
|
|
596
833
|
program
|
|
597
834
|
.command("discover <name>")
|
|
598
|
-
.description("Look up an agent by name
|
|
599
|
-
.option("-r, --registry <url>", "Registry URL")
|
|
835
|
+
.description("Look up an agent by name via DNS")
|
|
836
|
+
.option("-r, --registry <url>", "Registry URL (fallback if DNS fails)")
|
|
600
837
|
.action(async (name, opts) => {
|
|
601
838
|
const reg = registry.resolveRegistry(opts.registry);
|
|
602
839
|
const manifest = await registry.discover(name, reg);
|
|
@@ -645,6 +882,7 @@ program
|
|
|
645
882
|
.option("-r, --registry <url>", "Registry URL")
|
|
646
883
|
.option("--http", "Force HTTP delivery (uses registry endpoint)")
|
|
647
884
|
.option("--ssh", "Force SSH delivery (uses local peer SSH URL)")
|
|
885
|
+
.option("--trust", "Auto-trust the recipient's key (skip manual fingerprint verification)")
|
|
648
886
|
.action(async (name, message, opts) => {
|
|
649
887
|
const store = new ContextStore(resolve(opts.dir));
|
|
650
888
|
const reg = registry.resolveRegistry(opts.registry);
|
|
@@ -660,16 +898,24 @@ program
|
|
|
660
898
|
if (manifest.endpoint?.startsWith("http"))
|
|
661
899
|
httpEndpoint = manifest.endpoint;
|
|
662
900
|
// Auto-import key + add as peer
|
|
663
|
-
|
|
901
|
+
const existing = config.keyring.find((e) => e.signingKey === manifest.publicKey);
|
|
902
|
+
if (!existing) {
|
|
664
903
|
config.keyring.push({
|
|
665
904
|
name: manifest.name,
|
|
666
905
|
address: `${manifest.name}@registry`,
|
|
667
906
|
signingKey: manifest.publicKey,
|
|
668
907
|
encryptionKey: manifest.encryptionKey,
|
|
669
908
|
fingerprint: manifest.fingerprint,
|
|
670
|
-
trusted:
|
|
909
|
+
trusted: !!opts.trust,
|
|
671
910
|
added: new Date().toISOString(),
|
|
672
911
|
});
|
|
912
|
+
if (opts.trust) {
|
|
913
|
+
console.log(`Trusted ${manifest.name} (${manifest.fingerprint})`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else if (opts.trust && !existing.trusted) {
|
|
917
|
+
existing.trusted = true;
|
|
918
|
+
console.log(`Trusted ${manifest.name} (${existing.fingerprint})`);
|
|
673
919
|
}
|
|
674
920
|
if (manifest.endpoint && !config.peers.some((p) => p.name === manifest.name)) {
|
|
675
921
|
config.peers.push({
|
package/dist/crypto.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface KeyringEntry {
|
|
|
14
14
|
encryptionKey?: string;
|
|
15
15
|
fingerprint: string;
|
|
16
16
|
trusted: boolean;
|
|
17
|
+
subscribed?: boolean;
|
|
18
|
+
relationship?: string | null;
|
|
19
|
+
note?: string | null;
|
|
17
20
|
added: string;
|
|
18
21
|
}
|
|
19
22
|
export declare function generateKeys(storeRoot: string): Promise<{
|
package/dist/registry.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface Manifest {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function resolveRegistry(flag?: string): string;
|
|
19
19
|
export declare function register(store: ContextStore, endpoint: string, registry: string, name?: string): Promise<Manifest>;
|
|
20
|
+
export interface AgentListEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
endpoint: string;
|
|
23
|
+
fingerprint: string;
|
|
24
|
+
dns: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function listAgents(registry: string): Promise<AgentListEntry[]>;
|
|
20
27
|
export declare function discover(name: string, registry: string): Promise<Manifest>;
|
|
21
28
|
export declare function revoke(store: ContextStore, registry: string): Promise<void>;
|
|
22
29
|
export declare function checkUpdate(currentVersion: string): Promise<string | null>;
|
package/dist/registry.js
CHANGED
|
@@ -42,10 +42,16 @@ export async function register(store, endpoint, registry, name) {
|
|
|
42
42
|
}
|
|
43
43
|
return manifest;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
export async function listAgents(registry) {
|
|
46
|
+
const resp = await fetch(`${registry.replace(/\/$/, "")}/agents`);
|
|
47
|
+
if (!resp.ok) {
|
|
48
|
+
if (resp.status === 404)
|
|
49
|
+
throw new Error(`Registry at ${registry} does not support agent listing (/agents endpoint). Try discovering agents by name: openfuse discover <name>`);
|
|
50
|
+
throw new Error(`Registry returned ${resp.status}`);
|
|
51
|
+
}
|
|
52
|
+
const data = (await resp.json());
|
|
53
|
+
return data.agents || [];
|
|
54
|
+
}
|
|
49
55
|
export async function discover(name, registry) {
|
|
50
56
|
// If name contains a dot, it's a domain — try DNS TXT directly
|
|
51
57
|
// Otherwise try DNS at openfused.net, then fall back to registry API
|
package/dist/wasm-core.d.ts
CHANGED
package/package.json
CHANGED
package/wasm/openfused-core.wasm
CHANGED
|
Binary file
|