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
1,516 changes: 1,383 additions & 133 deletions .bos/generated/auth/contract.d.ts

Large diffs are not rendered by default.

33 changes: 26 additions & 7 deletions .bos/generated/runtime-map.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"generatedAt": "2026-11-05T22:12:47.714Z",
"generatedAt": "2026-11-12T07:23:35.229Z",
"env": "development",
"release": "2.0.0-rc.39",
"extends": "/Users/elliot.braem/workspace/product/activity.nearbuilders.org/bos.app.ts → bos://dev.everything.near/everything.dev",
"release": "2.0.0-rc.44",
"extends": "bos.app.ts → bos://dev.everything.near/everything.dev",
"sources": [
{
"key": "ui",
Expand All @@ -21,7 +21,8 @@
"name": "host",
"source": "remote",
"location": "https://cdn.everything.dev/bundles/dev.everything.near/everything.dev/host",
"integrity": "sha384-PsU9q6/jQS9heCznqcWkeG97L4hLl1aSENaOyK1y0syUzMK0M/gr2NTft3mkjx71"
"digest": "versions/3e37dfef1799bf87.json",
"integrity": "sha384-eEQI0AqhOr3tLmX2Y958qTtrISeBKBbIMgpMehyVqFVneXm0NVGhsKNMOO6CPJgs"
},
{
"key": "api",
Expand All @@ -37,9 +38,21 @@
"source": "remote",
"location": "https://cdn.everything.dev/bundles/dev.everything.near/everything.dev/auth",
"pluginManifestUrl": "https://cdn.everything.dev/bundles/dev.everything.near/everything.dev/auth/plugin.manifest.json",
"integrity": "sha384-FENdY7Q5xLCSYiT6gZOgFUFM0f0/9JVl8dVzmIs0oi1M9/eOwNA+4rT2mjl7NPQP",
"digest": "versions/f6835d88fcb01fdd.json",
"integrity": "sha384-outQaOt4wS2vf+UIeSfN3xIt074Ixerg2sLKtYSVyrEH9SUY4Sx6GQ8WZkkwdPdU",
"pluginVersion": "2.0.0-rc.8",
"contractSha256": "11e86ebd0af6581a6348df858fc88b5e583366553f2ebcaa4f31d787569c7fca"
"contractSha256": "c8022b18e4cedfc30d591060b8a9d81e65fdb3cf80658db441d3d4cd9401ee4d"
},
{
"key": "projects",
"kind": "plugin",
"name": "projects",
"source": "remote",
"location": "https://nearbuilders.org",
"pluginManifestUrl": "https://cdn.everything.dev/bundles/nearbuilding.near/nearbuilders.org/projects/plugin.manifest.json",
"callOnly": true,
"pluginVersion": "1.4.0",
"contractSha256": "9e920e55fee7831884e1442c9cccbcbd17c3b2c459a364fa62be6f911754cfc1"
}
],
"unresolved": [],
Expand Down Expand Up @@ -287,5 +300,11 @@
"nav": null
}
],
"shadowed": []
"shadowed": [],
"lockfile": {
"pointerDigest": "c0698d04fa560e36",
"release": "2.0.0-rc.44",
"resolvedAt": "2026-11-12T07:23:15.993Z",
"drift": []
}
}
4 changes: 2 additions & 2 deletions .bos/generated/shared-deps.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"schemaVersion": 1,
"kind": "everything-dev/shared-deps",
"generatedAt": "2026-11-05T17:37:02.703Z",
"generatedAt": "2026-11-12T07:06:48.228Z",
"deps": {},
"fingerprintSha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"generatedDigest": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"inputs": {
"mode": "catalog->bos",
"hostMode": "local"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ permissions:
jobs:
ci:
name: CI
uses: NEARBuilders/everything-dev/.github/workflows/consumer-ci.yml@v2.0.0-rc.39
uses: NEARBuilders/everything-dev/.github/workflows/consumer-ci.yml@v2.0.0-rc.44
with:
working_directory: .
secrets: inherit
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ permissions:
jobs:
deploy:
name: Deploy
uses: NEARBuilders/everything-dev/.github/workflows/consumer-deploy.yml@v2.0.0-rc.39
uses: NEARBuilders/everything-dev/.github/workflows/consumer-deploy.yml@v2.0.0-rc.44
with:
working_directory: .
verbose: ${{ inputs.verbose == true }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ permissions:
jobs:
deploy-staging:
name: Deploy Staging
uses: NEARBuilders/everything-dev/.github/workflows/consumer-staging.yml@v2.0.0-rc.39
uses: NEARBuilders/everything-dev/.github/workflows/consumer-staging.yml@v2.0.0-rc.44
with:
working_directory: .
verbose: ${{ inputs.verbose == true }}
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,9 @@ fabric.properties
:memory:

.turbo
# .bos is local-only runtime state (ADR 0047) — the single committed artifact
# is bos.lock.json at the repo root, not anything under .bos/.
.bos
!.bos/sync-snapshot.json
!.bos/infra-state.json
.bos/session.json

# alchemy state ledger (runtime artifact of hosting-adapter stacks)
Expand Down
7 changes: 2 additions & 5 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ export default createPlugin.withPlugins<PluginsClient>()({
.positive()
.default(APPROVED_SOURCE_DAILY_EVENT_LIMIT),
activityTrustedSourceDailyEventLimit: z.number().int().positive().optional(),
nearbuildersApiUrl: z.string().default("https://nearbuilders.org/api"),
}),

secrets: z.object({
Expand All @@ -229,7 +228,7 @@ export default createPlugin.withPlugins<PluginsClient>()({

contract,

initialize: (config, _plugins) =>
initialize: (config, plugins) =>
Effect.gen(function* () {
const database = yield* buildScoped(
DatabaseTag,
Expand Down Expand Up @@ -314,9 +313,7 @@ export default createPlugin.withPlugins<PluginsClient>()({
activityLeaderboard,
);
const activityEndorsementsService = new ActivityEndorsementsService(database);
const nearbuildersProjects = new NearbuildersProjectsClient(
config.variables.nearbuildersApiUrl,
);
const nearbuildersProjects = new NearbuildersProjectsClient(plugins.projects.client());
const activityBindingSessionsService = new ActivityBindingSessionsService(
database,
activityCredentialsService,
Expand Down
2 changes: 2 additions & 0 deletions api/src/lib/plugins-types.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ContractType as projectsContract } from "../../../.bos/generated/plugins/projects/contract.d.ts";
import type { ContractType as authContract } from "../../../.bos/generated/auth/contract.d.ts";
import type { RouterContract } from "@orpc/contract";
import type { PluginClientEntry } from "every-plugin";

export type PluginsClient = {
projects: PluginClientEntry<projectsContract>;
auth: PluginClientEntry<authContract>;
};
139 changes: 74 additions & 65 deletions api/src/services/nearbuilders-projects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ORPCError } from "@orpc/server";
import { ACTIVITY_SOURCE_ID_REGEX, NEAR_ACCOUNT_ID_REGEX } from "../contract";
import type { PluginsClient } from "../lib/plugins-types.gen";

export const NEARBUILDERS_SITE_URL = "https://nearbuilders.org";

Expand All @@ -8,15 +9,15 @@ const RANDOM_SLUG_SUFFIX_REGEX = /-(?=[a-z0-9]{6}$)(?=[a-z]*\d)[a-z0-9]{6}$/;
const NEARBUILDERS_HOSTS = new Set(["nearbuilders.org", "www.nearbuilders.org"]);
const PROJECT_KIND_SEGMENTS = new Set(["project", "idea", "scope", "result"]);

export interface NearbuildersProject {
id: string;
slug: string;
title: string;
ownerId: string | null;
domain: string | null;
logoUrl?: string | null;
apps?: Array<{ accountId: string | null }>;
}
export type ProjectsRpcClient = ReturnType<PluginsClient["projects"]["client"]>;

type ProjectRecord = Awaited<ReturnType<ProjectsRpcClient["getProject"]>>["data"];
type ProjectListItem = Awaited<ReturnType<ProjectsRpcClient["listProjects"]>>["data"][number];

export type NearbuildersProject = Pick<
ProjectListItem,
"id" | "slug" | "title" | "ownerId" | "domain" | "logoUrl"
> & { apps?: Array<Pick<ProjectRecord["apps"][number], "accountId">> };

export interface NearbuildersProjectSummary {
id: string;
Expand Down Expand Up @@ -121,25 +122,51 @@ export function deriveNearbuildersProjectDraft(
};
}

const REQUEST_TIMEOUT_MS = 5_000;

const callOptions = () => ({ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });

function rpcErrorCode(error: unknown): string | null {
const code = (error as { code?: unknown } | null)?.code;
return typeof code === "string" ? code : null;
}

function unavailable(error: unknown): ORPCError<string, unknown> {
const code = rpcErrorCode(error);
if (code === "NOT_FOUND") {
return new ORPCError("NOT_FOUND", { message: "nearbuilders.org project not found" });
}
return new ORPCError("SERVICE_UNAVAILABLE", {
message: code
? `nearbuilders.org returned ${code}. Try again shortly.`
: "nearbuilders.org could not be reached. Try again shortly.",
});
}

function expectData<T>(response: { data?: T } | null | undefined): T {
if (response?.data === undefined) {
throw new ORPCError("SERVICE_UNAVAILABLE", {
message: "nearbuilders.org returned an unexpected response",
});
}
return response.data;
}

export class NearbuildersProjectsClient {
readonly #apiUrl: string;
readonly #fetch: typeof fetch;
readonly #rpc: ProjectsRpcClient;

constructor(apiUrl: string, fetcher: typeof fetch = (...args) => fetch(...args)) {
this.#apiUrl = apiUrl.replace(/\/$/, "");
this.#fetch = fetcher;
constructor(rpc: ProjectsRpcClient) {
this.#rpc = rpc;
}

async resolve(input: string): Promise<NearbuildersProjectDraft> {
const reference = parseNearbuildersProjectReference(input);
if (reference.kind === "id") {
return deriveNearbuildersProjectDraft(
await this.#getProject(`/v1/projects/${encodeURIComponent(reference.id)}`, input),
);
return deriveNearbuildersProjectDraft(await this.#getProject(reference.id, input));
}
if (reference.kind === "slug") {
const project = await this.#findProject(
`/v1/projects/by-slug/${encodeURIComponent(reference.slug)}`,
const project = await this.#findProject(() =>
this.#rpc.getProjectBySlug({ slug: reference.slug }, callOptions()),
);
if (project) return deriveNearbuildersProjectDraft(project);
if (reference.fromLink) {
Expand All @@ -153,19 +180,17 @@ export class NearbuildersProjectsClient {
}

async getOwnerAccountIds(projectId: string): Promise<string[]> {
const project = await this.#getProject(
`/v1/projects/${encodeURIComponent(projectId)}`,
projectId,
);
return projectOwnerAccountIds(project);
return projectOwnerAccountIds(await this.#getProject(projectId, projectId));
}

async search(query: string): Promise<NearbuildersProjectSummary[]> {
const trimmed = query.trim().slice(0, 200);
if (trimmed.length < 2) return [];
const matches = await this.#request<NearbuildersProject[]>(
`/v1/projects?query=${encodeURIComponent(trimmed)}&kind=project&limit=${NEARBUILDERS_PROJECT_SEARCH_LIMIT}`,
);
const matches = await this.#list({
query: trimmed,
kind: "project",
limit: NEARBUILDERS_PROJECT_SEARCH_LIMIT,
});
return matches.slice(0, NEARBUILDERS_PROJECT_SEARCH_LIMIT).map((project) => ({
id: project.id,
slug: project.slug,
Expand All @@ -179,9 +204,7 @@ export class NearbuildersProjectsClient {
}

async #search(query: string, input: string): Promise<NearbuildersProject> {
const matches = await this.#request<NearbuildersProject[]>(
`/v1/projects?query=${encodeURIComponent(query.slice(0, 200))}`,
);
const matches = await this.#list({ query: query.slice(0, 200) });
const wanted = query.trim().toLowerCase();
const exact = matches.filter(
({ title, slug }) =>
Expand All @@ -204,8 +227,18 @@ export class NearbuildersProjectsClient {
});
}

async #getProject(path: string, input: string): Promise<NearbuildersProject> {
const project = await this.#findProject(path);
async #list(input: Parameters<ProjectsRpcClient["listProjects"]>[0]): Promise<ProjectListItem[]> {
let response: { data?: ProjectListItem[] };
try {
response = await this.#rpc.listProjects(input, callOptions());
} catch (error) {
throw unavailable(error);
}
return expectData(response);
}

async #getProject(id: string, input: string): Promise<NearbuildersProject> {
const project = await this.#findProject(() => this.#rpc.getProject({ id }, callOptions()));
if (!project) {
throw new ORPCError("NOT_FOUND", {
message: `No nearbuilders.org project matches "${input.trim()}"`,
Expand All @@ -214,41 +247,17 @@ export class NearbuildersProjectsClient {
return project;
}

async #findProject(path: string): Promise<NearbuildersProject | null> {
async #findProject(
request: () => Promise<{ data?: ProjectRecord }>,
): Promise<NearbuildersProject | null> {
let response: { data?: ProjectRecord };
try {
return await this.#request<NearbuildersProject>(path);
response = await request();
} catch (error) {
if (error instanceof ORPCError && error.code === "NOT_FOUND") return null;
throw error;
}
}

async #request<T>(path: string): Promise<T> {
let response: Response;
try {
response = await this.#fetch(`${this.#apiUrl}${path}`, {
headers: { accept: "application/json", "user-agent": "activity.nearbuilders.org" },
signal: AbortSignal.timeout(5_000),
});
} catch {
throw new ORPCError("SERVICE_UNAVAILABLE", {
message: "nearbuilders.org could not be reached. Try again shortly.",
});
}
if (response.status === 404) {
throw new ORPCError("NOT_FOUND", { message: "nearbuilders.org project not found" });
}
if (!response.ok) {
throw new ORPCError("SERVICE_UNAVAILABLE", {
message: `nearbuilders.org returned ${response.status}. Try again shortly.`,
});
}
const body = (await response.json()) as { data?: T };
if (body.data === undefined) {
throw new ORPCError("SERVICE_UNAVAILABLE", {
message: "nearbuilders.org returned an unexpected response",
});
const failure = unavailable(error);
if (failure.code === "NOT_FOUND") return null;
throw failure;
}
return body.data;
return expectData(response);
}
}
Loading
Loading