marifold 0.69.0 → 0.71.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.
Files changed (58) hide show
  1. package/dist/bridge-template/.env.example +6 -0
  2. package/dist/bridge-template/HOSTING.md +477 -0
  3. package/dist/bridge-template/README.md +176 -0
  4. package/dist/bridge-template/api/bridge.ts +5 -0
  5. package/dist/bridge-template/dist/Store.d.ts +51 -0
  6. package/dist/bridge-template/dist/Store.d.ts.map +1 -0
  7. package/dist/bridge-template/dist/Store.js +170 -0
  8. package/dist/bridge-template/dist/Store.js.map +1 -0
  9. package/dist/bridge-template/dist/index.d.ts +4 -0
  10. package/dist/bridge-template/dist/index.d.ts.map +1 -0
  11. package/dist/bridge-template/dist/index.js +189 -0
  12. package/dist/bridge-template/dist/index.js.map +1 -0
  13. package/dist/bridge-template/dist/serve.d.ts +2 -0
  14. package/dist/bridge-template/dist/serve.d.ts.map +1 -0
  15. package/dist/bridge-template/dist/serve.js +18 -0
  16. package/dist/bridge-template/dist/serve.js.map +1 -0
  17. package/dist/bridge-template/package.json +16 -0
  18. package/dist/bridge-template/setup/setup.py +319 -0
  19. package/dist/bridge-template/setup.sh +8 -0
  20. package/dist/bridge-template/vendor/workspace-protocol/dist/identity.d.ts +15 -0
  21. package/dist/bridge-template/vendor/workspace-protocol/dist/identity.d.ts.map +1 -0
  22. package/dist/bridge-template/vendor/workspace-protocol/dist/identity.js +93 -0
  23. package/dist/bridge-template/vendor/workspace-protocol/dist/identity.js.map +1 -0
  24. package/dist/bridge-template/vendor/workspace-protocol/dist/index.d.ts +3 -0
  25. package/dist/bridge-template/vendor/workspace-protocol/dist/index.d.ts.map +1 -0
  26. package/dist/bridge-template/vendor/workspace-protocol/dist/index.js +19 -0
  27. package/dist/bridge-template/vendor/workspace-protocol/dist/index.js.map +1 -0
  28. package/dist/bridge-template/vendor/workspace-protocol/dist/types.d.ts +97 -0
  29. package/dist/bridge-template/vendor/workspace-protocol/dist/types.d.ts.map +1 -0
  30. package/dist/bridge-template/vendor/workspace-protocol/dist/types.js +55 -0
  31. package/dist/bridge-template/vendor/workspace-protocol/dist/types.js.map +1 -0
  32. package/dist/bridge-template/vendor/workspace-protocol/package.json +9 -0
  33. package/dist/bridge-template/vercel.json +16 -0
  34. package/dist/commands/WorkspaceClient.d.ts +6 -0
  35. package/dist/commands/WorkspaceClient.d.ts.map +1 -0
  36. package/dist/commands/WorkspaceClient.js +72 -0
  37. package/dist/commands/WorkspaceClient.js.map +1 -0
  38. package/dist/commands/agent.d.ts.map +1 -1
  39. package/dist/commands/agent.js +9 -3
  40. package/dist/commands/agent.js.map +1 -1
  41. package/dist/commands/ask.d.ts.map +1 -1
  42. package/dist/commands/ask.js +10 -5
  43. package/dist/commands/ask.js.map +1 -1
  44. package/dist/commands/config.js +11 -8
  45. package/dist/commands/config.js.map +1 -1
  46. package/dist/commands/init.d.ts.map +1 -1
  47. package/dist/commands/init.js +10 -20
  48. package/dist/commands/init.js.map +1 -1
  49. package/dist/commands/schedule.d.ts.map +1 -1
  50. package/dist/commands/schedule.js +20 -8
  51. package/dist/commands/schedule.js.map +1 -1
  52. package/dist/commands/workspace.d.ts +9 -0
  53. package/dist/commands/workspace.d.ts.map +1 -0
  54. package/dist/commands/workspace.js +223 -0
  55. package/dist/commands/workspace.js.map +1 -0
  56. package/dist/index.js +9 -1
  57. package/dist/index.js.map +1 -1
  58. package/package.json +8 -6
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env python3
2
+ """Interactive Linux installer for a prepared Marifold bridge package."""
3
+
4
+ import argparse
5
+ import getpass
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import re
10
+ import secrets
11
+ import shutil
12
+ import socket
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ from urllib.parse import urlsplit
17
+
18
+ PROJECT = "marifold-personal-bridge"
19
+ DEFAULT_TARGET = Path("/opt/marifold-bridge")
20
+
21
+
22
+ def run(args, cwd=None):
23
+ result = subprocess.run(args, cwd=cwd, text=True, capture_output=True)
24
+ if result.returncode:
25
+ # Tool output can contain supplied connection strings. Keep it off the terminal.
26
+ raise RuntimeError(f"{args[0]} failed (exit {result.returncode}). Configuration is retained; see HOSTING.md for troubleshooting.")
27
+ return result.stdout.strip()
28
+
29
+
30
+ def validate_redis(value):
31
+ if any(c.isspace() or c in "'\"\\" for c in value):
32
+ raise ValueError("Redis URL must contain no whitespace or quotes; percent-encode password characters.")
33
+ try:
34
+ parsed = urlsplit(value)
35
+ port = parsed.port
36
+ valid = parsed.scheme in ("redis", "rediss") and parsed.hostname and not parsed.query and not parsed.fragment
37
+ valid = valid and re.fullmatch(r"/\d+|", parsed.path) and (port is None or 0 < port < 65536)
38
+ except ValueError:
39
+ valid = False
40
+ if not valid:
41
+ raise ValueError("Use a Redis TCP URL, optionally ending in /database_number.")
42
+ if parsed.scheme == "redis" and parsed.hostname not in ("127.0.0.1", "localhost", "::1"):
43
+ raise ValueError("Use rediss:// for Redis outside this server's loopback network.")
44
+ return value
45
+
46
+
47
+ def validate_domain(value):
48
+ value = value.lower().strip()
49
+ labels = value.split(".")
50
+ if len(value) > 253 or len(labels) < 2 or any(
51
+ not re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", label) for label in labels
52
+ ):
53
+ raise ValueError("Enter a DNS hostname such as bridge.example.com, without https:// or a path.")
54
+ return value
55
+
56
+
57
+ def confirm(prompt):
58
+ return input(prompt + " [y/N]: ").strip().lower() == "y"
59
+
60
+
61
+ def check_port(port):
62
+ with socket.socket() as sock:
63
+ try:
64
+ sock.bind(("0.0.0.0", port))
65
+ except OSError:
66
+ raise ValueError(f"Port {port} is already in use. Choose existing HTTPS ingress or resolve the conflict first.") from None
67
+
68
+
69
+ def write(path, value, mode=0o600):
70
+ with path.open("x") as output:
71
+ output.write(value)
72
+ path.chmod(mode)
73
+
74
+
75
+ def compose_config(dedicated, domain):
76
+ bridge = {
77
+ "build": {"context": "./package"},
78
+ "network_mode": "host",
79
+ "restart": "unless-stopped",
80
+ "env_file": ["./bridge.env"],
81
+ "user": "node",
82
+ "read_only": True,
83
+ "cap_drop": ["ALL"],
84
+ "security_opt": ["no-new-privileges:true"],
85
+ "healthcheck": {
86
+ "test": ["CMD", "node", "health.cjs"],
87
+ "interval": "10s", "timeout": "8s", "retries": 12, "start_period": "10s",
88
+ },
89
+ "logging": {"driver": "json-file", "options": {"max-size": "10m", "max-file": "3"}},
90
+ }
91
+ config = {"name": PROJECT, "services": {"bridge": bridge}, "volumes": {}}
92
+ if dedicated:
93
+ config["services"]["redis"] = {
94
+ "image": "redis:8.8", "network_mode": "host", "restart": "unless-stopped",
95
+ "command": ["redis-server", "/usr/local/etc/redis/redis.conf"],
96
+ "volumes": ["./redis.conf:/usr/local/etc/redis/redis.conf:ro", "redis-data:/data"],
97
+ "logging": bridge["logging"],
98
+ }
99
+ bridge["depends_on"] = ["redis"]
100
+ config["volumes"]["redis-data"] = {}
101
+ if domain:
102
+ config["services"]["https"] = {
103
+ "image": "caddy:2", "network_mode": "host", "restart": "unless-stopped",
104
+ "volumes": ["./Caddyfile:/etc/caddy/Caddyfile:ro", "caddy-data:/data", "caddy-config:/config"],
105
+ "depends_on": {"bridge": {"condition": "service_healthy"}},
106
+ "logging": bridge["logging"],
107
+ }
108
+ config["volumes"].update({"caddy-data": {}, "caddy-config": {}})
109
+ return config
110
+
111
+
112
+ def validate_package(source):
113
+ # Only copy the compiled package inputs; never copy local environment/configuration.
114
+ required = ["package.json", "dist", "vendor"]
115
+ for name in required:
116
+ item = source / name
117
+ if not item.exists() or item.is_symlink():
118
+ raise ValueError("Run setup from a complete prepared bridge package.")
119
+ if item.is_dir() and any(p.is_symlink() for p in item.rglob("*")):
120
+ raise ValueError("Prepared package inputs must not contain symlinks.")
121
+ manifest = json.loads((source / "package.json").read_text())
122
+ if manifest.get("name") != "marifold-personal-bridge":
123
+ raise ValueError("Run marifold workspace bridge prepare first, then use that package's setup.sh.")
124
+ return required
125
+
126
+
127
+ def prepare(source, target, redis_url, domain):
128
+ required = validate_package(source)
129
+ target.mkdir(mode=0o700) # Exclusive creation: never overwrite an existing installation.
130
+ package = target / "package"
131
+ package.mkdir(mode=0o755)
132
+ for name in required:
133
+ item = source / name
134
+ if item.is_dir():
135
+ shutil.copytree(item, package / name)
136
+ else:
137
+ shutil.copyfile(item, package / name)
138
+ for item in package.rglob("*"):
139
+ item.chmod(0o755 if item.is_dir() else 0o644)
140
+ # Installer owns its dependency installation; user-selected local node_modules are excluded.
141
+ write(package / "Dockerfile", "FROM node:24-bookworm-slim\nWORKDIR /app\nCOPY package.json ./\nCOPY vendor ./vendor\nRUN npm install --omit=dev --ignore-scripts\nCOPY dist ./dist\nCOPY health.cjs ./health.cjs\nUSER node\nCMD [\"node\", \"dist/serve.js\"]\n", 0o644)
142
+ write(package / ".dockerignore", "*\n!package.json\n!vendor/\n!vendor/**\n!dist/\n!dist/**\n!health.cjs\n!Dockerfile\n", 0o644)
143
+ write(package / "health.cjs", "const Redis = require('ioredis');\nconst client = new Redis(process.env.MARIFOLD_BRIDGE_REDIS_URL, {lazyConnect: true, retryStrategy: () => null, connectTimeout: 3000, maxRetriesPerRequest: 0});\nclient.on('error', () => {});\nconst timeout = setTimeout(() => process.exit(1), 6000);\n(async () => { try { await client.connect(); await client.ping(); const response = await fetch('http://127.0.0.1:32143/health'); if (!response.ok) throw new Error(); clearTimeout(timeout); client.disconnect(); } catch { process.exit(1); } })();\n", 0o644)
144
+ token = secrets.token_hex(32)
145
+ dedicated = redis_url is None
146
+ if dedicated:
147
+ password = secrets.token_hex(32)
148
+ redis_url = f"redis://default:{password}@127.0.0.1:32144/0"
149
+ write(target / "redis.conf", f"bind 127.0.0.1\nport 32144\nprotected-mode yes\nrequirepass {password}\ndir /data\nappendonly yes\nappendfsync everysec\nsave 900 1\nmaxmemory 128mb\nmaxmemory-policy noeviction\n", 0o644)
150
+ write(target / "bridge.env", f"MARIFOLD_BRIDGE_REDIS_URL='{redis_url}'\nMARIFOLD_BRIDGE_REGISTRATION_TOKEN='{token}'\nHOST=127.0.0.1\nPORT=32143\n")
151
+ write(target / "registration-token", token + "\n")
152
+ if domain:
153
+ write(target / "Caddyfile", f"{domain} {{\n reverse_proxy 127.0.0.1:32143\n}}\n", 0o644)
154
+ write(target / "compose.json", json.dumps(compose_config(dedicated, domain), indent=2) + "\n")
155
+ write(target / "installation.json", json.dumps({"schema": 1, "domain": domain, "dedicatedRedis": dedicated}) + "\n")
156
+
157
+
158
+ def start(target):
159
+ command = ["docker", "compose", "-f", str(target / "compose.json")]
160
+ run(command + ["config", "--quiet"])
161
+ print("Building and starting the bridge. The first image download may take several minutes.", flush=True)
162
+ run(command + ["up", "--detach", "--build", "--wait", "--wait-timeout", "180"])
163
+ run(command + ["exec", "-T", "bridge", "node", "health.cjs"])
164
+ print("Bridge is running; Redis PING and local HTTP health passed.")
165
+ print(f"Read your registration token locally: sudo cat {target}/registration-token")
166
+ print(f"Service status: sudo docker compose -f {target}/compose.json ps")
167
+ metadata = json.loads((target / "installation.json").read_text())
168
+ if metadata["domain"]:
169
+ print(f"Next verify https://{metadata['domain']}/health, then create your workspace with that HTTPS origin.")
170
+ print("Public DNS, certificate issuance and the cloud firewall still need to permit HTTPS; local health does not verify them.")
171
+ else:
172
+ print("Next route your existing HTTPS proxy or Cloudflare Tunnel to http://127.0.0.1:32143.")
173
+
174
+
175
+ def update(source, target):
176
+ required = validate_package(source)
177
+ config = json.loads((target / "compose.json").read_text())
178
+ bridge = config.get("services", {}).get("bridge", {})
179
+ build = bridge.get("build")
180
+ if not isinstance(build, dict) or not isinstance(build.get("context"), str):
181
+ raise ValueError("Only installer-managed bridge builds can be updated.")
182
+ installed = (target / build["context"]).resolve()
183
+ if not installed.is_relative_to(target.resolve()):
184
+ raise ValueError("Bridge build context must stay inside the installation.")
185
+ for name in ("Dockerfile", ".dockerignore", "health.cjs"):
186
+ if (installed / name).is_symlink() or not (installed / name).is_file():
187
+ raise ValueError("Incomplete installed bridge build inputs.")
188
+ command = ["docker", "compose", "-f", str(target / "compose.json")]
189
+ container = run(command + ["ps", "--quiet", "bridge"])
190
+ if not re.fullmatch(r"[a-f0-9]{12,64}", container):
191
+ raise ValueError("Start the existing bridge before updating it.")
192
+ image = run(["docker", "inspect", "--format", "{{.Image}}", container])
193
+ if not re.fullmatch(r"sha256:[a-f0-9]{64}", image):
194
+ raise ValueError("Could not identify the running bridge image for rollback.")
195
+ release = Path(tempfile.mkdtemp(prefix="release-", dir=target))
196
+ release.chmod(0o755)
197
+ for name in required:
198
+ item = source / name
199
+ if item.is_dir():
200
+ shutil.copytree(item, release / name)
201
+ else:
202
+ shutil.copyfile(item, release / name)
203
+ # Preserve local image-mirror and build settings chosen during installation.
204
+ for name in ("Dockerfile", ".dockerignore", "health.cjs"):
205
+ shutil.copyfile(installed / name, release / name)
206
+ for item in release.rglob("*"):
207
+ item.chmod(0o755 if item.is_dir() else 0o644)
208
+ tag = f"{PROJECT}:{release.name}"
209
+ rollback_tag = tag + "-previous"
210
+ run(["docker", "tag", image, rollback_tag])
211
+ previous = json.loads(json.dumps(config))
212
+ previous["services"]["bridge"]["image"] = rollback_tag
213
+ previous["services"]["bridge"]["pull_policy"] = "never"
214
+ backup = target / f"{release.name}-rollback.json"
215
+ write(backup, json.dumps(previous, indent=2) + "\n")
216
+ bridge["build"]["context"] = str(release)
217
+ bridge["image"] = tag
218
+ bridge["pull_policy"] = "never"
219
+ candidate = target / f"{release.name}-compose.json"
220
+ write(candidate, json.dumps(config, indent=2) + "\n")
221
+ staged = ["docker", "compose", "-f", str(candidate)]
222
+ run(staged + ["config", "--quiet"])
223
+ print("Building the new bridge while the existing bridge stays online.", flush=True)
224
+ run(staged + ["build", "bridge"])
225
+ print("Replacing only the bridge container; checking health. Existing device connections will reconnect.", flush=True)
226
+ candidate.replace(target / "compose.json")
227
+ up = ["up", "--detach", "--no-deps", "--no-build", "--pull", "never", "--wait", "--wait-timeout", "180", "bridge"]
228
+ try:
229
+ run(command + up)
230
+ run(command + ["exec", "-T", "bridge", "node", "health.cjs"])
231
+ except (RuntimeError, OSError, KeyboardInterrupt):
232
+ restore = target / f"{release.name}-restore.json"
233
+ shutil.copyfile(backup, restore)
234
+ restore.chmod(0o600)
235
+ restore.replace(target / "compose.json")
236
+ try:
237
+ run(command + up)
238
+ except (RuntimeError, OSError, KeyboardInterrupt):
239
+ raise RuntimeError(f"Update and automatic rollback failed. Configuration is restored; run docker compose -f {target}/compose.json up -d --no-deps --no-build --pull never bridge. Previous release: {backup}") from None
240
+ raise RuntimeError("New bridge health failed; the previous bridge image was restored.") from None
241
+ print("Bridge updated. Local HTTP and Redis PING passed; verify your public HTTPS origin and paired devices.")
242
+ print(f"Previous release retained for rollback: {backup}")
243
+
244
+
245
+ def main():
246
+ parser = argparse.ArgumentParser(description=__doc__)
247
+ mode = parser.add_mutually_exclusive_group()
248
+ mode.add_argument("--update", action="store_true", help="Update only the installed bridge, retaining secrets, data and a rollback image")
249
+ mode.add_argument("--start", action="store_true", help="Start an existing installer-managed installation without replacing secrets/data")
250
+ args = parser.parse_args()
251
+ if sys.platform != "linux" or os.geteuid() != 0:
252
+ raise ValueError("Run sudo bash setup.sh on the Linux ECS/EC2 server, not on your Mac.")
253
+ for executable in ("docker", "systemctl"):
254
+ if not shutil.which(executable):
255
+ raise ValueError("Install Docker Engine with its Compose plugin first: https://docs.docker.com/engine/install/ . Existing Docker installations are never replaced by this script.")
256
+ run(["docker", "compose", "version"])
257
+ target = DEFAULT_TARGET
258
+ if args.start or args.update:
259
+ if target.is_symlink() or target.stat().st_uid != 0 or target.stat().st_mode & 0o077:
260
+ raise ValueError("Installation directory must be root-owned, mode 0700, and not a symlink.")
261
+ for name in ("installation.json", "compose.json", "bridge.env"):
262
+ if (target / name).is_symlink() or not (target / name).is_file():
263
+ raise ValueError("Incomplete installation; inspect retained files before recovery.")
264
+ if json.loads((target / "installation.json").read_text()).get("schema") != 1:
265
+ raise ValueError("Unrecognized installation metadata.")
266
+ if args.update:
267
+ print("Update only the bridge container. Preserve Redis, Caddy, tokens and the configured image mirror. Retain the previous image for rollback.")
268
+ if confirm("Build and deploy the new bridge with a brief connection interruption?"):
269
+ update(Path(__file__).resolve().parent.parent, target)
270
+ return
271
+ if confirm("Start the existing bridge and enable Docker at boot, preserving its configuration?"):
272
+ run(["systemctl", "enable", "--now", "docker"])
273
+ start(target)
274
+ return
275
+ if target.exists() or target.is_symlink():
276
+ raise ValueError("/opt/marifold-bridge already exists. Use --start for a managed installation; setup will not overwrite it.")
277
+ print("Marifold bridge setup: Docker Compose on Linux, persistent containers with reboot recovery.")
278
+ print("No changes to an existing Redis configuration, firewall rules, DNS or other projects.")
279
+ print("Redis: 1) Create a separate persistent Redis on loopback port 32144 (default). 2) Use an existing Redis URL.")
280
+ choice = input("Redis choice [1/2, default 1]: ").strip() or "1"
281
+ if choice not in ("1", "2"):
282
+ raise ValueError("Choose 1 or 2.")
283
+ redis_url = None
284
+ if choice == "2":
285
+ print("Existing Redis must allow PING and bridge commands, persistence and non-eviction. Setup only tests PING; it does not configure or scan that database.")
286
+ redis_url = validate_redis(getpass.getpass("Redis URL (hidden): "))
287
+ print("HTTPS: 1) Set up Caddy for a domain. 2) Use an existing proxy/Cloudflare Tunnel (default).")
288
+ ingress = input("HTTPS choice [1/2, default 2]: ").strip() or "2"
289
+ if ingress not in ("1", "2"):
290
+ raise ValueError("Choose 1 or 2.")
291
+ domain = validate_domain(input("Bridge domain: ")) if ingress == "1" else None
292
+ ports = [32143] + ([32144] if redis_url is None else []) + ([80, 443] if domain else [])
293
+ for port in ports:
294
+ check_port(port)
295
+ print(f"Install: {target}; Redis: {'separate container' if redis_url is None else 'existing endpoint (credentials hidden)'}; HTTPS: {domain or 'existing ingress'}.")
296
+ print("Downloads Node/Redis/Caddy images as needed, generates a private token, enables Docker at boot and starts services. Container restarts preserve named data volumes.")
297
+ if not confirm("Proceed with this installation and its selected Redis connection?"):
298
+ print("Cancelled; no installation written.")
299
+ return
300
+ run(["systemctl", "enable", "--now", "docker"])
301
+ if run(["docker", "ps", "-a", "--filter", f"label=com.docker.compose.project={PROJECT}", "--format", "{{.ID}}"]):
302
+ raise ValueError("A Compose project with this name already exists; refusing to reuse its resources.")
303
+ if run(["docker", "volume", "ls", "--filter", f"label=com.docker.compose.project={PROJECT}", "--format", "{{.Name}}"]):
304
+ raise ValueError("Volumes from an earlier installation exist; refusing to reuse data with new credentials.")
305
+ source = Path(__file__).resolve().parent.parent
306
+ prepare(source, target, redis_url, domain)
307
+ start(target)
308
+
309
+
310
+ if __name__ == "__main__":
311
+ os.umask(0o077)
312
+ try:
313
+ main()
314
+ except (ValueError, RuntimeError, OSError, EOFError, KeyboardInterrupt) as error:
315
+ if isinstance(error, OSError):
316
+ print("Setup failed due to a filesystem or process error. Existing data was not deleted.", file=sys.stderr)
317
+ else:
318
+ print(f"Setup stopped: {error}", file=sys.stderr)
319
+ sys.exit(1)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ if ! command -v python3 >/dev/null 2>&1; then
4
+ echo 'Python 3 is required. Install your Linux distribution python3 package first.' >&2
5
+ exit 1
6
+ fi
7
+ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
8
+ exec python3 "$script_dir/setup/setup.py" "$@"
@@ -0,0 +1,15 @@
1
+ import type { EncryptedMessage, MessageHeader, Membership, PrivateIdentity, PublicIdentity, SignedMembership } from './types';
2
+ export declare function randomId(): string;
3
+ export declare function digest(value: string): string;
4
+ export declare function publicIdentity(identity: PrivateIdentity): PublicIdentity;
5
+ export declare function createIdentity(): Promise<PrivateIdentity>;
6
+ export declare function signText(identity: PrivateIdentity, text: string): string;
7
+ export declare function verifyText(identity: PublicIdentity, text: string, signature: string): boolean;
8
+ export declare function issueMembership(host: PrivateIdentity, membership: Membership): SignedMembership;
9
+ export declare function validMembership(certificate: SignedMembership, host: PublicIdentity): boolean;
10
+ export declare function encryptMessage(identity: PrivateIdentity, recipient: PublicIdentity, header: MessageHeader, value: unknown): Promise<EncryptedMessage>;
11
+ export declare function decryptMessage(identity: PrivateIdentity, sender: PublicIdentity, message: EncryptedMessage, expected: {
12
+ workspaceId: string;
13
+ recipient: string;
14
+ }): Promise<unknown>;
15
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,eAAe,EACf,cAAc,EACd,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAIjB,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AACD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,cAAc,CAExE;AACD,wBAAsB,cAAc,IAAI,OAAO,CAAC,eAAe,CAAC,CAS/D;AACD,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMxE;AACD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAW7F;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,GAAG,gBAAgB,CAE/F;AACD,wBAAgB,eAAe,CAAC,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAM5F;AACD,wBAAsB,cAAc,CAClC,QAAQ,EAAE,eAAe,EACzB,SAAS,EAAE,cAAc,EACzB,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,gBAAgB,CAAC,CAgB3B;AACD,wBAAsB,cAAc,CAClC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAAC,OAAO,CAAC,CAuBlB"}
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.randomId = randomId;
4
+ exports.digest = digest;
5
+ exports.publicIdentity = publicIdentity;
6
+ exports.createIdentity = createIdentity;
7
+ exports.signText = signText;
8
+ exports.verifyText = verifyText;
9
+ exports.issueMembership = issueMembership;
10
+ exports.validMembership = validMembership;
11
+ exports.encryptMessage = encryptMessage;
12
+ exports.decryptMessage = decryptMessage;
13
+ const node_crypto_1 = require("node:crypto");
14
+ const core_1 = require("@hpke/core");
15
+ const types_1 = require("./types");
16
+ const suite = new core_1.CipherSuite({ kem: new core_1.DhkemP256HkdfSha256(), kdf: new core_1.HkdfSha256(), aead: new core_1.Aes256Gcm() });
17
+ const encoder = new TextEncoder();
18
+ function randomId() {
19
+ return (0, node_crypto_1.randomBytes)(16).toString('hex');
20
+ }
21
+ function digest(value) {
22
+ return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
23
+ }
24
+ function publicIdentity(identity) {
25
+ return { signingKey: identity.signingKey, encryptionKey: identity.encryptionKey };
26
+ }
27
+ async function createIdentity() {
28
+ const keys = (0, node_crypto_1.generateKeyPairSync)('ed25519');
29
+ const encryption = await suite.kem.generateKeyPair();
30
+ return {
31
+ signingKey: keys.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
32
+ signingPrivateKey: keys.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),
33
+ encryptionKey: await crypto.subtle.exportKey('jwk', encryption.publicKey),
34
+ encryptionPrivateKey: await crypto.subtle.exportKey('jwk', encryption.privateKey),
35
+ };
36
+ }
37
+ function signText(identity, text) {
38
+ return (0, node_crypto_1.sign)(null, Buffer.from(text), (0, node_crypto_1.createPrivateKey)({ key: Buffer.from(identity.signingPrivateKey, 'base64'), type: 'pkcs8', format: 'der' })).toString('base64');
39
+ }
40
+ function verifyText(identity, text, signature) {
41
+ try {
42
+ return (0, node_crypto_1.verify)(null, Buffer.from(text), (0, node_crypto_1.createPublicKey)({ key: Buffer.from(identity.signingKey, 'base64'), type: 'spki', format: 'der' }), Buffer.from(signature, 'base64'));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ function issueMembership(host, membership) {
49
+ return { membership, signature: signText(host, JSON.stringify(membership)) };
50
+ }
51
+ function validMembership(certificate, host) {
52
+ return (certificate.membership.version === 1 &&
53
+ certificate.membership.host.signingKey === host.signingKey &&
54
+ verifyText(host, JSON.stringify(certificate.membership), certificate.signature));
55
+ }
56
+ async function encryptMessage(identity, recipient, header, value) {
57
+ const plaintext = encoder.encode(JSON.stringify(value));
58
+ if (plaintext.length > types_1.MAX_FRAME_BYTES / 2)
59
+ throw new Error('Message exceeds frame limit; use bounded transfers.');
60
+ const key = await suite.kem.importKey('jwk', recipient.encryptionKey, true);
61
+ const context = await suite.createSenderContext({
62
+ recipientPublicKey: key,
63
+ info: encoder.encode('marifold.workspace.v1'),
64
+ });
65
+ const ciphertext = Buffer.from(await context.seal(plaintext, (0, types_1.headerBytes)(header))).toString('base64');
66
+ const encapsulatedKey = Buffer.from(context.enc).toString('base64');
67
+ return {
68
+ header,
69
+ encapsulatedKey,
70
+ ciphertext,
71
+ signature: signText(identity, JSON.stringify([header, encapsulatedKey, ciphertext])),
72
+ };
73
+ }
74
+ async function decryptMessage(identity, sender, message, expected) {
75
+ if (Buffer.byteLength(JSON.stringify(message)) > types_1.MAX_FRAME_BYTES)
76
+ throw new Error('Frame too large.');
77
+ const header = (0, types_1.parseHeader)(message.header);
78
+ if (header.workspaceId !== expected.workspaceId || header.recipient !== expected.recipient)
79
+ throw new Error('Message belongs to a different workspace or device.');
80
+ if (header.expiresAt < Date.now() || header.expiresAt > Date.now() + types_1.MESSAGE_TTL_MS + 5000)
81
+ throw new Error('Message expired or has an invalid lifetime.');
82
+ if (!verifyText(sender, JSON.stringify([message.header, message.encapsulatedKey, message.ciphertext]), message.signature))
83
+ throw new Error('Invalid message signature.');
84
+ const key = await suite.kem.importKey('jwk', identity.encryptionPrivateKey, false);
85
+ const context = await suite.createRecipientContext({
86
+ recipientKey: key,
87
+ enc: Buffer.from(message.encapsulatedKey, 'base64'),
88
+ info: encoder.encode('marifold.workspace.v1'),
89
+ });
90
+ const plaintext = await context.open(Buffer.from(message.ciphertext, 'base64'), (0, types_1.headerBytes)(header));
91
+ return JSON.parse(new TextDecoder().decode(plaintext));
92
+ }
93
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './identity';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./identity"), exports);
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAwB;AACxB,6CAA2B"}
@@ -0,0 +1,97 @@
1
+ export declare const PROTOCOL_VERSION = 1;
2
+ export declare const MAX_FRAME_BYTES: number;
3
+ export declare const INVITATION_TTL_MS: number;
4
+ export declare const MESSAGE_TTL_MS = 60000;
5
+ export declare const RELAY_RETENTION_MS: number;
6
+ export interface PublicIdentity {
7
+ signingKey: string;
8
+ encryptionKey: JsonWebKey;
9
+ }
10
+ export interface PrivateIdentity extends PublicIdentity {
11
+ signingPrivateKey: string;
12
+ encryptionPrivateKey: JsonWebKey;
13
+ }
14
+ export interface Membership {
15
+ version: 1;
16
+ workspaceId: string;
17
+ deviceId: string;
18
+ name: string;
19
+ identity: PublicIdentity;
20
+ host: PublicIdentity;
21
+ issuedAt: number;
22
+ }
23
+ export interface SignedMembership {
24
+ membership: Membership;
25
+ signature: string;
26
+ }
27
+ export interface Invitation {
28
+ version: 1;
29
+ workspaceId: string;
30
+ bridgeUrl: string;
31
+ host: PublicIdentity;
32
+ secret: string;
33
+ expiresAt: number;
34
+ }
35
+ export interface WorkspaceDevice {
36
+ id: string;
37
+ name: string;
38
+ platform: string;
39
+ architecture: string;
40
+ executor: boolean;
41
+ online: boolean;
42
+ }
43
+ export interface WorkspaceSummary {
44
+ executor?: boolean;
45
+ id: string;
46
+ name: string;
47
+ role: 'host' | 'guest';
48
+ bridgeUrl: string;
49
+ deviceId: string;
50
+ hostDeviceId: string;
51
+ online: boolean;
52
+ }
53
+ export interface WorkspaceExecutionContext {
54
+ workspaceId: string;
55
+ originDeviceId: string;
56
+ executionDeviceId: string;
57
+ }
58
+ export interface MessageHeader {
59
+ version: 1;
60
+ workspaceId: string;
61
+ sender: string;
62
+ recipient: string;
63
+ id: string;
64
+ expiresAt: number;
65
+ }
66
+ export interface EncryptedMessage {
67
+ header: MessageHeader;
68
+ encapsulatedKey: string;
69
+ ciphertext: string;
70
+ signature: string;
71
+ }
72
+ export interface WorkspaceRequest {
73
+ type: 'request';
74
+ id: string;
75
+ operation: string;
76
+ input: unknown;
77
+ }
78
+ export interface WorkspaceResponse {
79
+ type: 'response';
80
+ id: string;
81
+ ok: boolean;
82
+ value?: unknown;
83
+ error?: string;
84
+ }
85
+ export interface WorkspaceEvent {
86
+ type: 'event';
87
+ topic: string;
88
+ value: unknown;
89
+ }
90
+ export type WorkspaceMessage = WorkspaceRequest | WorkspaceResponse | WorkspaceEvent;
91
+ export declare function record(value: unknown): Record<string, unknown>;
92
+ export declare function identifier(value: unknown): string;
93
+ export declare function label(value: unknown): string;
94
+ export declare function bridgeOrigin(input: string): string;
95
+ export declare function parseHeader(value: unknown): MessageHeader;
96
+ export declare function headerBytes(h: MessageHeader): Uint8Array;
97
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RELAY_RETENTION_MS = exports.MESSAGE_TTL_MS = exports.INVITATION_TTL_MS = exports.MAX_FRAME_BYTES = exports.PROTOCOL_VERSION = void 0;
4
+ exports.record = record;
5
+ exports.identifier = identifier;
6
+ exports.label = label;
7
+ exports.bridgeOrigin = bridgeOrigin;
8
+ exports.parseHeader = parseHeader;
9
+ exports.headerBytes = headerBytes;
10
+ exports.PROTOCOL_VERSION = 1;
11
+ exports.MAX_FRAME_BYTES = 256 * 1024;
12
+ exports.INVITATION_TTL_MS = 15 * 60_000;
13
+ exports.MESSAGE_TTL_MS = 60_000;
14
+ exports.RELAY_RETENTION_MS = 5 * 60_000;
15
+ function record(value) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ throw new Error('Expected an object.');
18
+ return value;
19
+ }
20
+ function identifier(value) {
21
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,100}$/.test(value))
22
+ throw new Error('Invalid identifier.');
23
+ return value;
24
+ }
25
+ function label(value) {
26
+ if (typeof value !== 'string' || !value.trim() || value.length > 80 || /[\x00-\x1f\x7f]/.test(value))
27
+ throw new Error('Invalid display name.');
28
+ return value.trim();
29
+ }
30
+ function bridgeOrigin(input) {
31
+ const url = new URL(input);
32
+ if (url.username || url.password || url.search || url.hash || url.pathname !== '/')
33
+ throw new Error('Bridge URL must be an origin without credentials.');
34
+ if (url.protocol !== 'https:' &&
35
+ !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))
36
+ throw new Error('Bridge requires HTTPS (HTTP is allowed only on loopback).');
37
+ return url.origin;
38
+ }
39
+ function parseHeader(value) {
40
+ const h = record(value);
41
+ if (h.version !== 1 || typeof h.expiresAt !== 'number' || !Number.isSafeInteger(h.expiresAt))
42
+ throw new Error('Invalid message header.');
43
+ return {
44
+ version: 1,
45
+ workspaceId: identifier(h.workspaceId),
46
+ sender: identifier(h.sender),
47
+ recipient: identifier(h.recipient),
48
+ id: identifier(h.id),
49
+ expiresAt: h.expiresAt,
50
+ };
51
+ }
52
+ function headerBytes(h) {
53
+ return new TextEncoder().encode(JSON.stringify([h.version, h.workspaceId, h.sender, h.recipient, h.id, h.expiresAt]));
54
+ }
55
+ //# sourceMappingURL=types.js.map