@dxos/cli-util 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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/dist/lib/chunk-errors.mjs +11 -0
- package/dist/lib/chunk-errors.mjs.map +1 -0
- package/dist/lib/chunk-server.mjs +22 -12
- package/dist/lib/chunk-server.mjs.map +1 -1
- package/dist/lib/index.mjs +18 -11
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/oauth.mjs.map +1 -1
- package/dist/lib/platform.browser.mjs +9 -2
- package/dist/lib/platform.browser.mjs.map +1 -1
- package/dist/lib/platform.node.mjs +9 -2
- package/dist/lib/platform.node.mjs.map +1 -1
- package/dist/lib/testing.mjs +2 -2
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/types/src/callback/index.d.ts +1 -1
- package/dist/types/src/callback/index.d.ts.map +1 -1
- package/dist/types/src/callback/server.d.ts +6 -1
- package/dist/types/src/callback/server.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/oauth/index.d.ts +1 -1
- package/dist/types/src/oauth/index.d.ts.map +1 -1
- package/dist/types/src/services/index.d.ts +2 -2
- package/dist/types/src/services/index.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -2
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-layer.d.ts +4 -3
- package/dist/types/src/testing/test-layer.d.ts.map +1 -1
- package/dist/types/src/util/Doc.d.ts +1 -1
- package/dist/types/src/util/Doc.d.ts.map +1 -1
- package/dist/types/src/util/errors.d.ts +36 -0
- package/dist/types/src/util/errors.d.ts.map +1 -0
- package/dist/types/src/util/form-builder.d.ts +2 -2
- package/dist/types/src/util/form-builder.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +11 -10
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/options.d.ts +1 -1
- package/dist/types/src/util/options.d.ts.map +1 -1
- package/dist/types/src/util/platform.browser.d.ts +3 -2
- package/dist/types/src/util/platform.browser.d.ts.map +1 -1
- package/dist/types/src/util/platform.node.d.ts +3 -2
- package/dist/types/src/util/platform.node.d.ts.map +1 -1
- package/dist/types/src/util/printer.d.ts +1 -1
- package/dist/types/src/util/printer.d.ts.map +1 -1
- package/dist/types/src/util/space-format.d.ts +2 -2
- package/dist/types/src/util/space-format.d.ts.map +1 -1
- package/dist/types/src/util/space.d.ts +1 -1
- package/dist/types/src/util/space.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -16
- package/src/callback/index.ts +1 -1
- package/src/callback/server.ts +41 -18
- package/src/index.ts +2 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/recovery.ts +1 -1
- package/src/services/index.ts +2 -2
- package/src/testing/index.ts +2 -2
- package/src/testing/test-layer.ts +19 -6
- package/src/util/Doc.ts +1 -1
- package/src/util/errors.ts +11 -0
- package/src/util/form-builder.test.ts +1 -1
- package/src/util/form-builder.ts +2 -2
- package/src/util/index.ts +11 -10
- package/src/util/options.ts +3 -3
- package/src/util/platform.browser.ts +6 -4
- package/src/util/platform.node.ts +6 -4
- package/src/util/printer.ts +1 -1
- package/src/util/space-format.ts +13 -9
- package/src/util/space.test.ts +3 -3
- package/src/util/space.ts +2 -2
- package/src/util/timeout.ts +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseError } from "@dxos/errors";
|
|
2
|
+
//#region src/util/errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* A host capability the CLI reached for was unavailable or refused the call — the clipboard
|
|
5
|
+
* outside a secure context, a browser that could not be launched. The original is the `cause`.
|
|
6
|
+
*/
|
|
7
|
+
var PlatformError = class extends BaseError.extend("PlatformError", "Platform operation failed.") {};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { PlatformError as t };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=chunk-errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-errors.mjs","names":[],"sources":["../../src/util/errors.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { BaseError } from '@dxos/errors';\n\n/**\n * A host capability the CLI reached for was unavailable or refused the call — the clipboard\n * outside a secure context, a browser that could not be launched. The original is the `cause`.\n */\nexport class PlatformError extends BaseError.extend('PlatformError', 'Platform operation failed.') {}\n"],"mappings":";;;;;;AAUA,IAAa,gBAAb,cAAmC,UAAU,OAAO,iBAAiB,4BAA4B,CAAC,CAAC,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as PlatformError } from "./chunk-errors.mjs";
|
|
1
2
|
import { t as CommandConfig } from "./chunk-command-config.mjs";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
3
4
|
import * as Layer from "effect/Layer";
|
|
@@ -5,11 +6,11 @@ import { openBrowser } from "#platform";
|
|
|
5
6
|
import * as Option from "effect/Option";
|
|
6
7
|
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer";
|
|
7
8
|
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as HttpRouter from "effect/http/HttpRouter";
|
|
10
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
11
|
+
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
8
12
|
import * as Ref from "effect/Ref";
|
|
9
13
|
import * as Scope from "effect/Scope";
|
|
10
|
-
import * as HttpRouter from "effect/unstable/http/HttpRouter";
|
|
11
|
-
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
|
|
12
|
-
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
|
|
13
14
|
import { getPort } from "get-port-please";
|
|
14
15
|
//#region src/callback/server.ts
|
|
15
16
|
/** Default timeout for a full browser round-trip. */
|
|
@@ -31,6 +32,8 @@ var getRelayPageHtml = (authUrl) => `
|
|
|
31
32
|
<body></body>
|
|
32
33
|
</html>
|
|
33
34
|
`;
|
|
35
|
+
/** The `error` param is attacker-controlled, and this page is served from the loopback origin. */
|
|
36
|
+
var escapeHtml = (text) => text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
34
37
|
/**
|
|
35
38
|
* Starts a local Bun HTTP server that captures a browser redirect back to `callbackPath`. Generic
|
|
36
39
|
* over the callback shape: all query params are captured and returned; an `error` query param
|
|
@@ -41,8 +44,11 @@ var getRelayPageHtml = (authUrl) => `
|
|
|
41
44
|
* (`/redirect/oauth-recovery`), and the email login link, which lands on `/` after the hub's
|
|
42
45
|
* activation route redirects.
|
|
43
46
|
*/
|
|
44
|
-
var startLocalCallbackServer = (callbackPath, { successMessage = "Authentication successful! You can close this window." } = {}) => Effect.gen(function* () {
|
|
45
|
-
const port = yield* Effect.promise(() => getPort({
|
|
47
|
+
var startLocalCallbackServer = (callbackPath, { successMessage = "Authentication successful! You can close this window.", accept } = {}) => Effect.gen(function* () {
|
|
48
|
+
const port = yield* Effect.promise(() => getPort({
|
|
49
|
+
random: true,
|
|
50
|
+
host: "127.0.0.1"
|
|
51
|
+
}));
|
|
46
52
|
const origin = `http://localhost:${port}`;
|
|
47
53
|
const received = yield* Ref.make(false);
|
|
48
54
|
const outcome = yield* Ref.make(Option.none());
|
|
@@ -56,6 +62,9 @@ var startLocalCallbackServer = (callbackPath, { successMessage = "Authentication
|
|
|
56
62
|
if (Option.isSome(yield* Ref.get(outcome))) return HttpServerResponse.text("Already received.", { status: 400 });
|
|
57
63
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
58
64
|
const params = parseUrl(request.url).searchParams;
|
|
65
|
+
const captured = {};
|
|
66
|
+
for (const [key, value] of params.entries()) captured[key] = value;
|
|
67
|
+
if (accept && !accept(captured)) return HttpServerResponse.text("Not the expected callback.", { status: 400 });
|
|
59
68
|
const error = params.get("error");
|
|
60
69
|
if (error) {
|
|
61
70
|
yield* Ref.set(outcome, Option.some({
|
|
@@ -63,13 +72,11 @@ var startLocalCallbackServer = (callbackPath, { successMessage = "Authentication
|
|
|
63
72
|
reason: error
|
|
64
73
|
}));
|
|
65
74
|
yield* Ref.set(received, true);
|
|
66
|
-
return HttpServerResponse.text(`<html><body><h1>Authentication failed</h1><p>${error}</p></body></html>`, {
|
|
75
|
+
return HttpServerResponse.text(`<html><body><h1>Authentication failed</h1><p>${escapeHtml(error)}</p></body></html>`, {
|
|
67
76
|
status: 400,
|
|
68
77
|
headers: { "Content-Type": "text/html" }
|
|
69
78
|
});
|
|
70
79
|
}
|
|
71
|
-
const captured = {};
|
|
72
|
-
for (const [key, value] of params.entries()) captured[key] = value;
|
|
73
80
|
yield* Ref.set(outcome, Option.some({
|
|
74
81
|
success: true,
|
|
75
82
|
params: captured
|
|
@@ -84,7 +91,10 @@ var startLocalCallbackServer = (callbackPath, { successMessage = "Authentication
|
|
|
84
91
|
const serverLayer = HttpRouter.serve(routes, {
|
|
85
92
|
disableLogger: !verbose,
|
|
86
93
|
disableListenLog: !verbose
|
|
87
|
-
}).pipe(Layer.provide(BunHttpServer.layer({
|
|
94
|
+
}).pipe(Layer.provide(BunHttpServer.layer({
|
|
95
|
+
port,
|
|
96
|
+
hostname: "127.0.0.1"
|
|
97
|
+
})));
|
|
88
98
|
const scope = yield* Scope.make();
|
|
89
99
|
yield* Layer.build(serverLayer).pipe(Scope.provide(scope));
|
|
90
100
|
const waitForResult = (timeoutMs = CALLBACK_TIMEOUT_MS) => Effect.race(Effect.gen(function* () {
|
|
@@ -94,10 +104,10 @@ var startLocalCallbackServer = (callbackPath, { successMessage = "Authentication
|
|
|
94
104
|
}
|
|
95
105
|
const result = yield* Ref.get(outcome);
|
|
96
106
|
return yield* Option.match(result, {
|
|
97
|
-
onNone: () => Effect.fail(
|
|
98
|
-
onSome: (value) => value.success ? Effect.succeed(value.params) : Effect.fail(
|
|
107
|
+
onNone: () => Effect.fail(new PlatformError({ message: "Callback received but no result." })),
|
|
108
|
+
onSome: (value) => value.success ? Effect.succeed(value.params) : Effect.fail(new PlatformError({ message: `Callback reported a failure: ${value.reason}` }))
|
|
99
109
|
});
|
|
100
|
-
}), Effect.sleep(`${timeoutMs} millis`).pipe(Effect.flatMap(() => Effect.fail(
|
|
110
|
+
}), Effect.sleep(`${timeoutMs} millis`).pipe(Effect.flatMap(() => Effect.fail(new PlatformError({ message: "Timed out waiting for the browser callback." })))));
|
|
101
111
|
return {
|
|
102
112
|
port,
|
|
103
113
|
origin,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-server.mjs","names":[],"sources":["../../src/callback/server.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as BunHttpServer from '@effect/platform-bun/BunHttpServer';\nimport * as Effect from 'effect/Effect';\nimport * as Exit from 'effect/Exit';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\nimport * as Ref from 'effect/Ref';\nimport * as Scope from 'effect/Scope';\nimport * as HttpRouter from 'effect/unstable/http/HttpRouter';\nimport * as HttpServerRequest from 'effect/unstable/http/HttpServerRequest';\nimport * as HttpServerResponse from 'effect/unstable/http/HttpServerResponse';\nimport { getPort } from 'get-port-please';\n\nimport { openBrowser } from '#platform';\n\nimport { CommandConfig } from '../services';\n\n/** Default timeout for a full browser round-trip. */\nexport const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Relay page that performs a top-level redirect to Edge's `authUrl`. Edge finalizes the flow\n * by redirecting the browser back to this server's callback path (bsky.social nullifies\n * `window.opener`, so a popup + postMessage relay can't be used).\n */\nconst getRelayPageHtml = (authUrl: string) => `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <script>\n window.location.href = '${authUrl.replace(/'/g, \"\\\\'\")}';\n </script>\n</head>\n<body></body>\n</html>\n`;\n\ntype CallbackOutcome = { success: true; params: Record<string, string> } | { success: false; reason: string };\n\n/**\n * A running local callback server.\n */\nexport type LocalCallbackServer = {\n /** Port the server is listening on. */\n readonly port: number;\n /** Origin (e.g. `http://localhost:1234`) to advertise as the redirect target. */\n readonly origin: string;\n /** Opens `authUrl` in the browser via the local relay page. */\n readonly open: (authUrl: string) => Effect.Effect<void, Error>;\n /** Resolves with the captured callback query params, or fails on an `error` param / timeout. */\n readonly waitForResult: (timeoutMs?: number) => Effect.Effect<Record<string, string>, Error>;\n /** Stops the server. */\n readonly stop: () => Effect.Effect<void>;\n};\n\nexport type LocalCallbackServerOptions = {\n /** Heading shown on the page the browser lands on. Defaults to an authentication wording. */\n readonly successMessage?: string;\n};\n\n/**\n * Starts a local Bun HTTP server that captures a browser redirect back to `callbackPath`. Generic\n * over the callback shape: all query params are captured and returned; an `error` query param\n * fails the wait. `open` additionally relays the browser to a remote auth URL, for flows that\n * start from the CLI rather than from a link the user clicks.\n *\n * Used by the integration-connect flow (`/redirect/oauth`), the identity-recovery login flow\n * (`/redirect/oauth-recovery`), and the email login link, which lands on `/` after the hub's\n * activation route redirects.\n */\nexport const startLocalCallbackServer = (\n callbackPath: `/${string}`,\n { successMessage = 'Authentication successful! You can close this window.' }: LocalCallbackServerOptions = {},\n): Effect.Effect<LocalCallbackServer, Error> =>\n Effect.gen(function* () {\n const port = yield* Effect.promise(() => getPort({ random: true }));\n const origin = `http://localhost:${port}`;\n const received = yield* Ref.make(false);\n const outcome = yield* Ref.make<Option.Option<CallbackOutcome>>(Option.none());\n\n // req.url is only the path + query, so reconstruct a full URL to parse the query string.\n const parseUrl = (rawUrl: string) => new URL(rawUrl.startsWith('http') ? rawUrl : `http://localhost${rawUrl}`);\n\n // v4's router is a service: routes are declared as `Route` values and installed by a layer,\n // rather than folded into an immutable router value.\n const routes = HttpRouter.addAll([\n HttpRouter.route(\n 'GET',\n '/oauth-relay',\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const authUrl = parseUrl(request.url).searchParams.get('authUrl');\n if (!authUrl) {\n return HttpServerResponse.text('Missing authUrl parameter', { status: 400 });\n }\n return HttpServerResponse.text(getRelayPageHtml(authUrl), {\n headers: { 'Content-Type': 'text/html' },\n });\n }),\n ),\n HttpRouter.route(\n 'GET',\n callbackPath,\n Effect.gen(function* () {\n if (Option.isSome(yield* Ref.get(outcome))) {\n return HttpServerResponse.text('Already received.', { status: 400 });\n }\n\n const request = yield* HttpServerRequest.HttpServerRequest;\n const params = parseUrl(request.url).searchParams;\n const error = params.get('error');\n if (error) {\n yield* Ref.set(outcome, Option.some({ success: false, reason: error }));\n yield* Ref.set(received, true);\n return HttpServerResponse.text(`<html><body><h1>Authentication failed</h1><p>${error}</p></body></html>`, {\n status: 400,\n headers: { 'Content-Type': 'text/html' },\n });\n }\n\n const captured: Record<string, string> = {};\n for (const [key, value] of params.entries()) {\n captured[key] = value;\n }\n yield* Ref.set(outcome, Option.some({ success: true, params: captured }));\n yield* Ref.set(received, true);\n return HttpServerResponse.text(`<html><body><h1>${successMessage}</h1></body></html>`, {\n headers: { 'Content-Type': 'text/html' },\n });\n }),\n ),\n ]);\n\n // The flow is interactive, so the router's per-request log lines (and the listen banner) are\n // noise between the user's command and its result; keep them behind `--verbose`.\n const verbose = yield* Effect.serviceOption(CommandConfig).pipe(\n Effect.map(Option.match({ onNone: () => false, onSome: (config) => config.verbose })),\n );\n const serverLayer = HttpRouter.serve(routes, {\n disableLogger: !verbose,\n disableListenLog: !verbose,\n }).pipe(Layer.provide(BunHttpServer.layer({ port })));\n const scope = yield* Scope.make();\n yield* Layer.build(serverLayer).pipe(Scope.provide(scope));\n\n const waitForResult = (timeoutMs: number = CALLBACK_TIMEOUT_MS) =>\n Effect.race(\n Effect.gen(function* () {\n while (true) {\n if (yield* Ref.get(received)) {\n break;\n }\n yield* Effect.sleep('500 millis');\n }\n const result = yield* Ref.get(outcome);\n return yield* Option.match(result, {\n onNone: () => Effect.fail(new Error('Callback received but no result.')),\n onSome: (value) =>\n value.success\n ? Effect.succeed(value.params)\n : Effect.fail(new Error(`Callback reported a failure: ${value.reason}`)),\n });\n }),\n Effect.sleep(`${timeoutMs} millis`).pipe(\n Effect.flatMap(() => Effect.fail(new Error('Timed out waiting for the browser callback.'))),\n ),\n );\n\n return {\n port,\n origin,\n open: (authUrl: string) => openBrowser(`${origin}/oauth-relay?authUrl=${encodeURIComponent(authUrl)}`),\n waitForResult,\n stop: () => Scope.close(scope, Exit.void).pipe(Effect.catch(() => Effect.void)),\n } satisfies LocalCallbackServer;\n });\n"],"mappings":";;;;;;;;;;;;;;;AAqBA,IAAa,sBAAsB;;;;;;AAOnC,IAAM,oBAAoB,YAAoB;;;;;;8BAMhB,QAAQ,QAAQ,MAAM,KAAK,EAAE;;;;;;;;;;;;;;;;AAwC3D,IAAa,4BACX,cACA,EAAE,iBAAiB,4DAAwF,CAAC,MAE5G,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,OAAO,cAAc,QAAQ,EAAE,QAAQ,KAAK,CAAC,CAAC;CAClE,MAAM,SAAS,oBAAoB;CACnC,MAAM,WAAW,OAAO,IAAI,KAAK,KAAK;CACtC,MAAM,UAAU,OAAO,IAAI,KAAqC,OAAO,KAAK,CAAC;CAG7E,MAAM,YAAY,WAAmB,IAAI,IAAI,OAAO,WAAW,MAAM,IAAI,SAAS,mBAAmB,QAAQ;CAI7G,MAAM,SAAS,WAAW,OAAO,CAC/B,WAAW,MACT,OACA,gBACA,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,kBAAkB;EACzC,MAAM,UAAU,SAAS,QAAQ,GAAG,CAAC,CAAC,aAAa,IAAI,SAAS;EAChE,IAAI,CAAC,SACH,OAAO,mBAAmB,KAAK,6BAA6B,EAAE,QAAQ,IAAI,CAAC;EAE7E,OAAO,mBAAmB,KAAK,iBAAiB,OAAO,GAAG,EACxD,SAAS,EAAE,gBAAgB,YAAY,EACzC,CAAC;CACH,CAAC,CACH,GACA,WAAW,MACT,OACA,cACA,OAAO,IAAI,aAAa;EACtB,IAAI,OAAO,OAAO,OAAO,IAAI,IAAI,OAAO,CAAC,GACvC,OAAO,mBAAmB,KAAK,qBAAqB,EAAE,QAAQ,IAAI,CAAC;EAGrE,MAAM,UAAU,OAAO,kBAAkB;EACzC,MAAM,SAAS,SAAS,QAAQ,GAAG,CAAC,CAAC;EACrC,MAAM,QAAQ,OAAO,IAAI,OAAO;EAChC,IAAI,OAAO;GACT,OAAO,IAAI,IAAI,SAAS,OAAO,KAAK;IAAE,SAAS;IAAO,QAAQ;GAAM,CAAC,CAAC;GACtE,OAAO,IAAI,IAAI,UAAU,IAAI;GAC7B,OAAO,mBAAmB,KAAK,gDAAgD,MAAM,qBAAqB;IACxG,QAAQ;IACR,SAAS,EAAE,gBAAgB,YAAY;GACzC,CAAC;EACH;EAEA,MAAM,WAAmC,CAAC;EAC1C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GACxC,SAAS,OAAO;EAElB,OAAO,IAAI,IAAI,SAAS,OAAO,KAAK;GAAE,SAAS;GAAM,QAAQ;EAAS,CAAC,CAAC;EACxE,OAAO,IAAI,IAAI,UAAU,IAAI;EAC7B,OAAO,mBAAmB,KAAK,mBAAmB,eAAe,sBAAsB,EACrF,SAAS,EAAE,gBAAgB,YAAY,EACzC,CAAC;CACH,CAAC,CACH,CACF,CAAC;CAID,MAAM,UAAU,OAAO,OAAO,cAAc,aAAa,CAAC,CAAC,KACzD,OAAO,IAAI,OAAO,MAAM;EAAE,cAAc;EAAO,SAAS,WAAW,OAAO;CAAQ,CAAC,CAAC,CACtF;CACA,MAAM,cAAc,WAAW,MAAM,QAAQ;EAC3C,eAAe,CAAC;EAChB,kBAAkB,CAAC;CACrB,CAAC,CAAC,CAAC,KAAK,MAAM,QAAQ,cAAc,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;CACpD,MAAM,QAAQ,OAAO,MAAM,KAAK;CAChC,OAAO,MAAM,MAAM,WAAW,CAAC,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC;CAEzD,MAAM,iBAAiB,YAAoB,wBACzC,OAAO,KACL,OAAO,IAAI,aAAa;EACtB,OAAO,MAAM;GACX,IAAI,OAAO,IAAI,IAAI,QAAQ,GACzB;GAEF,OAAO,OAAO,MAAM,YAAY;EAClC;EACA,MAAM,SAAS,OAAO,IAAI,IAAI,OAAO;EACrC,OAAO,OAAO,OAAO,MAAM,QAAQ;GACjC,cAAc,OAAO,qBAAK,IAAI,MAAM,kCAAkC,CAAC;GACvE,SAAS,UACP,MAAM,UACF,OAAO,QAAQ,MAAM,MAAM,IAC3B,OAAO,qBAAK,IAAI,MAAM,gCAAgC,MAAM,QAAQ,CAAC;EAC7E,CAAC;CACH,CAAC,GACD,OAAO,MAAM,GAAG,UAAU,QAAQ,CAAC,CAAC,KAClC,OAAO,cAAc,OAAO,qBAAK,IAAI,MAAM,6CAA6C,CAAC,CAAC,CAC5F,CACF;CAEF,OAAO;EACL;EACA;EACA,OAAO,YAAoB,YAAY,GAAG,OAAO,uBAAuB,mBAAmB,OAAO,GAAG;EACrG;EACA,YAAY,MAAM,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,KAAK,OAAO,YAAY,OAAO,IAAI,CAAC;CAChF;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"chunk-server.mjs","names":[],"sources":["../../src/callback/server.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as BunHttpServer from '@effect/platform-bun/BunHttpServer';\nimport * as Effect from 'effect/Effect';\nimport * as Exit from 'effect/Exit';\nimport * as HttpRouter from 'effect/http/HttpRouter';\nimport * as HttpServerRequest from 'effect/http/HttpServerRequest';\nimport * as HttpServerResponse from 'effect/http/HttpServerResponse';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\nimport * as Ref from 'effect/Ref';\nimport * as Scope from 'effect/Scope';\nimport { getPort } from 'get-port-please';\n\nimport { openBrowser } from '#platform';\n\nimport { CommandConfig } from '../services/index.ts';\nimport { PlatformError } from '../util/errors.ts';\n\n/** Default timeout for a full browser round-trip. */\nexport const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Relay page that performs a top-level redirect to Edge's `authUrl`. Edge finalizes the flow\n * by redirecting the browser back to this server's callback path (bsky.social nullifies\n * `window.opener`, so a popup + postMessage relay can't be used).\n */\nconst getRelayPageHtml = (authUrl: string) => `\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <script>\n window.location.href = '${authUrl.replace(/'/g, \"\\\\'\")}';\n </script>\n</head>\n<body></body>\n</html>\n`;\n\n/** The `error` param is attacker-controlled, and this page is served from the loopback origin. */\nconst escapeHtml = (text: string) => text.replace(/[&<>\"']/g, (char) => `&#${char.charCodeAt(0)};`);\n\ntype CallbackOutcome = { success: true; params: Record<string, string> } | { success: false; reason: string };\n\n/**\n * A running local callback server.\n */\nexport type LocalCallbackServer = {\n /** Port the server is listening on. */\n readonly port: number;\n /** Origin (e.g. `http://localhost:1234`) to advertise as the redirect target. */\n readonly origin: string;\n /** Opens `authUrl` in the browser via the local relay page. */\n readonly open: (authUrl: string) => Effect.Effect<void, Error>;\n /** Resolves with the captured callback query params, or fails on an `error` param / timeout. */\n readonly waitForResult: (timeoutMs?: number) => Effect.Effect<Record<string, string>, Error>;\n /** Stops the server. */\n readonly stop: () => Effect.Effect<void>;\n};\n\nexport type LocalCallbackServerOptions = {\n /** Heading shown on the page the browser lands on. Defaults to an authentication wording. */\n readonly successMessage?: string;\n /**\n * Rejects a callback without consuming the one-shot result, so a request that is not the awaited\n * one (another local process, a page probing loopback ports) cannot end the wait.\n */\n readonly accept?: (params: Record<string, string>) => boolean;\n};\n\n/**\n * Starts a local Bun HTTP server that captures a browser redirect back to `callbackPath`. Generic\n * over the callback shape: all query params are captured and returned; an `error` query param\n * fails the wait. `open` additionally relays the browser to a remote auth URL, for flows that\n * start from the CLI rather than from a link the user clicks.\n *\n * Used by the integration-connect flow (`/redirect/oauth`), the identity-recovery login flow\n * (`/redirect/oauth-recovery`), and the email login link, which lands on `/` after the hub's\n * activation route redirects.\n */\nexport const startLocalCallbackServer = (\n callbackPath: `/${string}`,\n { successMessage = 'Authentication successful! You can close this window.', accept }: LocalCallbackServerOptions = {},\n): Effect.Effect<LocalCallbackServer, Error> =>\n Effect.gen(function* () {\n // Probed on IPv4 loopback only: with no host, get-port-please also tries every other interface\n // and fails outright on machines without IPv6.\n const port = yield* Effect.promise(() => getPort({ random: true, host: '127.0.0.1' }));\n const origin = `http://localhost:${port}`;\n const received = yield* Ref.make(false);\n const outcome = yield* Ref.make<Option.Option<CallbackOutcome>>(Option.none());\n\n // req.url is only the path + query, so reconstruct a full URL to parse the query string.\n const parseUrl = (rawUrl: string) => new URL(rawUrl.startsWith('http') ? rawUrl : `http://localhost${rawUrl}`);\n\n // v4's router is a service: routes are declared as `Route` values and installed by a layer,\n // rather than folded into an immutable router value.\n const routes = HttpRouter.addAll([\n HttpRouter.route(\n 'GET',\n '/oauth-relay',\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const authUrl = parseUrl(request.url).searchParams.get('authUrl');\n if (!authUrl) {\n return HttpServerResponse.text('Missing authUrl parameter', { status: 400 });\n }\n return HttpServerResponse.text(getRelayPageHtml(authUrl), {\n headers: { 'Content-Type': 'text/html' },\n });\n }),\n ),\n HttpRouter.route(\n 'GET',\n callbackPath,\n Effect.gen(function* () {\n if (Option.isSome(yield* Ref.get(outcome))) {\n return HttpServerResponse.text('Already received.', { status: 400 });\n }\n\n const request = yield* HttpServerRequest.HttpServerRequest;\n const params = parseUrl(request.url).searchParams;\n const captured: Record<string, string> = {};\n for (const [key, value] of params.entries()) {\n captured[key] = value;\n }\n // Before the error branch too: an unexpected `?error=` must not end the wait either.\n if (accept && !accept(captured)) {\n return HttpServerResponse.text('Not the expected callback.', { status: 400 });\n }\n\n const error = params.get('error');\n if (error) {\n yield* Ref.set(outcome, Option.some({ success: false, reason: error }));\n yield* Ref.set(received, true);\n return HttpServerResponse.text(\n `<html><body><h1>Authentication failed</h1><p>${escapeHtml(error)}</p></body></html>`,\n {\n status: 400,\n headers: { 'Content-Type': 'text/html' },\n },\n );\n }\n\n yield* Ref.set(outcome, Option.some({ success: true, params: captured }));\n yield* Ref.set(received, true);\n return HttpServerResponse.text(`<html><body><h1>${successMessage}</h1></body></html>`, {\n headers: { 'Content-Type': 'text/html' },\n });\n }),\n ),\n ]);\n\n // The flow is interactive, so the router's per-request log lines (and the listen banner) are\n // noise between the user's command and its result; keep them behind `--verbose`.\n const verbose = yield* Effect.serviceOption(CommandConfig).pipe(\n Effect.map(Option.match({ onNone: () => false, onSome: (config) => config.verbose })),\n );\n // Loopback only, like the probe above: the callback carries a credential, so nothing off this\n // machine should reach it, and Bun's dual-stack default fails to bind without IPv6.\n const serverLayer = HttpRouter.serve(routes, {\n disableLogger: !verbose,\n disableListenLog: !verbose,\n }).pipe(Layer.provide(BunHttpServer.layer({ port, hostname: '127.0.0.1' })));\n const scope = yield* Scope.make();\n yield* Layer.build(serverLayer).pipe(Scope.provide(scope));\n\n const waitForResult = (timeoutMs: number = CALLBACK_TIMEOUT_MS) =>\n Effect.race(\n Effect.gen(function* () {\n while (true) {\n if (yield* Ref.get(received)) {\n break;\n }\n yield* Effect.sleep('500 millis');\n }\n const result = yield* Ref.get(outcome);\n return yield* Option.match(result, {\n onNone: () => Effect.fail(new PlatformError({ message: 'Callback received but no result.' })),\n onSome: (value) =>\n value.success\n ? Effect.succeed(value.params)\n : Effect.fail(new PlatformError({ message: `Callback reported a failure: ${value.reason}` })),\n });\n }),\n Effect.sleep(`${timeoutMs} millis`).pipe(\n Effect.flatMap(() =>\n Effect.fail(new PlatformError({ message: 'Timed out waiting for the browser callback.' })),\n ),\n ),\n );\n\n return {\n port,\n origin,\n open: (authUrl: string) => openBrowser(`${origin}/oauth-relay?authUrl=${encodeURIComponent(authUrl)}`),\n waitForResult,\n stop: () => Scope.close(scope, Exit.void).pipe(Effect.catch(() => Effect.void)),\n } satisfies LocalCallbackServer;\n });\n"],"mappings":"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"}
|
package/dist/lib/index.mjs
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
+
import { t as PlatformError } from "./chunk-errors.mjs";
|
|
1
2
|
import { t as CommandConfig } from "./chunk-command-config.mjs";
|
|
2
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import { BaseError } from "@dxos/errors";
|
|
3
5
|
import * as Layer from "effect/Layer";
|
|
4
6
|
import * as Option from "effect/Option";
|
|
5
7
|
import * as Pipeable from "effect/Pipeable";
|
|
6
|
-
import * as Options from "effect/
|
|
8
|
+
import * as Options from "effect/cli/Flag";
|
|
7
9
|
import { Database, Key } from "@dxos/echo";
|
|
8
10
|
import { ClientService } from "@dxos/client";
|
|
9
11
|
import * as Console from "effect/Console";
|
|
10
12
|
import * as Duration from "effect/Duration";
|
|
11
13
|
import * as AppSpace from "@dxos/app-toolkit/AppSpace";
|
|
12
|
-
import { BaseError } from "@dxos/errors";
|
|
13
14
|
import { log } from "@dxos/log";
|
|
14
|
-
import { EdgeReplicationSetting } from "@dxos/protocols/
|
|
15
|
+
import { EdgeReplicationSetting } from "@dxos/protocols/buf/dxos/echo/metadata_pb";
|
|
15
16
|
import { isBun } from "@dxos/util";
|
|
17
|
+
import { anyUnpack } from "@bufbuild/protobuf/wkt";
|
|
16
18
|
import { SpaceState } from "@dxos/client/echo";
|
|
19
|
+
import { toDate } from "@dxos/protocols/buf";
|
|
20
|
+
import { EpochSchema } from "@dxos/protocols/buf/dxos/halo/credentials_pb";
|
|
17
21
|
import * as Config from "effect/Config";
|
|
18
22
|
export * from "#platform";
|
|
19
23
|
//#region \0rolldown/runtime.js
|
|
@@ -311,8 +315,8 @@ var formatBytes = (bytes) => {
|
|
|
311
315
|
//#endregion
|
|
312
316
|
//#region src/util/options.ts
|
|
313
317
|
var Common = {
|
|
314
|
-
functionId: Options.
|
|
315
|
-
spaceId: Options.
|
|
318
|
+
functionId: Options.String("function-id").pipe(Options.withDescription("EDGE Function ID.")),
|
|
319
|
+
spaceId: Options.String("space-id").pipe(Options.withSchema(Key.SpaceId), Options.withDescription("Space ID."))
|
|
316
320
|
};
|
|
317
321
|
//#endregion
|
|
318
322
|
//#region src/util/printer.ts
|
|
@@ -447,7 +451,7 @@ var DEFAULT_OPTIONS = {
|
|
|
447
451
|
var READ_TIMEOUT_SECONDS = 2;
|
|
448
452
|
var tryWithFallback = (label, run, fallback) => Effect.tryPromise(run).pipe(Effect.timeoutOrElse({
|
|
449
453
|
duration: Duration.seconds(READ_TIMEOUT_SECONDS),
|
|
450
|
-
orElse: () => Effect.fail(
|
|
454
|
+
orElse: () => Effect.fail(new PlatformError({ message: `${label} timed out` }))
|
|
451
455
|
}), Effect.catch(() => Effect.succeed(fallback)));
|
|
452
456
|
var tryWithFallbackSync = (read, fallback) => {
|
|
453
457
|
try {
|
|
@@ -463,14 +467,17 @@ var formatSpace = Effect.fn(function* (space, options = {}) {
|
|
|
463
467
|
};
|
|
464
468
|
if (waitSeconds > 0) yield* Effect.tryPromise(() => space.waitUntilReady()).pipe(Effect.timeoutOrElse({
|
|
465
469
|
duration: Duration.seconds(waitSeconds),
|
|
466
|
-
orElse: () => Effect.fail(
|
|
470
|
+
orElse: () => Effect.fail(new PlatformError({ message: "waitUntilReady timed out" }))
|
|
467
471
|
}), Effect.catch(() => Effect.void));
|
|
468
472
|
const state = tryWithFallbackSync(() => space.state.get(), SpaceState.SPACE_INITIALIZING);
|
|
469
473
|
const ready = state === SpaceState.SPACE_READY;
|
|
470
474
|
const metrics = tryWithFallbackSync(() => space.internal.data.metrics, void 0);
|
|
471
|
-
const
|
|
475
|
+
const openedAt = toDate(metrics?.open);
|
|
476
|
+
const readyAt = toDate(metrics?.ready);
|
|
477
|
+
const startup = openedAt && readyAt ? readyAt.getTime() - openedAt.getTime() : void 0;
|
|
472
478
|
const pipeline = tryWithFallbackSync(() => space.internal.data.pipeline, void 0);
|
|
473
|
-
const
|
|
479
|
+
const epochAssertion = pipeline?.currentEpoch?.subject?.assertion;
|
|
480
|
+
const epoch = epochAssertion && anyUnpack(epochAssertion, EpochSchema)?.number;
|
|
474
481
|
const syncState = aggregateSyncState(yield* tryWithFallback("getAutomergeSyncState", () => space.internal.db.getAutomergeSyncState(), { peers: {} }));
|
|
475
482
|
const name = ready ? tryWithFallbackSync(() => space.properties.name, void 0) : "loading...";
|
|
476
483
|
const members = tryWithFallbackSync(() => space.members.get().length, 0);
|
|
@@ -515,10 +522,10 @@ var printSpace = (spaceData) => make({ title: spaceData.name ?? spaceData.id }).
|
|
|
515
522
|
//#endregion
|
|
516
523
|
//#region src/util/timeout.ts
|
|
517
524
|
var withTimeout = Effect.fnUntraced(function* (effect) {
|
|
518
|
-
const duration = (yield* Config.
|
|
525
|
+
const duration = (yield* Config.Int("TIMEOUT").pipe(Config.option)).pipe(Option.map(Duration.millis), Option.getOrElse(() => Duration.infinity));
|
|
519
526
|
return yield* effect.pipe(Effect.timeout(duration));
|
|
520
527
|
});
|
|
521
528
|
//#endregion
|
|
522
|
-
export { Ansi_exports as Ansi, CommandConfig, Common, Doc_exports as Doc, form_builder_exports as FormBuilder, SpaceNotFoundError, flushAndSync, formatBytes, formatSpace, getSpace, print, printList, printSpace, spaceIdWithDefault, spaceLayer, syncAllToEdge, waitForSync, withTimeout, withTypes };
|
|
529
|
+
export { Ansi_exports as Ansi, CommandConfig, Common, Doc_exports as Doc, form_builder_exports as FormBuilder, PlatformError, SpaceNotFoundError, flushAndSync, formatBytes, formatSpace, getSpace, print, printList, printSpace, spaceIdWithDefault, spaceLayer, syncAllToEdge, waitForSync, withTimeout, withTypes };
|
|
523
530
|
|
|
524
531
|
//# sourceMappingURL=index.mjs.map
|
package/dist/lib/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/util/Ansi.ts","../../src/util/Doc.ts","../../src/util/form-builder.ts","../../src/util/format.ts","../../src/util/options.ts","../../src/util/printer.ts","../../src/util/runtime.ts","../../src/util/space.ts","../../src/util/space-format.ts","../../src/util/timeout.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n/**\n * Minimal stand-in for `@effect/printer-ansi/Ansi`.\n *\n * Vendored alongside {@link ./doc.ts}, which carries the full rationale; in short, the package is\n * staying on the Effect 3 line and v4 has no counterpart. Only the styles the CLI uses are kept.\n *\n * `chalk` would cover this file (it is already a catalog dependency of `@dxos/log`), but not\n * `doc.ts`'s layout — so a swap would leave the vendoring in place and add a dependency.\n */\n\n/** An ANSI style: the escape written before the text and the one that restores the default. */\nexport type Ansi = {\n readonly open: string;\n readonly close: string;\n};\n\nconst style = (open: number, close: number): Ansi => ({ open: `\u001b[${open}m`, close: `\u001b[${close}m` });\n\nexport const bold: Ansi = style(1, 22);\nexport const green: Ansi = style(32, 39);\nexport const yellow: Ansi = style(33, 39);\nexport const cyan: Ansi = style(36, 39);\nexport const white: Ansi = style(37, 39);\nexport const blackBright: Ansi = style(90, 39);\n\n/** Applies `self` outside `that`, so closing the inner style restores the outer one. */\nexport const combine = (self: Ansi, that: Ansi): Ansi => ({\n open: self.open + that.open,\n close: that.close + self.close,\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Ansi from './Ansi';\n\n/**\n * Minimal stand-in for `@effect/printer/Doc` (and `AnsiDoc`'s renderer).\n *\n * Vendored: `@effect/printer` peers `effect ^3` permanently, v4 ships no printer, and its sealed\n * CLI-internal renderer exposes no general seam — so depending on either would cost a second copy\n * of Effect for rendering alone.\n *\n * There is no reflow: nothing constructs a `group` or a soft line, so every break is mandatory and\n * rendering is a straight line-by-line walk. Revisit if a v4 printer ships.\n */\n\n/** A rendered line: the styled text, plus the column width its escape sequences do not occupy. */\ntype Line = { text: string; width: number };\n\n/**\n * A document. The type parameter carries the annotation type for source compatibility with\n * `@effect/printer`; it is unused here because annotations are applied as they are rendered.\n */\nexport type Doc<_A = never> = { readonly lines: () => Line[] };\n\nconst make = (lines: () => Line[]): Doc<any> => ({ lines });\n\nconst plain = (text: string): Line => ({ text, width: text.length });\n\n/** Joins two runs of lines, merging at the boundary: only an explicit break starts a new line. */\nconst join = (left: Line[], right: Line[]): Line[] => {\n if (left.length === 0) {\n return right;\n }\n if (right.length === 0) {\n return left;\n }\n const last = left[left.length - 1];\n const first = right[0];\n return [...left.slice(0, -1), { text: last.text + first.text, width: last.width + first.width }, ...right.slice(1)];\n};\n\n/** A literal. Embedded newlines split the text, matching `Doc.string`. */\nexport const string = (value: string): Doc<never> => make(() => value.split('\\n').map(plain));\n\nexport const text = string;\n\n/** A mandatory line break. */\nexport const hardLine: Doc<never> = make(() => [plain(''), plain('')]);\n\n/** A line break. Identical to {@link hardLine} here, since nothing flattens a document. */\nexport const line: Doc<never> = hardLine;\n\nexport const cat = <A>(self: Doc<A>, that: Doc<A>): Doc<A> => make(() => join(self.lines(), that.lines()));\n\nexport const hcat = <A>(docs: Iterable<Doc<A>>): Doc<A> =>\n make(() => [...docs].reduce<Line[]>((acc, next) => join(acc, next.lines()), []));\n\n/** Concatenates with a line break between each document. */\nexport const vsep = <A>(docs: Iterable<Doc<A>>): Doc<A> => make(() => [...docs].flatMap((entry) => entry.lines()));\n\n/** Pads the document out to `width` columns. Padding only; an overrun is left alone. */\nexport const fill =\n (width: number) =>\n <A>(self: Doc<A>): Doc<A> =>\n make(() => {\n const lines = self.lines();\n if (lines.length === 0) {\n return [{ text: ' '.repeat(width), width }];\n }\n const last = lines[lines.length - 1];\n const padding = Math.max(0, width - last.width);\n return [...lines.slice(0, -1), { text: last.text + ' '.repeat(padding), width: last.width + padding }];\n });\n\n/** Indents every line by `columns`. */\nexport const indent = <A>(self: Doc<A>, columns: number): Doc<A> =>\n make(() => {\n const prefix = ' '.repeat(columns);\n return self.lines().map(({ text, width }) => ({ text: prefix + text, width: width + columns }));\n });\n\n/** Applies an ANSI style to every non-empty line, leaving the measured width untouched. */\nexport const annotate = <A>(self: Doc<A>, style: Ansi.Ansi): Doc<A> =>\n make(() =>\n self.lines().map(({ text, width }) => ({ text: text === '' ? text : style.open + text + style.close, width })),\n );\n\n/**\n * Renders a document to a string.\n *\n * The options bag is accepted for source compatibility with `AnsiDoc.render`; the layout it selects\n * is irrelevant without reflow.\n */\nexport const render = <A>(self: Doc<A>, _options?: { readonly style?: string }): string =>\n self\n .lines()\n .map(({ text }) => text)\n .join('\\n');\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport * as Pipeable from 'effect/Pipeable';\n\nimport * as Ansi from './Ansi';\nimport * as Doc from './Doc';\n\nexport type FormBuilderOptions = {\n title?: string;\n prefix?: string;\n};\n\nexport interface FormBuilder extends Pipeable.Pipeable {\n readonly entries: ReadonlyArray<{ key: string; value: Doc.Doc<any>; isNested?: boolean }>;\n readonly title?: string;\n readonly prefix: string;\n}\n\nclass FormBuilderImpl implements FormBuilder {\n readonly entries: Array<{ key: string; value: Doc.Doc<any>; isNested?: boolean }> = [];\n readonly title?: string;\n readonly prefix: string;\n\n constructor(options: FormBuilderOptions = {}) {\n this.title = options.title;\n this.prefix = options.prefix ?? '- ';\n }\n\n pipe() {\n // eslint-disable-next-line prefer-rest-params\n return Pipeable.pipeArguments(this, arguments);\n }\n}\n\n/**\n * Creates a new FormBuilder instance.\n */\nexport const make = (props?: FormBuilderOptions): FormBuilder => new FormBuilderImpl(props);\n\n// Helper to calculate dimensions for formatting\nconst calculateDimensions = (entries: ReadonlyArray<{ key: string }>, prefix: string) => {\n const maxKeyLen = Math.max(0, ...entries.map((entry) => entry.key.length));\n const targetWidth = prefix.length + maxKeyLen + 2;\n return { maxKeyLen, targetWidth };\n};\n\n// Helper to build a formatted key line\nconst buildKeyLine = (prefix: string, key: string, targetWidth: number) => {\n // Use fill to pad the key to targetWidth.\n // Note: We don't add indentation here; indentation is handled by the parent container or nestImpl.\n return Doc.annotate(Doc.fill(targetWidth)(Doc.text(prefix + key + ': ')), Ansi.blackBright);\n};\n\n// Implementation helper\nconst setImpl = <T>(\n builder: FormBuilder,\n key: string,\n value: T,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder => {\n if (value !== undefined) {\n let valueDoc: Doc.Doc<any>;\n if (typeof value === 'object' && value !== null) {\n // Assume it's a Doc.Doc\n valueDoc = value as unknown as Doc.Doc<any>;\n } else {\n valueDoc = Doc.text(String(value));\n }\n\n if (color) {\n const ansi = typeof color === 'function' ? color(value as T) : color;\n valueDoc = Doc.annotate(valueDoc, ansi);\n }\n\n (builder as FormBuilderImpl).entries.push({\n key,\n value: valueDoc,\n isNested: false,\n });\n }\n return builder;\n};\n\n/**\n * Adds a key-value pair to the form.\n */\nexport function set<T>(\n builder: FormBuilder,\n key: string,\n value: T,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder;\nexport function set<T>(\n key: string,\n value: T,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): (builder: FormBuilder) => FormBuilder;\nexport function set<T>(\n builderOrKey: FormBuilder | string,\n keyOrValue?: string | T,\n valueOrColor?: T | Ansi.Ansi | ((value: T) => Ansi.Ansi),\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrKey instanceof FormBuilderImpl) {\n // Direct: set(builder, key, value, color?)\n const builder = builderOrKey;\n const key = keyOrValue as string;\n const value = valueOrColor as T;\n return setImpl(builder, key, value, color);\n } else {\n // Curried: set(key, value, color?)\n const key = builderOrKey as string;\n const value = keyOrValue as T;\n const color = valueOrColor as Ansi.Ansi | ((value: T) => Ansi.Ansi) | undefined;\n return (builder: FormBuilder) => setImpl(builder, key, value, color);\n }\n}\n\n// Implementation helper\nconst nestImpl = (parent: FormBuilder, key: string, builder: FormBuilder): FormBuilder => {\n // Build nested entries without title, directly under the parent field name\n\n // Create a temporary builder without title to ignore it.\n const nestedBuilder: FormBuilder = {\n ...builder,\n title: undefined,\n entries: builder.entries,\n };\n\n // Build content.\n let valueDoc = build(nestedBuilder);\n\n // Indent the content by 2 spaces.\n // This ensures that when this doc is embedded in the parent, it is visually nested.\n valueDoc = Doc.indent(valueDoc, 2);\n\n (parent.entries as Array<{ key: string; value: Doc.Doc<any>; isNested?: boolean }>).push({\n key,\n value: valueDoc,\n isNested: true,\n });\n return parent;\n};\n\n/**\n * Nests another form builder under a key.\n */\nexport function nest(parent: FormBuilder, key: string, builder: FormBuilder): FormBuilder;\nexport function nest(key: string, builder: FormBuilder): (parent: FormBuilder) => FormBuilder;\nexport function nest(\n parentOrKey: FormBuilder | string,\n keyOrBuilder?: string | FormBuilder,\n builder?: FormBuilder,\n): FormBuilder | ((parent: FormBuilder) => FormBuilder) {\n if (parentOrKey instanceof FormBuilderImpl) {\n // Direct: nest(parent, key, builder)\n const parent = parentOrKey;\n const key = keyOrBuilder as string;\n return nestImpl(parent, key, builder!);\n } else {\n // Curried: nest(key, builder)\n const key = parentOrKey as string;\n const builder = keyOrBuilder as FormBuilder;\n return (parent: FormBuilder) => nestImpl(parent, key, builder);\n }\n}\n\n// Implementation helper\nconst optionImpl = <T>(\n builder: FormBuilder,\n key: string,\n value: Option.Option<T>,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder => {\n if (Option.isSome(value)) {\n return setImpl(builder, key, value.value, color);\n }\n return builder;\n};\n\n/**\n * Adds an optional value if it exists.\n */\nexport function option<T>(\n builder: FormBuilder,\n key: string,\n value: Option.Option<T>,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder;\nexport function option<T>(\n key: string,\n value: Option.Option<T>,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): (builder: FormBuilder) => FormBuilder;\nexport function option<T>(\n builderOrKey: FormBuilder | string,\n keyOrValue?: string | Option.Option<T>,\n valueOrColor?: Option.Option<T> | Ansi.Ansi | ((value: T) => Ansi.Ansi),\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrKey instanceof FormBuilderImpl) {\n // Direct: option(builder, key, value, color?)\n const builder = builderOrKey;\n const key = keyOrValue as string;\n const value = valueOrColor as Option.Option<T>;\n return optionImpl(builder, key, value, color);\n } else {\n // Curried: option(key, value, color?)\n const key = builderOrKey as string;\n const value = keyOrValue as Option.Option<T>;\n const color = valueOrColor as Ansi.Ansi | ((value: T) => Ansi.Ansi) | undefined;\n return (builder: FormBuilder) => optionImpl(builder, key, value, color);\n }\n}\n\n// Implementation helper\nconst nestedOptionImpl = (builder: FormBuilder, key: string, value: Option.Option<FormBuilder>): FormBuilder => {\n if (Option.isSome(value)) {\n return nestImpl(builder, key, value.value);\n }\n return builder;\n};\n\n/**\n * Nests an optional form builder if it exists.\n */\nexport function nestedOption(builder: FormBuilder, key: string, value: Option.Option<FormBuilder>): FormBuilder;\nexport function nestedOption(key: string, value: Option.Option<FormBuilder>): (builder: FormBuilder) => FormBuilder;\nexport function nestedOption(\n builderOrKey: FormBuilder | string,\n keyOrValue?: string | Option.Option<FormBuilder>,\n value?: Option.Option<FormBuilder>,\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrKey instanceof FormBuilderImpl) {\n // Direct: nestedOption(builder, key, value)\n const builder = builderOrKey;\n const key = keyOrValue as string;\n return nestedOptionImpl(builder, key, value!);\n } else {\n // Curried: nestedOption(key, value)\n const key = builderOrKey as string;\n const value = keyOrValue as Option.Option<FormBuilder>;\n return (builder: FormBuilder) => nestedOptionImpl(builder, key, value);\n }\n}\n\n// Implementation helper\nconst whenImpl = (builder: FormBuilder, condition: any, ...ops: ((b: FormBuilder) => FormBuilder)[]): FormBuilder => {\n if (condition) {\n for (const op of ops) {\n op(builder);\n }\n }\n return builder;\n};\n\n/**\n * Conditionally executes operations.\n */\nexport function when(builder: FormBuilder, condition: any, ...ops: ((b: FormBuilder) => FormBuilder)[]): FormBuilder;\nexport function when(\n condition: any,\n ...ops: ((b: FormBuilder) => FormBuilder)[]\n): (builder: FormBuilder) => FormBuilder;\nexport function when(\n builderOrCondition: FormBuilder | any,\n conditionOrOp?: any | ((b: FormBuilder) => FormBuilder),\n ...ops: ((b: FormBuilder) => FormBuilder)[]\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrCondition instanceof FormBuilderImpl) {\n // Direct: when(builder, condition, ...ops)\n const builder = builderOrCondition;\n const condition = conditionOrOp;\n return whenImpl(builder, condition, ...ops);\n } else {\n // Curried: when(condition, ...ops)\n const condition = builderOrCondition;\n const allOps = [conditionOrOp, ...ops].filter(\n (op): op is (b: FormBuilder) => FormBuilder => typeof op === 'function',\n );\n return (builder: FormBuilder) => whenImpl(builder, condition, ...allOps);\n }\n}\n\n// Implementation helper\nconst eachImpl = <T>(\n builder: FormBuilder,\n items: T[],\n fn: (item: T) => (b: FormBuilder) => FormBuilder,\n): FormBuilder => {\n items.forEach((item) => fn(item)(builder));\n return builder;\n};\n\n/**\n * Iterates over an array of items.\n */\nexport function each<T>(\n builder: FormBuilder,\n items: T[],\n fn: (item: T) => (b: FormBuilder) => FormBuilder,\n): FormBuilder;\nexport function each<T>(\n items: T[],\n fn: (item: T) => (b: FormBuilder) => FormBuilder,\n): (builder: FormBuilder) => FormBuilder;\nexport function each<T>(\n builderOrItems: FormBuilder | T[],\n itemsOrFn?: T[] | ((item: T) => (b: FormBuilder) => FormBuilder),\n fn?: (item: T) => (b: FormBuilder) => FormBuilder,\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrItems instanceof FormBuilderImpl) {\n // Direct: each(builder, items, fn)\n const builder = builderOrItems;\n const items = itemsOrFn as T[];\n return eachImpl(builder, items, fn!);\n } else {\n // Curried: each(items, fn)\n const items = builderOrItems as T[];\n const fn = itemsOrFn as (item: T) => (b: FormBuilder) => FormBuilder;\n return (builder: FormBuilder) => eachImpl(builder, items, fn);\n }\n}\n\n/**\n * Builds the final document.\n */\nexport const build = (builder: FormBuilder): Doc.Doc<any> => {\n const { targetWidth } = calculateDimensions(builder.entries, builder.prefix);\n const entryLines: Doc.Doc<any>[] = [];\n\n builder.entries.forEach(({ key, value, isNested }) => {\n const keyLine = buildKeyLine(builder.prefix, key, targetWidth);\n if (isNested) {\n // Nested content should start on a new line.\n // value is already indented by nestImpl, so we just cat with hardLine.\n entryLines.push(Doc.hcat([keyLine, Doc.hardLine, value]));\n } else {\n // Single-line value, combine key and value.\n // If the value itself is multiline (e.g. from Doc.string with newlines, though Doc handles that specifically),\n // we might want layout flexibility.\n // For now, standard behavior:\n entryLines.push(Doc.hcat([keyLine, value]));\n }\n });\n\n const entriesDoc = Doc.vsep(entryLines);\n\n if (builder.title) {\n const titleDoc = Doc.hcat([Doc.annotate(Doc.text(builder.title), Ansi.combine(Ansi.bold, Ansi.cyan))]);\n // Join title and entries with a single line break, no extra spacing\n return Doc.cat(titleDoc, Doc.cat(Doc.line, entriesDoc));\n }\n\n return entriesDoc;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nconst BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];\n\n/**\n * Format a byte count using binary units.\n */\nexport const formatBytes = (bytes: number): string => {\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < BYTE_UNITS.length - 1) {\n value /= 1024;\n unit++;\n }\n\n return `${unit === 0 ? value : value.toFixed(1)} ${BYTE_UNITS[unit]}`;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Options from 'effect/unstable/cli/Flag';\n\nimport { Key } from '@dxos/echo';\n\n//\n// Common options.\n// NOTE: Sub-commands should Function.pipe(Options.optional) if required.\n//\n\nexport const Common = {\n functionId: Options.string('function-id').pipe(Options.withDescription('EDGE Function ID.')),\n spaceId: Options.string('space-id').pipe(Options.withSchema(Key.SpaceId), Options.withDescription('Space ID.')),\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Doc from './Doc';\n\n/**\n * Pretty print document.\n */\nexport const print = (doc: Doc.Doc<any>) => Doc.render(doc, { style: 'pretty' });\n\n/**\n * Pretty prints a list of documents with ANSI colors.\n */\nexport const printList = (items: Array<Doc.Doc<any>>) =>\n Doc.render(Doc.vsep(items.map((item) => Doc.cat(item, Doc.hardLine))), { style: 'pretty' });\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { ClientService } from '@dxos/client';\nimport { type Type } from '@dxos/echo';\n\n/** @deprecated Migrate to providing types via plugin capabilities. */\nexport const withTypes: (...types: Type.AnyEntity[]) => Effect.Effect<void, never, ClientService> = (...types) =>\n Effect.gen(function* () {\n const client = yield* ClientService;\n yield* Effect.promise(() => client.addTypes(types));\n });\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Console from 'effect/Console';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\n\nimport * as AppSpace from '@dxos/app-toolkit/AppSpace';\nimport { ClientService } from '@dxos/client';\nimport { type Space } from '@dxos/client/echo';\nimport { Database, type Key } from '@dxos/echo';\nimport { BaseError, type BaseErrorOptions } from '@dxos/errors';\nimport { log } from '@dxos/log';\nimport { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';\nimport { isBun } from '@dxos/util';\n\nimport { CommandConfig } from '../services';\n\n/** Matches the budget `Space.syncToEdge` gives its own wait, so neither half of a drain dominates. */\nconst SPACE_READY_TIMEOUT = Duration.seconds(60);\n\nexport const getSpace = (spaceId: Key.SpaceId): Effect.Effect<Space, SpaceNotFoundError, ClientService> =>\n Effect.gen(function* () {\n const client = yield* ClientService;\n // v4 no longer yields an `Option` as an `Effect`, so the miss fails directly.\n const space = client.spaces.get(spaceId);\n return space ?? (yield* Effect.fail(new SpaceNotFoundError(spaceId)));\n });\n\nexport const spaceIdWithDefault = (spaceId: Option.Option<Key.SpaceId>) =>\n Effect.gen(function* () {\n const client = yield* ClientService;\n return Option.getOrElse(spaceId, () => {\n const defaultSpace = AppSpace.getDefaultSpace(client);\n if (!defaultSpace) {\n throw new Error('No space ID provided and no default space found.');\n }\n return defaultSpace.id;\n });\n });\n\n// TODO(wittjosiah): Factor out.\nexport const spaceLayer = (\n spaceId$: Option.Option<Key.SpaceId>,\n fallbackToPersonalSpace = false,\n): Layer.Layer<Database.Service, never, ClientService> => {\n const getSpace = Effect.fn(function* () {\n const client = yield* ClientService;\n\n // Resolution order when fallbackToPersonalSpace is true:\n // 1. the explicit spaceId arg (if provided);\n // 2. the space designated as default by the settings space;\n // 3. the first user-visible space.\n // This keeps profiles created outside composer-app (which is what designates\n // a default space on identity creation) usable — the alternative\n // is a \"Space not found\" throw deep inside CredentialsService.\n const resolveSpace = () => {\n if (!fallbackToPersonalSpace) {\n return spaceId$.pipe(Option.flatMap((id) => Option.fromNullishOr(client.spaces.get(id))));\n }\n return spaceId$.pipe(\n Option.flatMap((id) => Option.fromNullishOr(client.spaces.get(id))),\n Option.orElse(() => Option.fromNullishOr(AppSpace.getDefaultSpace(client))),\n // Not the raw first space: the settings space is created first and holds app config only.\n Option.orElse(() => Option.fromNullishOr(client.spaces.get().find(AppSpace.isVisibleSpace))),\n );\n };\n\n const space = resolveSpace().pipe(Option.getOrUndefined);\n\n if (space) {\n yield* Effect.promise(() => space.waitUntilReady());\n }\n return space;\n });\n\n // When no space can be resolved we install a stub whose `db` getter throws\n // on access — preserves the existing semantics for commands that *do* need\n // a db — but the release callback must NOT touch `db` or it will throw\n // during teardown (e.g. after a command emits a friendly error and\n // returns early). A shared sentinel object short-circuits the release.\n const NO_DB_STUB = {\n get db(): Database.Database {\n throw new Error('Space not found');\n },\n };\n const db = Layer.effect(\n Database.Service,\n Effect.acquireRelease(\n Effect.gen(function* () {\n const space = yield* getSpace();\n if (!space) {\n return NO_DB_STUB;\n }\n return { db: space.db };\n }),\n (holder) => (holder === NO_DB_STUB ? Effect.void : Effect.promise(() => holder.db.flush())),\n ),\n );\n\n return db;\n};\n\n/**\n * Block until `space` is fully replicated to EDGE, enabling replication first if the preference is\n * off. Reports whether it ran: p2p networking is unavailable outside bun, which is how a test run\n * is recognised.\n */\n// TODO(dmaretskyi): There a race condition with edge connection not showing up.\nconst syncSpaceToEdge = Effect.fn(function* (space: Space) {\n // TODO(wittjosiah): Find a better way to do this.\n if (!isBun()) {\n return false;\n }\n\n // TODO(wittjosiah): This should probably be prompted for.\n if (space.internal.data.edgeReplication !== EdgeReplicationSetting.ENABLED) {\n log.info('enabling edge replication', { spaceId: space.id });\n yield* Effect.promise(() => space.internal.setEdgeReplicationPreference(EdgeReplicationSetting.ENABLED));\n }\n\n yield* Effect.promise(() =>\n space.internal.syncToEdge({\n onProgress: (state) => log.info('syncing', { state: state ?? 'no connection to edge' }),\n }),\n );\n return true;\n});\n\n/**\n * Block until `space` is fully replicated to EDGE, reporting completion on stdout. A no-op outside\n * bun, which prints nothing.\n */\nexport const waitForSync = Effect.fn(function* (space: Space) {\n if (yield* syncSpaceToEdge(space)) {\n yield* Console.log('Sync complete');\n }\n});\n\nexport const flushAndSync = Effect.fn(function* (opts?: Database.FlushOptions) {\n yield* Database.flush(opts);\n const spaceId = yield* Database.spaceId;\n const space = yield* getSpace(spaceId);\n yield* waitForSync(space);\n});\n\n/**\n * Flush and sync every space of the current identity to EDGE.\n *\n * `dx` force-exits the moment a command returns, so replication that is merely queued is lost with\n * the process. A command that provisions spaces (`dx account signup`) must therefore drain all of\n * them, not just the one it wrote to — the settings space carries the default-space designation,\n * without which the next device resolves no default space at all.\n */\nexport const syncAllToEdge = Effect.fn(function* () {\n const { json } = yield* CommandConfig;\n const client = yield* ClientService;\n const spaces = client.spaces.get();\n // One line for the batch, never one per space: `waitForSync`'s per-space chatter would drown the\n // command's own output, and any of it on stdout makes `--json` unparseable.\n if (!json && spaces.length > 0) {\n yield* Console.log(`Syncing ${spaces.length} space(s) to EDGE...`);\n }\n\n // Concurrent, because each space's sync is a wait on its own replication round: serially, a\n // profile with N spaces pays N times the per-space timeout before the command can exit.\n yield* Effect.forEach(\n spaces,\n Effect.fn(function* (space: Space) {\n // Bounded, unlike the raw promise: a space whose initialization never lands would otherwise\n // hold the command open forever, and `syncToEdge` below only budgets its own wait.\n yield* Effect.promise(() => space.waitUntilReady()).pipe(Effect.timeout(SPACE_READY_TIMEOUT));\n yield* Effect.promise(() => space.db.flush());\n yield* syncSpaceToEdge(space);\n }),\n { concurrency: 'unbounded', discard: true },\n );\n});\n\n// TODO(burdon): Reconcile with @dxos/protocols\nexport class SpaceNotFoundError extends BaseError.extend('SpaceNotFoundError', 'Space not found') {\n constructor(spaceId: string, options?: Omit<BaseErrorOptions, 'context'>) {\n super({ context: { spaceId }, ...options });\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\n\nimport { type Space, SpaceState, type SpaceSyncState } from '@dxos/client/echo';\n\nimport * as FormBuilder from './form-builder';\n\nexport type FormatSpaceOptions = {\n verbose?: boolean;\n truncateKeys?: boolean;\n /**\n * If set, wait up to this many seconds for the space to reach\n * `SPACE_READY` before reading its fields. If unset, read whatever state\n * is available right now — much safer for `space list` etc., where a\n * single stuck space would otherwise hang the entire command.\n */\n waitSeconds?: number;\n};\n\nconst DEFAULT_OPTIONS: Required<FormatSpaceOptions> = {\n verbose: false,\n truncateKeys: false,\n waitSeconds: 0,\n};\n\n/**\n * Per-async-read internal timeout. Some `space.internal.*` getters do\n * filesystem / network IO and can themselves hang on a partially-loaded\n * space; cap each one so the command can never be held hostage by SDK\n * internals.\n */\nconst READ_TIMEOUT_SECONDS = 2;\n\nconst tryWithFallback = <T>(label: string, run: () => Promise<T>, fallback: T) =>\n Effect.tryPromise(run).pipe(\n Effect.timeoutOrElse({\n duration: Duration.seconds(READ_TIMEOUT_SECONDS),\n orElse: () => Effect.fail(new Error(`${label} timed out`)),\n }),\n Effect.catch(() => Effect.succeed(fallback)),\n );\n\nconst tryWithFallbackSync = <T>(read: () => T, fallback: T): T => {\n try {\n return read();\n } catch {\n return fallback;\n }\n};\n\n// TODO(wittjosiah): Use @effect/printer.\nexport const formatSpace = Effect.fn(function* (space: Space, options: FormatSpaceOptions = {}) {\n const { waitSeconds } = { ...DEFAULT_OPTIONS, ...options };\n\n // Opt-in wait. Defaults to NO wait so a single stuck space can't hang\n // an enumeration command (e.g. `dx space list`).\n if (waitSeconds > 0) {\n yield* Effect.tryPromise(() => space.waitUntilReady()).pipe(\n Effect.timeoutOrElse({\n duration: Duration.seconds(waitSeconds),\n orElse: () => Effect.fail(new Error('waitUntilReady timed out')),\n }),\n Effect.catch(() => Effect.void),\n );\n }\n\n const state = tryWithFallbackSync(() => space.state.get(), SpaceState.SPACE_INITIALIZING);\n const ready = state === SpaceState.SPACE_READY;\n\n // TODO(burdon): Factor out.\n // TODO(burdon): Agent needs to restart before `ready` is available.\n const metrics = tryWithFallbackSync(\n () => space.internal.data.metrics,\n undefined as { open?: Date; ready?: Date } | undefined,\n );\n const startup = metrics?.open && metrics?.ready ? metrics.ready.getTime() - metrics.open.getTime() : undefined;\n\n // TODO(burdon): Get feeds from client-services if verbose (factor out from devtools/diagnostics).\n // const host = client.services.services.DevtoolsHost!;\n const pipeline = tryWithFallbackSync(() => space.internal.data.pipeline, undefined);\n const epoch = pipeline?.currentEpoch?.subject.assertion.number;\n\n // The sync-state read does IO; cap it so a stuck space can't hang the\n // command. Falls back to a \"no peers\" placeholder.\n const syncStateRaw = yield* tryWithFallback(\n 'getAutomergeSyncState',\n () => space.internal.db.getAutomergeSyncState(),\n {\n peers: {},\n } as SpaceSyncState,\n );\n const syncState = aggregateSyncState(syncStateRaw);\n\n const name = ready ? tryWithFallbackSync(() => space.properties.name, undefined) : 'loading...';\n const members = tryWithFallbackSync(() => space.members.get().length, 0);\n const objects = tryWithFallbackSync(() => space.internal.db.getAllObjectIds().length, 0);\n const key = options.truncateKeys\n ? tryWithFallbackSync(() => space.key.truncate(), '')\n : tryWithFallbackSync(() => space.key.toHex(), '');\n\n return {\n id: space.id,\n state: SpaceState[state],\n name,\n\n members,\n objects,\n\n key,\n epoch,\n startup,\n automergeRoot: pipeline?.directoryUrl,\n // appliedEpoch,\n syncState: `${syncState.count} ${getSyncIndicator(syncState.up, syncState.down)} (${syncState.peers} peers)`,\n };\n});\n\nconst aggregateSyncState = (syncState: SpaceSyncState) => {\n const peers = Object.keys(syncState.peers ?? {}).length;\n const missingOnLocal = Math.max(...Object.values(syncState.peers ?? {}).map((peer) => peer.missingOnLocal), 0);\n const missingOnRemote = Math.max(...Object.values(syncState.peers ?? {}).map((peer) => peer.missingOnRemote), 0);\n const differentDocuments = Math.max(\n ...Object.values(syncState.peers ?? {}).map((peer) => peer.differentDocuments),\n 0,\n );\n\n return {\n count: missingOnLocal + missingOnRemote + differentDocuments,\n peers,\n up: missingOnRemote > 0 || differentDocuments > 0,\n down: missingOnLocal > 0 || differentDocuments > 0,\n };\n};\n\nconst getSyncIndicator = (up: boolean, down: boolean) => {\n if (up) {\n if (down) {\n return '⇅';\n } else {\n return '↑';\n }\n } else {\n if (down) {\n return '↓';\n } else {\n return '✓';\n }\n }\n};\n\nexport type FormattedSpace = {\n id: string;\n state: string;\n name: string | undefined;\n members: number;\n objects: number;\n key: string;\n epoch: any;\n startup: number | undefined;\n automergeRoot: string | undefined;\n syncState: string;\n};\n\n/**\n * Pretty prints a space with ANSI colors.\n */\nexport const printSpace = (spaceData: FormattedSpace) =>\n FormBuilder.make({ title: spaceData.name ?? spaceData.id }).pipe(\n FormBuilder.set('id', spaceData.id),\n FormBuilder.set('state', spaceData.state),\n FormBuilder.set('key', spaceData.key),\n FormBuilder.set('members', spaceData.members),\n FormBuilder.set('objects', spaceData.objects),\n FormBuilder.set('epoch', spaceData.epoch ?? '<none>'),\n FormBuilder.set('startup', spaceData.startup ? `${spaceData.startup}ms` : '<none>'),\n FormBuilder.set('syncState', spaceData.syncState),\n FormBuilder.set('automergeRoot', spaceData.automergeRoot ?? '<none>'),\n FormBuilder.build,\n );\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport type * as Cause from 'effect/Cause';\nimport * as Config from 'effect/Config';\nimport type * as ConfigError from 'effect/Config';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nexport const withTimeout: <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n) => Effect.Effect<A, Cause.TimeoutError | ConfigError.ConfigError | E, R> = Effect.fnUntraced(function* (effect) {\n const timeout = yield* Config.int('TIMEOUT').pipe(Config.option);\n const duration = timeout.pipe(\n Option.map(Duration.millis),\n Option.getOrElse(() => Duration.infinity),\n );\n return yield* effect.pipe(Effect.timeout(duration));\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/util/Ansi.ts","../../src/util/Doc.ts","../../src/util/form-builder.ts","../../src/util/format.ts","../../src/util/options.ts","../../src/util/printer.ts","../../src/util/runtime.ts","../../src/util/space.ts","../../src/util/space-format.ts","../../src/util/timeout.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n/**\n * Minimal stand-in for `@effect/printer-ansi/Ansi`.\n *\n * Vendored alongside {@link ./doc.ts}, which carries the full rationale; in short, the package is\n * staying on the Effect 3 line and v4 has no counterpart. Only the styles the CLI uses are kept.\n *\n * `chalk` would cover this file (it is already a catalog dependency of `@dxos/log`), but not\n * `doc.ts`'s layout — so a swap would leave the vendoring in place and add a dependency.\n */\n\n/** An ANSI style: the escape written before the text and the one that restores the default. */\nexport type Ansi = {\n readonly open: string;\n readonly close: string;\n};\n\nconst style = (open: number, close: number): Ansi => ({ open: `\u001b[${open}m`, close: `\u001b[${close}m` });\n\nexport const bold: Ansi = style(1, 22);\nexport const green: Ansi = style(32, 39);\nexport const yellow: Ansi = style(33, 39);\nexport const cyan: Ansi = style(36, 39);\nexport const white: Ansi = style(37, 39);\nexport const blackBright: Ansi = style(90, 39);\n\n/** Applies `self` outside `that`, so closing the inner style restores the outer one. */\nexport const combine = (self: Ansi, that: Ansi): Ansi => ({\n open: self.open + that.open,\n close: that.close + self.close,\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Ansi from './Ansi.ts';\n\n/**\n * Minimal stand-in for `@effect/printer/Doc` (and `AnsiDoc`'s renderer).\n *\n * Vendored: `@effect/printer` peers `effect ^3` permanently, v4 ships no printer, and its sealed\n * CLI-internal renderer exposes no general seam — so depending on either would cost a second copy\n * of Effect for rendering alone.\n *\n * There is no reflow: nothing constructs a `group` or a soft line, so every break is mandatory and\n * rendering is a straight line-by-line walk. Revisit if a v4 printer ships.\n */\n\n/** A rendered line: the styled text, plus the column width its escape sequences do not occupy. */\ntype Line = { text: string; width: number };\n\n/**\n * A document. The type parameter carries the annotation type for source compatibility with\n * `@effect/printer`; it is unused here because annotations are applied as they are rendered.\n */\nexport type Doc<_A = never> = { readonly lines: () => Line[] };\n\nconst make = (lines: () => Line[]): Doc<any> => ({ lines });\n\nconst plain = (text: string): Line => ({ text, width: text.length });\n\n/** Joins two runs of lines, merging at the boundary: only an explicit break starts a new line. */\nconst join = (left: Line[], right: Line[]): Line[] => {\n if (left.length === 0) {\n return right;\n }\n if (right.length === 0) {\n return left;\n }\n const last = left[left.length - 1];\n const first = right[0];\n return [...left.slice(0, -1), { text: last.text + first.text, width: last.width + first.width }, ...right.slice(1)];\n};\n\n/** A literal. Embedded newlines split the text, matching `Doc.string`. */\nexport const string = (value: string): Doc<never> => make(() => value.split('\\n').map(plain));\n\nexport const text = string;\n\n/** A mandatory line break. */\nexport const hardLine: Doc<never> = make(() => [plain(''), plain('')]);\n\n/** A line break. Identical to {@link hardLine} here, since nothing flattens a document. */\nexport const line: Doc<never> = hardLine;\n\nexport const cat = <A>(self: Doc<A>, that: Doc<A>): Doc<A> => make(() => join(self.lines(), that.lines()));\n\nexport const hcat = <A>(docs: Iterable<Doc<A>>): Doc<A> =>\n make(() => [...docs].reduce<Line[]>((acc, next) => join(acc, next.lines()), []));\n\n/** Concatenates with a line break between each document. */\nexport const vsep = <A>(docs: Iterable<Doc<A>>): Doc<A> => make(() => [...docs].flatMap((entry) => entry.lines()));\n\n/** Pads the document out to `width` columns. Padding only; an overrun is left alone. */\nexport const fill =\n (width: number) =>\n <A>(self: Doc<A>): Doc<A> =>\n make(() => {\n const lines = self.lines();\n if (lines.length === 0) {\n return [{ text: ' '.repeat(width), width }];\n }\n const last = lines[lines.length - 1];\n const padding = Math.max(0, width - last.width);\n return [...lines.slice(0, -1), { text: last.text + ' '.repeat(padding), width: last.width + padding }];\n });\n\n/** Indents every line by `columns`. */\nexport const indent = <A>(self: Doc<A>, columns: number): Doc<A> =>\n make(() => {\n const prefix = ' '.repeat(columns);\n return self.lines().map(({ text, width }) => ({ text: prefix + text, width: width + columns }));\n });\n\n/** Applies an ANSI style to every non-empty line, leaving the measured width untouched. */\nexport const annotate = <A>(self: Doc<A>, style: Ansi.Ansi): Doc<A> =>\n make(() =>\n self.lines().map(({ text, width }) => ({ text: text === '' ? text : style.open + text + style.close, width })),\n );\n\n/**\n * Renders a document to a string.\n *\n * The options bag is accepted for source compatibility with `AnsiDoc.render`; the layout it selects\n * is irrelevant without reflow.\n */\nexport const render = <A>(self: Doc<A>, _options?: { readonly style?: string }): string =>\n self\n .lines()\n .map(({ text }) => text)\n .join('\\n');\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Option from 'effect/Option';\nimport * as Pipeable from 'effect/Pipeable';\n\nimport * as Ansi from './Ansi.ts';\nimport * as Doc from './Doc.ts';\n\nexport type FormBuilderOptions = {\n title?: string;\n prefix?: string;\n};\n\nexport interface FormBuilder extends Pipeable.Pipeable {\n readonly entries: ReadonlyArray<{ key: string; value: Doc.Doc<any>; isNested?: boolean }>;\n readonly title?: string;\n readonly prefix: string;\n}\n\nclass FormBuilderImpl implements FormBuilder {\n readonly entries: Array<{ key: string; value: Doc.Doc<any>; isNested?: boolean }> = [];\n readonly title?: string;\n readonly prefix: string;\n\n constructor(options: FormBuilderOptions = {}) {\n this.title = options.title;\n this.prefix = options.prefix ?? '- ';\n }\n\n pipe() {\n // eslint-disable-next-line prefer-rest-params\n return Pipeable.pipeArguments(this, arguments);\n }\n}\n\n/**\n * Creates a new FormBuilder instance.\n */\nexport const make = (props?: FormBuilderOptions): FormBuilder => new FormBuilderImpl(props);\n\n// Helper to calculate dimensions for formatting\nconst calculateDimensions = (entries: ReadonlyArray<{ key: string }>, prefix: string) => {\n const maxKeyLen = Math.max(0, ...entries.map((entry) => entry.key.length));\n const targetWidth = prefix.length + maxKeyLen + 2;\n return { maxKeyLen, targetWidth };\n};\n\n// Helper to build a formatted key line\nconst buildKeyLine = (prefix: string, key: string, targetWidth: number) => {\n // Use fill to pad the key to targetWidth.\n // Note: We don't add indentation here; indentation is handled by the parent container or nestImpl.\n return Doc.annotate(Doc.fill(targetWidth)(Doc.text(prefix + key + ': ')), Ansi.blackBright);\n};\n\n// Implementation helper\nconst setImpl = <T>(\n builder: FormBuilder,\n key: string,\n value: T,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder => {\n if (value !== undefined) {\n let valueDoc: Doc.Doc<any>;\n if (typeof value === 'object' && value !== null) {\n // Assume it's a Doc.Doc\n valueDoc = value as unknown as Doc.Doc<any>;\n } else {\n valueDoc = Doc.text(String(value));\n }\n\n if (color) {\n const ansi = typeof color === 'function' ? color(value as T) : color;\n valueDoc = Doc.annotate(valueDoc, ansi);\n }\n\n (builder as FormBuilderImpl).entries.push({\n key,\n value: valueDoc,\n isNested: false,\n });\n }\n return builder;\n};\n\n/**\n * Adds a key-value pair to the form.\n */\nexport function set<T>(\n builder: FormBuilder,\n key: string,\n value: T,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder;\nexport function set<T>(\n key: string,\n value: T,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): (builder: FormBuilder) => FormBuilder;\nexport function set<T>(\n builderOrKey: FormBuilder | string,\n keyOrValue?: string | T,\n valueOrColor?: T | Ansi.Ansi | ((value: T) => Ansi.Ansi),\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrKey instanceof FormBuilderImpl) {\n // Direct: set(builder, key, value, color?)\n const builder = builderOrKey;\n const key = keyOrValue as string;\n const value = valueOrColor as T;\n return setImpl(builder, key, value, color);\n } else {\n // Curried: set(key, value, color?)\n const key = builderOrKey as string;\n const value = keyOrValue as T;\n const color = valueOrColor as Ansi.Ansi | ((value: T) => Ansi.Ansi) | undefined;\n return (builder: FormBuilder) => setImpl(builder, key, value, color);\n }\n}\n\n// Implementation helper\nconst nestImpl = (parent: FormBuilder, key: string, builder: FormBuilder): FormBuilder => {\n // Build nested entries without title, directly under the parent field name\n\n // Create a temporary builder without title to ignore it.\n const nestedBuilder: FormBuilder = {\n ...builder,\n title: undefined,\n entries: builder.entries,\n };\n\n // Build content.\n let valueDoc = build(nestedBuilder);\n\n // Indent the content by 2 spaces.\n // This ensures that when this doc is embedded in the parent, it is visually nested.\n valueDoc = Doc.indent(valueDoc, 2);\n\n (parent.entries as Array<{ key: string; value: Doc.Doc<any>; isNested?: boolean }>).push({\n key,\n value: valueDoc,\n isNested: true,\n });\n return parent;\n};\n\n/**\n * Nests another form builder under a key.\n */\nexport function nest(parent: FormBuilder, key: string, builder: FormBuilder): FormBuilder;\nexport function nest(key: string, builder: FormBuilder): (parent: FormBuilder) => FormBuilder;\nexport function nest(\n parentOrKey: FormBuilder | string,\n keyOrBuilder?: string | FormBuilder,\n builder?: FormBuilder,\n): FormBuilder | ((parent: FormBuilder) => FormBuilder) {\n if (parentOrKey instanceof FormBuilderImpl) {\n // Direct: nest(parent, key, builder)\n const parent = parentOrKey;\n const key = keyOrBuilder as string;\n return nestImpl(parent, key, builder!);\n } else {\n // Curried: nest(key, builder)\n const key = parentOrKey as string;\n const builder = keyOrBuilder as FormBuilder;\n return (parent: FormBuilder) => nestImpl(parent, key, builder);\n }\n}\n\n// Implementation helper\nconst optionImpl = <T>(\n builder: FormBuilder,\n key: string,\n value: Option.Option<T>,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder => {\n if (Option.isSome(value)) {\n return setImpl(builder, key, value.value, color);\n }\n return builder;\n};\n\n/**\n * Adds an optional value if it exists.\n */\nexport function option<T>(\n builder: FormBuilder,\n key: string,\n value: Option.Option<T>,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder;\nexport function option<T>(\n key: string,\n value: Option.Option<T>,\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): (builder: FormBuilder) => FormBuilder;\nexport function option<T>(\n builderOrKey: FormBuilder | string,\n keyOrValue?: string | Option.Option<T>,\n valueOrColor?: Option.Option<T> | Ansi.Ansi | ((value: T) => Ansi.Ansi),\n color?: Ansi.Ansi | ((value: T) => Ansi.Ansi),\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrKey instanceof FormBuilderImpl) {\n // Direct: option(builder, key, value, color?)\n const builder = builderOrKey;\n const key = keyOrValue as string;\n const value = valueOrColor as Option.Option<T>;\n return optionImpl(builder, key, value, color);\n } else {\n // Curried: option(key, value, color?)\n const key = builderOrKey as string;\n const value = keyOrValue as Option.Option<T>;\n const color = valueOrColor as Ansi.Ansi | ((value: T) => Ansi.Ansi) | undefined;\n return (builder: FormBuilder) => optionImpl(builder, key, value, color);\n }\n}\n\n// Implementation helper\nconst nestedOptionImpl = (builder: FormBuilder, key: string, value: Option.Option<FormBuilder>): FormBuilder => {\n if (Option.isSome(value)) {\n return nestImpl(builder, key, value.value);\n }\n return builder;\n};\n\n/**\n * Nests an optional form builder if it exists.\n */\nexport function nestedOption(builder: FormBuilder, key: string, value: Option.Option<FormBuilder>): FormBuilder;\nexport function nestedOption(key: string, value: Option.Option<FormBuilder>): (builder: FormBuilder) => FormBuilder;\nexport function nestedOption(\n builderOrKey: FormBuilder | string,\n keyOrValue?: string | Option.Option<FormBuilder>,\n value?: Option.Option<FormBuilder>,\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrKey instanceof FormBuilderImpl) {\n // Direct: nestedOption(builder, key, value)\n const builder = builderOrKey;\n const key = keyOrValue as string;\n return nestedOptionImpl(builder, key, value!);\n } else {\n // Curried: nestedOption(key, value)\n const key = builderOrKey as string;\n const value = keyOrValue as Option.Option<FormBuilder>;\n return (builder: FormBuilder) => nestedOptionImpl(builder, key, value);\n }\n}\n\n// Implementation helper\nconst whenImpl = (builder: FormBuilder, condition: any, ...ops: ((b: FormBuilder) => FormBuilder)[]): FormBuilder => {\n if (condition) {\n for (const op of ops) {\n op(builder);\n }\n }\n return builder;\n};\n\n/**\n * Conditionally executes operations.\n */\nexport function when(builder: FormBuilder, condition: any, ...ops: ((b: FormBuilder) => FormBuilder)[]): FormBuilder;\nexport function when(\n condition: any,\n ...ops: ((b: FormBuilder) => FormBuilder)[]\n): (builder: FormBuilder) => FormBuilder;\nexport function when(\n builderOrCondition: FormBuilder | any,\n conditionOrOp?: any | ((b: FormBuilder) => FormBuilder),\n ...ops: ((b: FormBuilder) => FormBuilder)[]\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrCondition instanceof FormBuilderImpl) {\n // Direct: when(builder, condition, ...ops)\n const builder = builderOrCondition;\n const condition = conditionOrOp;\n return whenImpl(builder, condition, ...ops);\n } else {\n // Curried: when(condition, ...ops)\n const condition = builderOrCondition;\n const allOps = [conditionOrOp, ...ops].filter(\n (op): op is (b: FormBuilder) => FormBuilder => typeof op === 'function',\n );\n return (builder: FormBuilder) => whenImpl(builder, condition, ...allOps);\n }\n}\n\n// Implementation helper\nconst eachImpl = <T>(\n builder: FormBuilder,\n items: T[],\n fn: (item: T) => (b: FormBuilder) => FormBuilder,\n): FormBuilder => {\n items.forEach((item) => fn(item)(builder));\n return builder;\n};\n\n/**\n * Iterates over an array of items.\n */\nexport function each<T>(\n builder: FormBuilder,\n items: T[],\n fn: (item: T) => (b: FormBuilder) => FormBuilder,\n): FormBuilder;\nexport function each<T>(\n items: T[],\n fn: (item: T) => (b: FormBuilder) => FormBuilder,\n): (builder: FormBuilder) => FormBuilder;\nexport function each<T>(\n builderOrItems: FormBuilder | T[],\n itemsOrFn?: T[] | ((item: T) => (b: FormBuilder) => FormBuilder),\n fn?: (item: T) => (b: FormBuilder) => FormBuilder,\n): FormBuilder | ((builder: FormBuilder) => FormBuilder) {\n if (builderOrItems instanceof FormBuilderImpl) {\n // Direct: each(builder, items, fn)\n const builder = builderOrItems;\n const items = itemsOrFn as T[];\n return eachImpl(builder, items, fn!);\n } else {\n // Curried: each(items, fn)\n const items = builderOrItems as T[];\n const fn = itemsOrFn as (item: T) => (b: FormBuilder) => FormBuilder;\n return (builder: FormBuilder) => eachImpl(builder, items, fn);\n }\n}\n\n/**\n * Builds the final document.\n */\nexport const build = (builder: FormBuilder): Doc.Doc<any> => {\n const { targetWidth } = calculateDimensions(builder.entries, builder.prefix);\n const entryLines: Doc.Doc<any>[] = [];\n\n builder.entries.forEach(({ key, value, isNested }) => {\n const keyLine = buildKeyLine(builder.prefix, key, targetWidth);\n if (isNested) {\n // Nested content should start on a new line.\n // value is already indented by nestImpl, so we just cat with hardLine.\n entryLines.push(Doc.hcat([keyLine, Doc.hardLine, value]));\n } else {\n // Single-line value, combine key and value.\n // If the value itself is multiline (e.g. from Doc.string with newlines, though Doc handles that specifically),\n // we might want layout flexibility.\n // For now, standard behavior:\n entryLines.push(Doc.hcat([keyLine, value]));\n }\n });\n\n const entriesDoc = Doc.vsep(entryLines);\n\n if (builder.title) {\n const titleDoc = Doc.hcat([Doc.annotate(Doc.text(builder.title), Ansi.combine(Ansi.bold, Ansi.cyan))]);\n // Join title and entries with a single line break, no extra spacing\n return Doc.cat(titleDoc, Doc.cat(Doc.line, entriesDoc));\n }\n\n return entriesDoc;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nconst BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'];\n\n/**\n * Format a byte count using binary units.\n */\nexport const formatBytes = (bytes: number): string => {\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < BYTE_UNITS.length - 1) {\n value /= 1024;\n unit++;\n }\n\n return `${unit === 0 ? value : value.toFixed(1)} ${BYTE_UNITS[unit]}`;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Options from 'effect/cli/Flag';\n\nimport { Key } from '@dxos/echo';\n\n//\n// Common options.\n// NOTE: Sub-commands should Function.pipe(Options.optional) if required.\n//\n\nexport const Common = {\n functionId: Options.String('function-id').pipe(Options.withDescription('EDGE Function ID.')),\n spaceId: Options.String('space-id').pipe(Options.withSchema(Key.SpaceId), Options.withDescription('Space ID.')),\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Doc from './Doc.ts';\n\n/**\n * Pretty print document.\n */\nexport const print = (doc: Doc.Doc<any>) => Doc.render(doc, { style: 'pretty' });\n\n/**\n * Pretty prints a list of documents with ANSI colors.\n */\nexport const printList = (items: Array<Doc.Doc<any>>) =>\n Doc.render(Doc.vsep(items.map((item) => Doc.cat(item, Doc.hardLine))), { style: 'pretty' });\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { ClientService } from '@dxos/client';\nimport { type Type } from '@dxos/echo';\n\n/** @deprecated Migrate to providing types via plugin capabilities. */\nexport const withTypes: (...types: Type.AnyEntity[]) => Effect.Effect<void, never, ClientService> = (...types) =>\n Effect.gen(function* () {\n const client = yield* ClientService;\n yield* Effect.promise(() => client.addTypes(types));\n });\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Console from 'effect/Console';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Option from 'effect/Option';\n\nimport * as AppSpace from '@dxos/app-toolkit/AppSpace';\nimport { ClientService } from '@dxos/client';\nimport { type Space } from '@dxos/client/echo';\nimport { Database, type Key } from '@dxos/echo';\nimport { BaseError, type BaseErrorOptions } from '@dxos/errors';\nimport { log } from '@dxos/log';\nimport { EdgeReplicationSetting } from '@dxos/protocols/buf/dxos/echo/metadata_pb';\nimport { isBun } from '@dxos/util';\n\nimport { CommandConfig } from '../services/index.ts';\n\n/** Matches the budget `Space.syncToEdge` gives its own wait, so neither half of a drain dominates. */\nconst SPACE_READY_TIMEOUT = Duration.seconds(60);\n\nexport const getSpace = (spaceId: Key.SpaceId): Effect.Effect<Space, SpaceNotFoundError, ClientService> =>\n Effect.gen(function* () {\n const client = yield* ClientService;\n // v4 no longer yields an `Option` as an `Effect`, so the miss fails directly.\n const space = client.spaces.get(spaceId);\n return space ?? (yield* Effect.fail(new SpaceNotFoundError(spaceId)));\n });\n\nexport const spaceIdWithDefault = (spaceId: Option.Option<Key.SpaceId>) =>\n Effect.gen(function* () {\n const client = yield* ClientService;\n return Option.getOrElse(spaceId, () => {\n const defaultSpace = AppSpace.getDefaultSpace(client);\n if (!defaultSpace) {\n throw new Error('No space ID provided and no default space found.');\n }\n return defaultSpace.id;\n });\n });\n\n// TODO(wittjosiah): Factor out.\nexport const spaceLayer = (\n spaceId$: Option.Option<Key.SpaceId>,\n fallbackToPersonalSpace = false,\n): Layer.Layer<Database.Service, never, ClientService> => {\n const getSpace = Effect.fn(function* () {\n const client = yield* ClientService;\n\n // Resolution order when fallbackToPersonalSpace is true:\n // 1. the explicit spaceId arg (if provided);\n // 2. the space designated as default by the settings space;\n // 3. the first user-visible space.\n // This keeps profiles created outside composer-app (which is what designates\n // a default space on identity creation) usable — the alternative\n // is a \"Space not found\" throw deep inside CredentialsService.\n const resolveSpace = () => {\n if (!fallbackToPersonalSpace) {\n return spaceId$.pipe(Option.flatMap((id) => Option.fromNullishOr(client.spaces.get(id))));\n }\n return spaceId$.pipe(\n Option.flatMap((id) => Option.fromNullishOr(client.spaces.get(id))),\n Option.orElse(() => Option.fromNullishOr(AppSpace.getDefaultSpace(client))),\n // Not the raw first space: the settings space is created first and holds app config only.\n Option.orElse(() => Option.fromNullishOr(client.spaces.get().find(AppSpace.isVisibleSpace))),\n );\n };\n\n const space = resolveSpace().pipe(Option.getOrUndefined);\n\n if (space) {\n yield* Effect.promise(() => space.waitUntilReady());\n }\n return space;\n });\n\n // When no space can be resolved we install a stub whose `db` getter throws\n // on access — preserves the existing semantics for commands that *do* need\n // a db — but the release callback must NOT touch `db` or it will throw\n // during teardown (e.g. after a command emits a friendly error and\n // returns early). A shared sentinel object short-circuits the release.\n const NO_DB_STUB = {\n get db(): Database.Database {\n throw new Error('Space not found');\n },\n };\n const db = Layer.effect(\n Database.Service,\n Effect.acquireRelease(\n Effect.gen(function* () {\n const space = yield* getSpace();\n if (!space) {\n return NO_DB_STUB;\n }\n return { db: space.db };\n }),\n (holder) => (holder === NO_DB_STUB ? Effect.void : Effect.promise(() => holder.db.flush())),\n ),\n );\n\n return db;\n};\n\n/**\n * Block until `space` is fully replicated to EDGE, enabling replication first if the preference is\n * off. Reports whether it ran: p2p networking is unavailable outside bun, which is how a test run\n * is recognised.\n */\n// TODO(dmaretskyi): There a race condition with edge connection not showing up.\nconst syncSpaceToEdge = Effect.fn(function* (space: Space) {\n // TODO(wittjosiah): Find a better way to do this.\n if (!isBun()) {\n return false;\n }\n\n // TODO(wittjosiah): This should probably be prompted for.\n if (space.internal.data.edgeReplication !== EdgeReplicationSetting.ENABLED) {\n log.info('enabling edge replication', { spaceId: space.id });\n yield* Effect.promise(() => space.internal.setEdgeReplicationPreference(EdgeReplicationSetting.ENABLED));\n }\n\n yield* Effect.promise(() =>\n space.internal.syncToEdge({\n onProgress: (state) => log.info('syncing', { state: state ?? 'no connection to edge' }),\n }),\n );\n return true;\n});\n\n/**\n * Block until `space` is fully replicated to EDGE, reporting completion on stdout. A no-op outside\n * bun, which prints nothing.\n */\nexport const waitForSync = Effect.fn(function* (space: Space) {\n if (yield* syncSpaceToEdge(space)) {\n yield* Console.log('Sync complete');\n }\n});\n\nexport const flushAndSync = Effect.fn(function* (opts?: Database.FlushOptions) {\n yield* Database.flush(opts);\n const spaceId = yield* Database.spaceId;\n const space = yield* getSpace(spaceId);\n yield* waitForSync(space);\n});\n\n/**\n * Flush and sync every space of the current identity to EDGE.\n *\n * `dx` force-exits the moment a command returns, so replication that is merely queued is lost with\n * the process. A command that provisions spaces (`dx account signup`) must therefore drain all of\n * them, not just the one it wrote to — the settings space carries the default-space designation,\n * without which the next device resolves no default space at all.\n */\nexport const syncAllToEdge = Effect.fn(function* () {\n const { json } = yield* CommandConfig;\n const client = yield* ClientService;\n const spaces = client.spaces.get();\n // One line for the batch, never one per space: `waitForSync`'s per-space chatter would drown the\n // command's own output, and any of it on stdout makes `--json` unparseable.\n if (!json && spaces.length > 0) {\n yield* Console.log(`Syncing ${spaces.length} space(s) to EDGE...`);\n }\n\n // Concurrent, because each space's sync is a wait on its own replication round: serially, a\n // profile with N spaces pays N times the per-space timeout before the command can exit.\n yield* Effect.forEach(\n spaces,\n Effect.fn(function* (space: Space) {\n // Bounded, unlike the raw promise: a space whose initialization never lands would otherwise\n // hold the command open forever, and `syncToEdge` below only budgets its own wait.\n yield* Effect.promise(() => space.waitUntilReady()).pipe(Effect.timeout(SPACE_READY_TIMEOUT));\n yield* Effect.promise(() => space.db.flush());\n yield* syncSpaceToEdge(space);\n }),\n { concurrency: 'unbounded', discard: true },\n );\n});\n\n// TODO(burdon): Reconcile with @dxos/protocols\nexport class SpaceNotFoundError extends BaseError.extend('SpaceNotFoundError', 'Space not found') {\n constructor(spaceId: string, options?: Omit<BaseErrorOptions, 'context'>) {\n super({ context: { spaceId }, ...options });\n }\n}\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { anyUnpack } from '@bufbuild/protobuf/wkt';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\n\nimport { type Space, SpaceState, type SpaceSyncState } from '@dxos/client/echo';\nimport { toDate } from '@dxos/protocols/buf';\nimport { EpochSchema } from '@dxos/protocols/buf/dxos/halo/credentials_pb';\n\nimport { PlatformError } from './errors.ts';\nimport * as FormBuilder from './form-builder.ts';\n\nexport type FormatSpaceOptions = {\n verbose?: boolean;\n truncateKeys?: boolean;\n /**\n * If set, wait up to this many seconds for the space to reach\n * `SPACE_READY` before reading its fields. If unset, read whatever state\n * is available right now — much safer for `space list` etc., where a\n * single stuck space would otherwise hang the entire command.\n */\n waitSeconds?: number;\n};\n\nconst DEFAULT_OPTIONS: Required<FormatSpaceOptions> = {\n verbose: false,\n truncateKeys: false,\n waitSeconds: 0,\n};\n\n/**\n * Per-async-read internal timeout. Some `space.internal.*` getters do\n * filesystem / network IO and can themselves hang on a partially-loaded\n * space; cap each one so the command can never be held hostage by SDK\n * internals.\n */\nconst READ_TIMEOUT_SECONDS = 2;\n\nconst tryWithFallback = <T>(label: string, run: () => Promise<T>, fallback: T) =>\n Effect.tryPromise(run).pipe(\n Effect.timeoutOrElse({\n duration: Duration.seconds(READ_TIMEOUT_SECONDS),\n orElse: () => Effect.fail(new PlatformError({ message: `${label} timed out` })),\n }),\n Effect.catch(() => Effect.succeed(fallback)),\n );\n\nconst tryWithFallbackSync = <T>(read: () => T, fallback: T): T => {\n try {\n return read();\n } catch {\n return fallback;\n }\n};\n\n// TODO(wittjosiah): Use @effect/printer.\nexport const formatSpace = Effect.fn(function* (space: Space, options: FormatSpaceOptions = {}) {\n const { waitSeconds } = { ...DEFAULT_OPTIONS, ...options };\n\n // Opt-in wait. Defaults to NO wait so a single stuck space can't hang\n // an enumeration command (e.g. `dx space list`).\n if (waitSeconds > 0) {\n yield* Effect.tryPromise(() => space.waitUntilReady()).pipe(\n Effect.timeoutOrElse({\n duration: Duration.seconds(waitSeconds),\n orElse: () => Effect.fail(new PlatformError({ message: 'waitUntilReady timed out' })),\n }),\n Effect.catch(() => Effect.void),\n );\n }\n\n const state = tryWithFallbackSync(() => space.state.get(), SpaceState.SPACE_INITIALIZING);\n const ready = state === SpaceState.SPACE_READY;\n\n // TODO(burdon): Factor out.\n // TODO(burdon): Agent needs to restart before `ready` is available.\n const metrics = tryWithFallbackSync(() => space.internal.data.metrics, undefined);\n const openedAt = toDate(metrics?.open);\n const readyAt = toDate(metrics?.ready);\n const startup = openedAt && readyAt ? readyAt.getTime() - openedAt.getTime() : undefined;\n\n // TODO(burdon): Get feeds from client-services if verbose (factor out from devtools/diagnostics).\n // const host = client.services.services.DevtoolsHost!;\n const pipeline = tryWithFallbackSync(() => space.internal.data.pipeline, undefined);\n const epochAssertion = pipeline?.currentEpoch?.subject?.assertion;\n const epoch = epochAssertion && anyUnpack(epochAssertion, EpochSchema)?.number;\n\n // The sync-state read does IO; cap it so a stuck space can't hang the\n // command. Falls back to a \"no peers\" placeholder.\n const syncStateRaw = yield* tryWithFallback(\n 'getAutomergeSyncState',\n () => space.internal.db.getAutomergeSyncState(),\n {\n peers: {},\n } as SpaceSyncState,\n );\n const syncState = aggregateSyncState(syncStateRaw);\n\n const name = ready ? tryWithFallbackSync(() => space.properties.name, undefined) : 'loading...';\n const members = tryWithFallbackSync(() => space.members.get().length, 0);\n const objects = tryWithFallbackSync(() => space.internal.db.getAllObjectIds().length, 0);\n const key = options.truncateKeys\n ? tryWithFallbackSync(() => space.key.truncate(), '')\n : tryWithFallbackSync(() => space.key.toHex(), '');\n\n return {\n id: space.id,\n state: SpaceState[state],\n name,\n\n members,\n objects,\n\n key,\n epoch,\n startup,\n automergeRoot: pipeline?.directoryUrl,\n // appliedEpoch,\n syncState: `${syncState.count} ${getSyncIndicator(syncState.up, syncState.down)} (${syncState.peers} peers)`,\n };\n});\n\nconst aggregateSyncState = (syncState: SpaceSyncState) => {\n const peers = Object.keys(syncState.peers ?? {}).length;\n const missingOnLocal = Math.max(...Object.values(syncState.peers ?? {}).map((peer) => peer.missingOnLocal), 0);\n const missingOnRemote = Math.max(...Object.values(syncState.peers ?? {}).map((peer) => peer.missingOnRemote), 0);\n const differentDocuments = Math.max(\n ...Object.values(syncState.peers ?? {}).map((peer) => peer.differentDocuments),\n 0,\n );\n\n return {\n count: missingOnLocal + missingOnRemote + differentDocuments,\n peers,\n up: missingOnRemote > 0 || differentDocuments > 0,\n down: missingOnLocal > 0 || differentDocuments > 0,\n };\n};\n\nconst getSyncIndicator = (up: boolean, down: boolean) => {\n if (up) {\n if (down) {\n return '⇅';\n } else {\n return '↑';\n }\n } else {\n if (down) {\n return '↓';\n } else {\n return '✓';\n }\n }\n};\n\nexport type FormattedSpace = {\n id: string;\n state: string;\n name: string | undefined;\n members: number;\n objects: number;\n key: string;\n epoch: any;\n startup: number | undefined;\n automergeRoot: string | undefined;\n syncState: string;\n};\n\n/**\n * Pretty prints a space with ANSI colors.\n */\nexport const printSpace = (spaceData: FormattedSpace) =>\n FormBuilder.make({ title: spaceData.name ?? spaceData.id }).pipe(\n FormBuilder.set('id', spaceData.id),\n FormBuilder.set('state', spaceData.state),\n FormBuilder.set('key', spaceData.key),\n FormBuilder.set('members', spaceData.members),\n FormBuilder.set('objects', spaceData.objects),\n FormBuilder.set('epoch', spaceData.epoch ?? '<none>'),\n FormBuilder.set('startup', spaceData.startup ? `${spaceData.startup}ms` : '<none>'),\n FormBuilder.set('syncState', spaceData.syncState),\n FormBuilder.set('automergeRoot', spaceData.automergeRoot ?? '<none>'),\n FormBuilder.build,\n );\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport type * as Cause from 'effect/Cause';\nimport * as Config from 'effect/Config';\nimport type * as ConfigError from 'effect/Config';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nexport const withTimeout: <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n) => Effect.Effect<A, Cause.TimeoutError | ConfigError.ConfigError | E, R> = Effect.fnUntraced(function* (effect) {\n const timeout = yield* Config.Int('TIMEOUT').pipe(Config.option);\n const duration = timeout.pipe(\n Option.map(Duration.millis),\n Option.getOrElse(() => Duration.infinity),\n );\n return yield* effect.pipe(Effect.timeout(duration));\n});\n"],"mappings":"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"}
|
package/dist/lib/oauth.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.mjs","names":[],"sources":["../../src/oauth/recovery.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { getEdgeUrlWithProtocol } from '@dxos/edge-client';\nimport { BaseError } from '@dxos/errors';\nimport { EntityId, SpaceId } from '@dxos/keys';\n\nimport { CALLBACK_TIMEOUT_MS, startLocalCallbackServer } from '../callback/server';\n\n/** A gate OAuth flow (recovery or registration) failed to initiate or complete. */\nexport class OAuthFlowError extends BaseError.extend('OAuthFlowError', 'OAuth flow failed.') {}\n\n/** Path Edge redirects the browser to after a recovery/register OAuth round-trip. */\nconst RECOVERY_CALLBACK_PATH = '/redirect/oauth-recovery';\n\nexport type RecoveryOAuthParams = {\n /** Edge services base URL (from client config `runtime.services.edge.url`); `ws(s)` or `http(s)`. */\n readonly edgeBaseUrl: string;\n /** OAuth provider id (e.g. `'atproto'`). */\n readonly provider: string;\n /** Requested scopes. */\n readonly scopes: readonly string[];\n /** atproto handle or DID — required so Edge can resolve the user's PDS / auth server. */\n readonly loginHint: string;\n};\n\ntype InitiateEnvelope = { success: boolean; data?: { authUrl?: string }; error?: { message?: string } };\n\n/**\n * Drives one of the gate's OAuth account-recovery flows from a CLI: starts a local callback server,\n * asks Edge to begin a flow of the given purpose (advertising the local server as the redirect\n * origin via the `Origin` header), opens the browser, and resolves with the query params Edge\n * carries back in its redirect.\n *\n * Both purposes run before any identity exists, so no auth header is sent — Edge resolves the\n * user's space / token server-side, and the `spaceId` / `accessTokenId` in the request are unused\n * (random values satisfy request validation).\n */\nconst performOAuthRoundTrip = Effect.fn(function* (params: RecoveryOAuthParams, purpose: 'recovery' | 'register') {\n const server = yield* startLocalCallbackServer(RECOVERY_CALLBACK_PATH).pipe(\n Effect.mapError(OAuthFlowError.wrap({ ifTypeDiffers: true })),\n );\n return yield* Effect.gen(function* () {\n // Config carries the edge URL in the client's socket form (`wss://`), which `fetch` rejects\n // outright -- the HTTP clients normalize it the same way.\n const initiateUrl = new URL('/oauth/initiate', getEdgeUrlWithProtocol(params.edgeBaseUrl, 'http')).toString();\n const response = yield* Effect.tryPromise({\n try: () =>\n fetch(initiateUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Origin': server.origin },\n body: JSON.stringify({\n provider: params.provider,\n scopes: params.scopes,\n spaceId: SpaceId.random(),\n accessTokenId: EntityId.random(),\n purpose,\n loginHint: params.loginHint,\n // Tells kms-service to write the recovery binding once the provider authenticates.\n ...(purpose === 'register' ? { registerRecovery: true } : {}),\n }),\n }),\n catch: OAuthFlowError.wrap({ message: 'OAuth initiate request failed.' }),\n });\n\n const envelope = yield* Effect.tryPromise({\n try: () => response.json() as Promise<InitiateEnvelope>,\n catch: OAuthFlowError.wrap({ message: 'OAuth initiate response parse failed.' }),\n });\n if (!envelope.success || !envelope.data?.authUrl) {\n return yield* Effect.fail(\n new OAuthFlowError({ message: `OAuth initiation failed: ${envelope.error?.message ?? 'unknown error'}` }),\n );\n }\n\n yield* server.open(envelope.data.authUrl).pipe(Effect.mapError(OAuthFlowError.wrap({ ifTypeDiffers: true })));\n return yield* server\n .waitForResult(CALLBACK_TIMEOUT_MS)\n .pipe(Effect.mapError(OAuthFlowError.wrap({ ifTypeDiffers: true })));\n }).pipe(Effect.ensuring(server.stop()));\n});\n\n/**\n * Runs the gate's OAuth identity-recovery flow and resolves with the one-time `recoveryProof` Edge\n * carries back in its redirect. The caller redeems the proof via\n * `client.halo.recoverIdentity({ recoveryProof })`.\n */\nexport const performRecoveryOAuthFlow = Effect.fn(function* (params: RecoveryOAuthParams) {\n const callbackParams = yield* performOAuthRoundTrip(params, 'recovery');\n const recoveryProof = callbackParams.recoveryProof;\n if (!recoveryProof) {\n return yield* Effect.fail(\n new OAuthFlowError({ message: 'OAuth recovery completed but no recoveryProof was returned.' }),\n );\n }\n return { recoveryProof };\n});\n\n/**\n * Runs the gate's OAuth account-registration flow and resolves with the opaque `registrationToken`\n * Edge carries back in its redirect. The caller exchanges it via\n * `EdgeHttpClient.completeOAuthRegistration`, which routes the OAuth refresh token into the default\n * space, records the recovery binding, and returns the provider-verified email.\n */\nexport const performRegisterOAuthFlow = Effect.fn(function* (params: RecoveryOAuthParams) {\n const callbackParams = yield* performOAuthRoundTrip(params, 'register');\n const registrationToken = callbackParams.registrationToken;\n if (!registrationToken) {\n return yield* Effect.fail(\n new OAuthFlowError({ message: 'OAuth registration completed but no registrationToken was returned.' }),\n );\n }\n return { registrationToken };\n});\n"],"mappings":";;;;;;;AAaA,IAAa,iBAAb,cAAoC,UAAU,OAAO,kBAAkB,oBAAoB,CAAC,CAAC,CAAC;;AAG9F,IAAM,yBAAyB;;;;;;;;;;;AAyB/B,IAAM,wBAAwB,OAAO,GAAG,WAAW,QAA6B,SAAkC;CAChH,MAAM,SAAS,OAAO,yBAAyB,sBAAsB,CAAC,CAAC,KACrE,OAAO,SAAS,eAAe,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAC9D;CACA,OAAO,OAAO,OAAO,IAAI,aAAa;EAGpC,MAAM,cAAc,IAAI,IAAI,mBAAmB,uBAAuB,OAAO,aAAa,MAAM,CAAC,CAAC,CAAC,SAAS;EAC5G,MAAM,WAAW,OAAO,OAAO,WAAW;GACxC,WACE,MAAM,aAAa;IACjB,QAAQ;IACR,SAAS;KAAE,gBAAgB;KAAoB,UAAU,OAAO;IAAO;IACvE,MAAM,KAAK,UAAU;KACnB,UAAU,OAAO;KACjB,QAAQ,OAAO;KACf,SAAS,QAAQ,OAAO;KACxB,eAAe,SAAS,OAAO;KAC/B;KACA,WAAW,OAAO;KAElB,GAAI,YAAY,aAAa,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC7D,CAAC;GACH,CAAC;GACH,OAAO,eAAe,KAAK,EAAE,SAAS,iCAAiC,CAAC;EAC1E,CAAC;EAED,MAAM,WAAW,OAAO,OAAO,WAAW;GACxC,WAAW,SAAS,KAAK;GACzB,OAAO,eAAe,KAAK,EAAE,SAAS,wCAAwC,CAAC;EACjF,CAAC;EACD,IAAI,CAAC,SAAS,WAAW,CAAC,SAAS,MAAM,SACvC,OAAO,OAAO,OAAO,KACnB,IAAI,eAAe,EAAE,SAAS,4BAA4B,SAAS,OAAO,WAAW,kBAAkB,CAAC,CAC1G;EAGF,OAAO,OAAO,KAAK,SAAS,KAAK,OAAO,CAAC,CAAC,KAAK,OAAO,SAAS,eAAe,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC;EAC5G,OAAO,OAAO,OACX,cAAc,mBAAmB,CAAA,CACjC,KAAK,OAAO,SAAS,eAAe,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC;CACvE,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC;AACxC,CAAC;;;;;;AAOD,IAAa,2BAA2B,OAAO,GAAG,WAAW,QAA6B;CAExF,MAAM,iBAAgB,OADQ,sBAAsB,QAAQ,UAAU,EAAA,CACjC;CACrC,IAAI,CAAC,eACH,OAAO,OAAO,OAAO,KACnB,IAAI,eAAe,EAAE,SAAS,8DAA8D,CAAC,CAC/F;CAEF,OAAO,EAAE,cAAc;AACzB,CAAC;;;;;;;AAQD,IAAa,2BAA2B,OAAO,GAAG,WAAW,QAA6B;CAExF,MAAM,qBAAoB,OADI,sBAAsB,QAAQ,UAAU,EAAA,CAC7B;CACzC,IAAI,CAAC,mBACH,OAAO,OAAO,OAAO,KACnB,IAAI,eAAe,EAAE,SAAS,sEAAsE,CAAC,CACvG;CAEF,OAAO,EAAE,kBAAkB;AAC7B,CAAC"}
|
|
1
|
+
{"version":3,"file":"oauth.mjs","names":[],"sources":["../../src/oauth/recovery.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { getEdgeUrlWithProtocol } from '@dxos/edge-client';\nimport { BaseError } from '@dxos/errors';\nimport { EntityId, SpaceId } from '@dxos/keys';\n\nimport { CALLBACK_TIMEOUT_MS, startLocalCallbackServer } from '../callback/server.ts';\n\n/** A gate OAuth flow (recovery or registration) failed to initiate or complete. */\nexport class OAuthFlowError extends BaseError.extend('OAuthFlowError', 'OAuth flow failed.') {}\n\n/** Path Edge redirects the browser to after a recovery/register OAuth round-trip. */\nconst RECOVERY_CALLBACK_PATH = '/redirect/oauth-recovery';\n\nexport type RecoveryOAuthParams = {\n /** Edge services base URL (from client config `runtime.services.edge.url`); `ws(s)` or `http(s)`. */\n readonly edgeBaseUrl: string;\n /** OAuth provider id (e.g. `'atproto'`). */\n readonly provider: string;\n /** Requested scopes. */\n readonly scopes: readonly string[];\n /** atproto handle or DID — required so Edge can resolve the user's PDS / auth server. */\n readonly loginHint: string;\n};\n\ntype InitiateEnvelope = { success: boolean; data?: { authUrl?: string }; error?: { message?: string } };\n\n/**\n * Drives one of the gate's OAuth account-recovery flows from a CLI: starts a local callback server,\n * asks Edge to begin a flow of the given purpose (advertising the local server as the redirect\n * origin via the `Origin` header), opens the browser, and resolves with the query params Edge\n * carries back in its redirect.\n *\n * Both purposes run before any identity exists, so no auth header is sent — Edge resolves the\n * user's space / token server-side, and the `spaceId` / `accessTokenId` in the request are unused\n * (random values satisfy request validation).\n */\nconst performOAuthRoundTrip = Effect.fn(function* (params: RecoveryOAuthParams, purpose: 'recovery' | 'register') {\n const server = yield* startLocalCallbackServer(RECOVERY_CALLBACK_PATH).pipe(\n Effect.mapError(OAuthFlowError.wrap({ ifTypeDiffers: true })),\n );\n return yield* Effect.gen(function* () {\n // Config carries the edge URL in the client's socket form (`wss://`), which `fetch` rejects\n // outright -- the HTTP clients normalize it the same way.\n const initiateUrl = new URL('/oauth/initiate', getEdgeUrlWithProtocol(params.edgeBaseUrl, 'http')).toString();\n const response = yield* Effect.tryPromise({\n try: () =>\n fetch(initiateUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Origin': server.origin },\n body: JSON.stringify({\n provider: params.provider,\n scopes: params.scopes,\n spaceId: SpaceId.random(),\n accessTokenId: EntityId.random(),\n purpose,\n loginHint: params.loginHint,\n // Tells kms-service to write the recovery binding once the provider authenticates.\n ...(purpose === 'register' ? { registerRecovery: true } : {}),\n }),\n }),\n catch: OAuthFlowError.wrap({ message: 'OAuth initiate request failed.' }),\n });\n\n const envelope = yield* Effect.tryPromise({\n try: () => response.json() as Promise<InitiateEnvelope>,\n catch: OAuthFlowError.wrap({ message: 'OAuth initiate response parse failed.' }),\n });\n if (!envelope.success || !envelope.data?.authUrl) {\n return yield* Effect.fail(\n new OAuthFlowError({ message: `OAuth initiation failed: ${envelope.error?.message ?? 'unknown error'}` }),\n );\n }\n\n yield* server.open(envelope.data.authUrl).pipe(Effect.mapError(OAuthFlowError.wrap({ ifTypeDiffers: true })));\n return yield* server\n .waitForResult(CALLBACK_TIMEOUT_MS)\n .pipe(Effect.mapError(OAuthFlowError.wrap({ ifTypeDiffers: true })));\n }).pipe(Effect.ensuring(server.stop()));\n});\n\n/**\n * Runs the gate's OAuth identity-recovery flow and resolves with the one-time `recoveryProof` Edge\n * carries back in its redirect. The caller redeems the proof via\n * `client.halo.recoverIdentity({ recoveryProof })`.\n */\nexport const performRecoveryOAuthFlow = Effect.fn(function* (params: RecoveryOAuthParams) {\n const callbackParams = yield* performOAuthRoundTrip(params, 'recovery');\n const recoveryProof = callbackParams.recoveryProof;\n if (!recoveryProof) {\n return yield* Effect.fail(\n new OAuthFlowError({ message: 'OAuth recovery completed but no recoveryProof was returned.' }),\n );\n }\n return { recoveryProof };\n});\n\n/**\n * Runs the gate's OAuth account-registration flow and resolves with the opaque `registrationToken`\n * Edge carries back in its redirect. The caller exchanges it via\n * `EdgeHttpClient.completeOAuthRegistration`, which routes the OAuth refresh token into the default\n * space, records the recovery binding, and returns the provider-verified email.\n */\nexport const performRegisterOAuthFlow = Effect.fn(function* (params: RecoveryOAuthParams) {\n const callbackParams = yield* performOAuthRoundTrip(params, 'register');\n const registrationToken = callbackParams.registrationToken;\n if (!registrationToken) {\n return yield* Effect.fail(\n new OAuthFlowError({ message: 'OAuth registration completed but no registrationToken was returned.' }),\n );\n }\n return { registrationToken };\n});\n"],"mappings":";;;;;;;AAaA,IAAa,iBAAb,cAAoC,UAAU,OAAO,kBAAkB,oBAAoB,CAAC,CAAC,CAAC;;AAG9F,IAAM,yBAAyB;;;;;;;;;;;AAyB/B,IAAM,wBAAwB,OAAO,GAAG,WAAW,QAA6B,SAAkC;CAChH,MAAM,SAAS,OAAO,yBAAyB,sBAAsB,CAAC,CAAC,KACrE,OAAO,SAAS,eAAe,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAC9D;CACA,OAAO,OAAO,OAAO,IAAI,aAAa;EAGpC,MAAM,cAAc,IAAI,IAAI,mBAAmB,uBAAuB,OAAO,aAAa,MAAM,CAAC,CAAC,CAAC,SAAS;EAC5G,MAAM,WAAW,OAAO,OAAO,WAAW;GACxC,WACE,MAAM,aAAa;IACjB,QAAQ;IACR,SAAS;KAAE,gBAAgB;KAAoB,UAAU,OAAO;IAAO;IACvE,MAAM,KAAK,UAAU;KACnB,UAAU,OAAO;KACjB,QAAQ,OAAO;KACf,SAAS,QAAQ,OAAO;KACxB,eAAe,SAAS,OAAO;KAC/B;KACA,WAAW,OAAO;KAElB,GAAI,YAAY,aAAa,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC7D,CAAC;GACH,CAAC;GACH,OAAO,eAAe,KAAK,EAAE,SAAS,iCAAiC,CAAC;EAC1E,CAAC;EAED,MAAM,WAAW,OAAO,OAAO,WAAW;GACxC,WAAW,SAAS,KAAK;GACzB,OAAO,eAAe,KAAK,EAAE,SAAS,wCAAwC,CAAC;EACjF,CAAC;EACD,IAAI,CAAC,SAAS,WAAW,CAAC,SAAS,MAAM,SACvC,OAAO,OAAO,OAAO,KACnB,IAAI,eAAe,EAAE,SAAS,4BAA4B,SAAS,OAAO,WAAW,kBAAkB,CAAC,CAC1G;EAGF,OAAO,OAAO,KAAK,SAAS,KAAK,OAAO,CAAC,CAAC,KAAK,OAAO,SAAS,eAAe,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC;EAC5G,OAAO,OAAO,OACX,cAAc,mBAAmB,CAAA,CACjC,KAAK,OAAO,SAAS,eAAe,KAAK,EAAE,eAAe,KAAK,CAAC,CAAC,CAAC;CACvE,CAAC,CAAC,CAAC,KAAK,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC;AACxC,CAAC;;;;;;AAOD,IAAa,2BAA2B,OAAO,GAAG,WAAW,QAA6B;CAExF,MAAM,iBAAgB,OADQ,sBAAsB,QAAQ,UAAU,EAAA,CACjC;CACrC,IAAI,CAAC,eACH,OAAO,OAAO,OAAO,KACnB,IAAI,eAAe,EAAE,SAAS,8DAA8D,CAAC,CAC/F;CAEF,OAAO,EAAE,cAAc;AACzB,CAAC;;;;;;;AAQD,IAAa,2BAA2B,OAAO,GAAG,WAAW,QAA6B;CAExF,MAAM,qBAAoB,OADI,sBAAsB,QAAQ,UAAU,EAAA,CAC7B;CACzC,IAAI,CAAC,mBACH,OAAO,OAAO,OAAO,KACnB,IAAI,eAAe,EAAE,SAAS,sEAAsE,CAAC,CACvG;CAEF,OAAO,EAAE,kBAAkB;AAC7B,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as PlatformError } from "./chunk-errors.mjs";
|
|
1
2
|
import * as Effect from "effect/Effect";
|
|
2
3
|
//#region src/util/platform.browser.ts
|
|
3
4
|
/**
|
|
@@ -8,7 +9,10 @@ import * as Effect from "effect/Effect";
|
|
|
8
9
|
*/
|
|
9
10
|
var copyToClipboard = (text) => Effect.tryPromise({
|
|
10
11
|
try: () => navigator.clipboard.writeText(text),
|
|
11
|
-
catch: (error) =>
|
|
12
|
+
catch: (error) => new PlatformError({
|
|
13
|
+
message: "Failed to copy to clipboard.",
|
|
14
|
+
cause: error
|
|
15
|
+
})
|
|
12
16
|
});
|
|
13
17
|
/**
|
|
14
18
|
* Open a URL in a new tab.
|
|
@@ -17,7 +21,10 @@ var openBrowser = (url) => Effect.try({
|
|
|
17
21
|
try: () => {
|
|
18
22
|
if (!window.open(url, "_blank", "noopener,noreferrer")) throw new Error("Popup blocked.");
|
|
19
23
|
},
|
|
20
|
-
catch: (error) =>
|
|
24
|
+
catch: (error) => new PlatformError({
|
|
25
|
+
message: "Failed to open browser.",
|
|
26
|
+
cause: error
|
|
27
|
+
})
|
|
21
28
|
});
|
|
22
29
|
//#endregion
|
|
23
30
|
export { copyToClipboard, openBrowser };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.browser.mjs","names":[],"sources":["../../src/util/platform.browser.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\n/**\n * Copy text to the system clipboard.\n *\n * Requires a secure context; browsers additionally reject the call unless it happens while handling\n * a user gesture, which a command dispatched from a terminal prompt does satisfy.\n */\nexport const copyToClipboard = (text: string): Effect.Effect<void,
|
|
1
|
+
{"version":3,"file":"platform.browser.mjs","names":[],"sources":["../../src/util/platform.browser.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { PlatformError } from './errors.ts';\n\n/**\n * Copy text to the system clipboard.\n *\n * Requires a secure context; browsers additionally reject the call unless it happens while handling\n * a user gesture, which a command dispatched from a terminal prompt does satisfy.\n */\nexport const copyToClipboard = (text: string): Effect.Effect<void, PlatformError> =>\n Effect.tryPromise({\n try: () => navigator.clipboard.writeText(text),\n catch: (error) => new PlatformError({ message: 'Failed to copy to clipboard.', cause: error }),\n });\n\n/**\n * Open a URL in a new tab.\n */\nexport const openBrowser = (url: string): Effect.Effect<void, PlatformError> =>\n Effect.try({\n try: () => {\n const opened = window.open(url, '_blank', 'noopener,noreferrer');\n if (!opened) {\n throw new Error('Popup blocked.');\n }\n },\n catch: (error) => new PlatformError({ message: 'Failed to open browser.', cause: error }),\n });\n"],"mappings":";;;;;;;;;AAcA,IAAa,mBAAmB,SAC9B,OAAO,WAAW;CAChB,WAAW,UAAU,UAAU,UAAU,IAAI;CAC7C,QAAQ,UAAU,IAAI,cAAc;EAAE,SAAS;EAAgC,OAAO;CAAM,CAAC;AAC/F,CAAC;;;;AAKH,IAAa,eAAe,QAC1B,OAAO,IAAI;CACT,WAAW;EAET,IAAI,CADW,OAAO,KAAK,KAAK,UAAU,qBACrC,GACH,MAAM,IAAI,MAAM,gBAAgB;CAEpC;CACA,QAAQ,UAAU,IAAI,cAAc;EAAE,SAAS;EAA2B,OAAO;CAAM,CAAC;AAC1F,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as PlatformError } from "./chunk-errors.mjs";
|
|
1
2
|
import * as Effect from "effect/Effect";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
//#region src/util/platform.node.ts
|
|
@@ -45,7 +46,10 @@ var copyToClipboard = (text) => Effect.tryPromise({
|
|
|
45
46
|
proc.on("error", fallback);
|
|
46
47
|
});
|
|
47
48
|
},
|
|
48
|
-
catch: (error) =>
|
|
49
|
+
catch: (error) => new PlatformError({
|
|
50
|
+
message: "Failed to copy to clipboard.",
|
|
51
|
+
cause: error
|
|
52
|
+
})
|
|
49
53
|
});
|
|
50
54
|
/**
|
|
51
55
|
* Open a URL in the system's default browser.
|
|
@@ -80,7 +84,10 @@ var openBrowser = (url) => Effect.tryPromise({
|
|
|
80
84
|
proc.on("error", reject);
|
|
81
85
|
});
|
|
82
86
|
},
|
|
83
|
-
catch: (error) =>
|
|
87
|
+
catch: (error) => new PlatformError({
|
|
88
|
+
message: "Failed to open browser.",
|
|
89
|
+
cause: error
|
|
90
|
+
})
|
|
84
91
|
});
|
|
85
92
|
//#endregion
|
|
86
93
|
export { copyToClipboard, openBrowser };
|