Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dev-error-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"everything-dev": patch
---

Build errors are visible again in `bos dev`. Rsbuild prints `error Build error:` on stderr and the actual detail (`File: …`, `× …`) on stdout; the log pipeline now joins those detail lines onto the header as one error event and treats bundler `×` lines as errors on any stream, so the live tail and shutdown summary show the cause instead of a bare header. CLI commands also stop reporting "Unknown error" for Effect tagged errors (e.g. `ArtifactGenError` from `bos typecheck` / `bos types gen`): the tag, phase and cause chain are now shown.
5 changes: 5 additions & 0 deletions .changeset/dev-hmr-data-uri.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"every-plugin": patch
---

`FixMfDataUriPlugin` now only rewrites `@module-federation/*` imports inside data-URI modules. It used to strip the absolute `node_modules` prefix from every data-URI import, which turned Rsbuild 2's dev HMR client entry (`@rsbuild/core/dist/client/hmr.js`) into a bare specifier its exports map rejects, so `every-plugin dev` failed for the UI and plugin UIs with "Package subpath './dist/client/hmr.js' is not defined by exports".
6 changes: 6 additions & 0 deletions .changeset/soft-frames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ui": minor
"@everything-dev/auth-plugin": minor
---

Soft frames visual system: the UI moves from shadcn `base-maia` to square `base-lyra`, with 1px bevel frames — raised buttons that press in, sunken inputs, framed cards, dialogs and menus — in light and dark. Adds `Window`/`WindowTitleBar`/`WindowBody`, an `xl` button size and a `brand` button variant. The landing page is now a centered title, the under-construction GIF and one sign-in button, without the app shell.
78 changes: 64 additions & 14 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
# everything.dev

The open runtime for apps on NEAR — composed from published config and loaded through a shared host, UI, and API runtime.
The open runtime for apps on NEAR — every app is a node, owned by a NEAR account, composed from published config and run by a shared runtime.

## Nodes

**Node**:
An app published at `bos://<account>/<gateway>` — its config is signed by `<account>` and authored with `App(...)`. Every app is a node; "node" names its place in the graph, "app" names what it is.
_Avoid_: runtime (that's the package that runs nodes), tenant, project

**Root node**:
`bos://dev.everything.near/everything.dev` — the node every other node ultimately extends.
_Avoid_: base runtime, platform, parent app

**Child node**:
A node addressed under another node's path, `bos://<account>/<gateway>/plugins/<id>` — usually a plugin published from its parent's repo, runnable on its own and composable into any node.
_Avoid_: embedded plugin (a plugin that has not yet published as a node), sub-app

**Gateway**:
The second segment of a node address; for a top-level node, usually its domain.
_Avoid_: name, slug

**Owner**:
The NEAR account that signs a node's config — a single-key account or a multisig (Sputnik DAOs are multisigs). Ownership is whatever is true on-chain.
_Avoid_: organization (an organization links to an owner; it never is one), admin

**Address**:
Where a node is reached: a subdomain of its parent's domain, or its own domain.
_Avoid_: URL, host

**Hosting**:
What serves a node: the shared host, a sandbox, or a standalone deployment. Independent of Address.
_Avoid_: tier, instance

**Extends**:
A node inheriting its parent node's config, child-wins merged.
_Avoid_: fork, inherit

**Composition**:
A node pulling another node's surfaces in as one of its `plugins`; the composed node plays the **plugin** role. References follow the composed node's latest publish unless pinned.
_Avoid_: install, import, dependency

**Surface**:
One loadable part of a node — host, UI, API, or auth.
_Avoid_: remote (the Module Federation transport word), workspace (the repo directory), slot

**Contract**:
The API shape a node provides or requires, identified by its published contract types; what makes swapping one node's UI over another node's API checkable.
_Avoid_: interface, schema

**Node card**:
A node's public self-description — title, extends, surfaces, contracts, skill, MCP endpoint, and source — published with its config.
_Avoid_: manifest (that's the build artifact), metadata

## Language

**Runtime**:
A published app configuration (`bos.app.ts` authored, resolved through `extends`) that the host loads as remotes.
_Avoid_: app (ambiguous between the runtime and a tenant), project
The `everything-dev` package and host process that loads a node's surfaces and runs it. `RuntimeConfig` in code is a historical name for a node's resolved config.
_Avoid_: app or node (those are what the runtime runs), base runtime

**SlotResolver**:
The host's entry-URL seam (`entryUrls` in `everything-dev/ui/slot`) — one pure derivation from a stamped config slot (`EntrySlot`) to the URLs each surface loads: `web` (browser entry — bustered fixed name in development, pin-derived hashed outside it), `ssr` (server container entry; undefined when the slot has no SSR coordinates), and `browserManifest` (the pin-derived hashed mf-manifest, structurally absent for local slots — atomic-deploys 08). Surfaces resolve lazily and throw loudly outside development naming slot + surface; buster arithmetic (integrity for web, ssrIntegrity→containerVersion for the server) lives inside the module, never at call sites.
Expand All @@ -26,24 +76,24 @@ _Avoid_: sync file, template file (sync no longer carries ui source)

## Organization access

**Organization**:
A group of people who act together; it links to one or more Owners so its members can work on those owners' nodes.
_Avoid_: owner, org account

**Linked Account**:
An Owner an organization has proven control of — a single-key account the member has signed in with, or a multisig the member belongs to on-chain. An account links to at most one organization.
_Avoid_: DAO link, connected wallet

**Team**:
A named sub-group within an organization that shares access to the organization's feature areas.
A named sub-group within an organization.
_Avoid_: team account, team wallet

**Active Team**:
The Team currently selected for a user's organization work.
The Team a user is currently acting as.
_Avoid_: current team, team account

**Feature Area**:
A product capability that an organization can grant to a Team.
_Avoid_: permission, role

**Team Workspace**:
The organization view scoped to an Active Team and its granted feature areas.
_Avoid_: organization account

**Gateway Origin**:
The canonical base runtime origin for a network, where every passkey ceremony takes place regardless of which domain the member arrived from.
The root node's origin for a network, where every passkey ceremony takes place regardless of which domain the member arrived from.
_Avoid_: base URL, main domain

**Device Link**:
Expand Down
28 changes: 28 additions & 0 deletions docs/adr/0027-every-app-is-a-node.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ADR 0027: Every app is a node

Date: 2026-10-06
Status: Accepted

Apps already extend each other (`bos://<account>/<gateway>` → `extends`), but plugins have no identity of their own: `bos plugin publish` rewrites the host app's whole config, and the only way to point at a plugin is a fragment into someone else's config (`bos://acct/gw#plugins.registry`). Most plugins are apps — an API and a UI exposed for composition — so we treat every app as a **node** in one graph, and give plugins a real address nested under the node that publishes them: `bos://<account>/<gateway>/plugins/<id>`, stored at `apps/<account>/<gateway>/plugins/<id>/bos.config.json`. The authoring surface does not change: a node is still `App(...)`, `gateway` keeps its name, and `RuntimeConfig` stays in code as a historical name; "Runtime" now means the `everything-dev` package and host that run nodes.

## Decisions

1. **Address.** A node is `bos://<account>/<gateway>[/plugins/<id>]…`; the path nests, so a child node can compose its own children. `parseBosUrl` and `getRegistryConfigKey` (`packages/everything-dev/src/fastkv.ts`) already accept the trailing segments. Nested addresses mirror the bundle layout (`bundles/<account>/<gateway>/<workspace>/…`, ADR 0015/0020), avoid global plugin names, and let one prefix scan of `apps/<account>/<gateway>/` return a node and everything under it.
2. **Ownership is on-chain.** The signing account owns the node (ADR 0001, unchanged) — a single-key account or a multisig; Sputnik DAOs are multisigs and publish through a proposal whose `FunctionCall` executes as the DAO. An organization never owns a node: it **links** to owners through a verified `linkAccount { accountId, kind: "account" | "multisig" }` (the member's signed-in NEAR account, or on-chain membership of the multisig), replacing the unverified `linkDao`. An account links to at most one organization.
3. **Follow by default.** A composed or extended reference follows the target's latest publish — instant updates are the point. `@<version>` pins to the content-addressed version manifest that already exists (`every-plugin/src/version-manifest.ts`). Contract checks gate a follow before the new version is adopted.
4. **Contracts make swaps checkable.** A node declares the contracts it provides (its published contract types, by sha256 and semver) and requires. A UI node requiring `auth@^1` runs over any node providing it — "swap the UI, keep the functionality" becomes a check, not a hope.
5. **Node card.** Each published config carries a node card — title, extends, surfaces, provides/requires, skill URL, MCP endpoint, source (repository, git sha) — also served at `/.well-known/bos.json`, so agents and the registry discover nodes the same way (aligned with NEP-330 source metadata, agent skills, and `.well-known/mcp.json`).
6. **Graduation.** A child node that moves to its own domain publishes at the new top-level address, and its old nested address `extends` the new one, so existing composers keep following.

## Considered options

- **Flat `bos://<account>/<name>` for plugins** — host-independent identity, but needs globally unique names per account and redefines `gateway` as a generic name. Rejected; graduation (6) covers the independence case.
- **`modules/{ns}/{name}` + ModuleRecord** (beta-v2 map decision #3) — a second key space and record type beside app configs. Rejected in favour of one config shape at every level of the tree; the record's useful fields land in the node card.
- **Organization-owned nodes** — would make an off-chain auth record the authority over an on-chain write. Rejected; ownership follows the signer.

## Consequences

- The registry indexes nested configs and shows "used by" for every node; a plugin that has not published under its own nested address is shown as embedded until it does.
- `bos plugin publish <id>` must write `apps/<account>/<gateway>/plugins/<id>/bos.config.json` and the parent references it with `Plugin(id).extends("bos://…/plugins/<id>")`; the CLI and publish track is shared with the citynode "Everything is a node" wayfinder (citynode.app#233–#252).
- Each plugin directory moves from `plugin.dev.ts` to the `bos.app.ts` + `bos.dev.ts` pair, so any node can boot standalone on the universal image (ADR 0021).
- The dev panel can only write a node when the connected wallet is its owner; multisig owners get a proposal flow later.
3 changes: 2 additions & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Format: context → decision → consequences, one file per decision.
| [0024](./0024-composition-stays-manifest-based.md) | Composition stays manifest-based; route chunks load lazily | Accepted |
| [0025](./0025-auth-identity-value-classes.md) | Auth identity values derive from the runtime config | Accepted |
| [0026](./0026-pnpm-workspace-node-runtime-v2.md) | pnpm workspace, node runtime — one toolchain for parent and children (v2) | Accepted |
| [0027](./0027-every-app-is-a-node.md) | Every app is a node — nested plugin addresses, on-chain ownership, follow by default | Accepted |


Skipped numbers: 0006 exists; there are no gaps otherwise. 0015–0018 were
Expand All @@ -42,7 +43,7 @@ low-reference file in each pair moved (see git history).
## Number assignment discipline

- Take the next free number (`ls docs/adr/ | sort -n | tail -1`, then +1 —
next free is **0026**). Never reuse a number, even for a rejected ADR.
next free is **0028**). Never reuse a number, even for a rejected ADR.
- When citing an ADR in code, plans, changesets, or AGENTS.md, cite the
number **with the filename** if the context could be ambiguous.
- Superseded ADRs stay in place with a `Superseded by` status link to the
Expand Down
41 changes: 23 additions & 18 deletions packages/every-plugin/src/build/rspack/fix-mf-data-uri-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,41 @@ import type { Compiler, RspackPluginInstance } from "@rspack/core";

const MF_DATA_URI_MARKER = "data:text/javascript,";

const MF_ABSOLUTE_IMPORT_RE = /(["'])\/[^"']*?\/node_modules\/(@module-federation\/[^"']*)/g;

export class FixMfDataUriPlugin implements RspackPluginInstance {
name = "FixMfDataUriPlugin";

apply(compiler: Compiler) {
compiler.hooks.compilation.tap(this.name, (_compilation, { normalModuleFactory }) => {
normalModuleFactory.hooks.beforeResolve.tap(this.name, (resolveData) => {
if (!resolveData?.request) return;
if (!resolveData.request.includes(MF_DATA_URI_MARKER)) return;
this.reencodeDataUri(resolveData);
resolveData.request = rewriteMfDataUriRequest(resolveData.request);
});
});
}
}

private reencodeDataUri(resolveData: { request: string }) {
const { request } = resolveData;
const idx = request.indexOf(MF_DATA_URI_MARKER);
if (idx === -1) return;

const contentStart = idx + MF_DATA_URI_MARKER.length;
const prefix = request.substring(0, contentStart);
const rawContent = request.substring(contentStart);
/**
* The MF runtime's generated data-URI module imports its runtime pieces by
* absolute node_modules paths (`require.resolve` output), which differ per
* machine. Rewriting them to bare specifiers keeps the module — and therefore
* the module id space — machine-independent; `mfDataUriAliases()` points the
* specifiers back at their on-disk targets for resolution.
*
* Only `@module-federation/*` imports are rewritten: other data-URI entries
* (e.g. Rsbuild's dev HMR client, `@rsbuild/core/dist/client/hmr.js`) import
* subpaths their package's exports map does not expose, so stripping their
* absolute prefix makes them unresolvable.
*/
export function rewriteMfDataUriRequest(request: string): string {
const idx = request.indexOf(MF_DATA_URI_MARKER);
if (idx === -1) return request;

// The MF runtime's generated data-URI module imports its runtime pieces
// by absolute node_modules paths (`require.resolve` output), which differ
// per machine. Rewriting them to bare specifiers keeps the module — and
// therefore the module id space — machine-independent; the composition
// aliases the specifiers to their on-disk targets for resolution.
const decoded = safeDecode(rawContent).replace(/(["'])\/[^"']*?\/node_modules\//g, "$1");
resolveData.request = prefix + encodeURIComponent(decoded);
}
const contentStart = idx + MF_DATA_URI_MARKER.length;
const prefix = request.substring(0, contentStart);
const decoded = safeDecode(request.substring(contentStart));
return prefix + encodeURIComponent(decoded.replace(MF_ABSOLUTE_IMPORT_RE, "$1$2"));
}

function safeDecode(content: string): string {
Expand Down
36 changes: 36 additions & 0 deletions packages/every-plugin/tests/unit/fix-mf-data-uri.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { rewriteMfDataUriRequest } from "../../src/build/rspack/fix-mf-data-uri-plugin";

const dataUri = (source: string) => `data:text/javascript,${encodeURIComponent(source)}`;
const decode = (request: string) =>
decodeURIComponent(request.slice("data:text/javascript,".length));

describe("rewriteMfDataUriRequest", () => {
it("strips the machine-absolute prefix from MF runtime imports", () => {
const request = dataUri(
[
`import * as runtime from "/Users/a/proj/node_modules/.pnpm/x/node_modules/@module-federation/webpack-bundler-runtime/dist/index.js";`,
`import plugin from '/home/b/node_modules/@module-federation/node/dist/src/runtimePlugin.js';`,
].join("\n"),
);
const out = decode(rewriteMfDataUriRequest(request));
expect(out).toContain(`"@module-federation/webpack-bundler-runtime/dist/index.js"`);
expect(out).toContain(`'@module-federation/node/dist/src/runtimePlugin.js'`);
expect(out).not.toContain("/node_modules/");
});

it("leaves non-MF absolute imports alone so unexported subpaths still resolve", () => {
const hmr = "/Users/a/proj/node_modules/@rsbuild/core/dist/client/hmr.js";
const overlay = "/Users/a/proj/node_modules/@rsbuild/core/dist/client/overlay.js";
const request = dataUri(
`import { init } from '${hmr}';\nimport '${overlay}';\ninit('t', {}, "localhost", 3003);`,
);
const out = decode(rewriteMfDataUriRequest(request));
expect(out).toContain(`'${hmr}'`);
expect(out).toContain(`'${overlay}'`);
});

it("passes through requests that are not data URIs", () => {
expect(rewriteMfDataUriRequest("./src/index.ts")).toBe("./src/index.ts");
});
});
3 changes: 2 additions & 1 deletion packages/everything-dev/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolveLocalDevelopmentPath } from "./config";
import type { WorkspaceDeployResult } from "./contract";
import { syncResolvedSharedDeps } from "./shared-deps";
import type { BosConfig, BosPluginRef, RuntimeConfig } from "./types";
import { describeError } from "./utils/error";
import { run } from "./utils/run";
import { padRight } from "./utils/string";
import { colors, icons } from "./utils/theme";
Expand Down Expand Up @@ -273,7 +274,7 @@ export async function buildWorkspaceTargets(opts: {
key: ws.key,
kind: ws.kind,
success: false,
error: result.reason?.message ?? "Unknown error",
error: describeError(result.reason),
});
}
}
Expand Down
9 changes: 5 additions & 4 deletions packages/everything-dev/src/commands/db.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Context, Effect } from "effect";
import { findConfigPath, MISSING_CONFIG_MESSAGE } from "../config";
import { describeError } from "../utils/error";
import { type BosBuilder, BosDepsTag } from "./shared";

export function registerDb(builder: BosBuilder) {
Expand Down Expand Up @@ -36,7 +37,7 @@ export function registerDb(builder: BosBuilder) {
plugin: input.plugin,
source: "remote" as const,
section: "",
error: error instanceof Error ? error.message : "Unknown error",
error: describeError(error),
};
}
}),
Expand Down Expand Up @@ -82,7 +83,7 @@ export function registerDb(builder: BosBuilder) {
appliedHashCount: 0,
expectedTables: [],
missingTables: [],
error: error instanceof Error ? error.message : "Unknown error",
error: describeError(error),
};
}
}),
Expand Down Expand Up @@ -112,9 +113,9 @@ export function registerDb(builder: BosBuilder) {
} catch (error) {
return {
status: "error" as const,
message: error instanceof Error ? error.message : "Unknown error",
message: describeError(error),
diagnosis: null,
error: error instanceof Error ? error.message : "Unknown error",
error: describeError(error),
};
}
}),
Expand Down
3 changes: 2 additions & 1 deletion packages/everything-dev/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { timePhase } from "../progress";
import { openResolution } from "../resolution/session";
import { syncResolvedSharedDeps } from "../shared-deps";
import type { BosConfig, BosConfigInput, StarterLevel } from "../types";
import { describeError } from "../utils/error";
import { saveBosConfig } from "../utils/save-config";
import type { BosBuilder } from "./shared";

Expand Down Expand Up @@ -351,7 +352,7 @@ export function registerInit(builder: BosBuilder) {
overrides: input.overrides,
filesCopied: 0,
timings: [],
error: error instanceof Error ? error.message : "Unknown error",
error: describeError(error),
};
}
}),
Expand Down
Loading
Loading