From 3a82416dfa0c7eecbb1f2dcabbb0b5af7b93c3e1 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 13 Aug 2026 13:55:46 -0500 Subject: [PATCH 01/24] wip --- .env.example | 3 - api/src/contract.ts | 30 ++ .../db/migrations/0002_awesome_madame_web.sql | 3 + api/src/db/migrations/meta/0002_snapshot.json | 176 ++++++++ api/src/db/migrations/meta/_journal.json | 9 +- api/src/db/schema.ts | 13 +- api/src/index.ts | 10 + api/src/services/tenants.ts | 52 ++- api/tests/unit/tenants-service.test.ts | 56 +++ bos.config.json | 3 - host/.env.example | 3 - host/src/services/binding-resolver.ts | 148 +++++++ host/src/services/tenant-runtime.ts | 164 +------- .../integration/tenant-host-nested.test.ts | 45 ++- host/tests/integration/tenant-runtime.test.ts | 322 ++++++++++----- tests/regression/http/tenant_bindings_test.go | 184 +++++++++ ui/src/routes/_layout.tsx | 380 +----------------- ui/src/routes/_layout/_authenticated.tsx | 251 +++++++++++- .../_layout/_authenticated/admin/index.tsx | 104 +++++ .../_layout/_authenticated/admin/system.tsx | 73 ++++ ui/src/routes/_layout/_public.tsx | 53 +++ ui/src/routes/_layout/{ => _public}/about.tsx | 2 +- ui/src/routes/_layout/_public/index.tsx | 111 +++++ ui/src/routes/_layout/{ => _public}/login.tsx | 50 +-- ui/src/routes/_layout/index.tsx | 122 ------ ui/src/routes/_layout/skill.tsx | 126 ------ 26 files changed, 1563 insertions(+), 930 deletions(-) create mode 100644 api/src/db/migrations/0002_awesome_madame_web.sql create mode 100644 api/src/db/migrations/meta/0002_snapshot.json create mode 100644 host/src/services/binding-resolver.ts create mode 100644 tests/regression/http/tenant_bindings_test.go create mode 100644 ui/src/routes/_layout/_authenticated/admin/index.tsx create mode 100644 ui/src/routes/_layout/_authenticated/admin/system.tsx create mode 100644 ui/src/routes/_layout/_public.tsx rename ui/src/routes/_layout/{ => _public}/about.tsx (99%) create mode 100644 ui/src/routes/_layout/_public/index.tsx rename ui/src/routes/_layout/{ => _public}/login.tsx (86%) delete mode 100644 ui/src/routes/_layout/index.tsx delete mode 100644 ui/src/routes/_layout/skill.tsx diff --git a/.env.example b/.env.example index 450772d4..452789fd 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,6 @@ # app.host CORS_ORIGIN=http://localhost:4100 -TENANT_WHITELIST= -ALLOW_OVERRIDE= -ALLOW_UNTRUSTED_SSR= CSP_STRICT= # app.api diff --git a/api/src/contract.ts b/api/src/contract.ts index 5887408c..92bc79b2 100644 --- a/api/src/contract.ts +++ b/api/src/contract.ts @@ -20,6 +20,9 @@ export const TenantSchema = z.object({ orgId: z.string(), name: z.string(), status: TenantStatusSchema, + allowUiOverrides: z.boolean(), + allowBackendOverrides: z.boolean(), + allowSsr: z.boolean(), createdAt: z.string(), updatedAt: z.string(), deletedAt: z.string().nullable(), @@ -27,6 +30,17 @@ export const TenantSchema = z.object({ export type Tenant = z.infer; +export const TenantBindingSchema = z.object({ + hostname: z + .string() + .describe("Subdomain hostname that routes to this tenant on the parent domain"), + accountId: z.string(), + allowUiOverrides: z.boolean(), + allowBackendOverrides: z.boolean(), + allowSsr: z.boolean(), + status: TenantStatusSchema, +}); + const ThingSchema = z.object({ thingId: z.string().describe("Unique identifier for the thing"), type: z.string().describe("Plugin-derived thing type"), @@ -80,6 +94,9 @@ export const contract = oc.router({ name: z.string(), accountId: z.string(), status: z.enum(["active", "pending"]).optional(), + allowUiOverrides: z.boolean().default(true), + allowBackendOverrides: z.boolean().default(false), + allowSsr: z.boolean().default(false), }), ) .output(TenantSchema) @@ -94,6 +111,9 @@ export const contract = oc.router({ subdomain: z.string().optional(), accountId: z.string().optional(), status: TenantStatusSchema.optional(), + allowUiOverrides: z.boolean().optional(), + allowBackendOverrides: z.boolean().optional(), + allowSsr: z.boolean().optional(), }), ) .output(TenantSchema) @@ -128,6 +148,16 @@ export const contract = oc.router({ .output(TenantSchema) .errors({ NOT_FOUND }), + listTenantBindings: oc + .route({ + method: "GET", + path: "/tenants/bindings", + summary: "List all active tenant domain bindings", + description: + "Public — returns the subdomain-to-config mapping used by the host's BindingResolver.", + }) + .output(z.array(TenantBindingSchema)), + tenantPreflight: oc .route({ method: "POST", path: "/tenants/preflight" }) .input( diff --git a/api/src/db/migrations/0002_awesome_madame_web.sql b/api/src/db/migrations/0002_awesome_madame_web.sql new file mode 100644 index 00000000..e39a3327 --- /dev/null +++ b/api/src/db/migrations/0002_awesome_madame_web.sql @@ -0,0 +1,3 @@ +ALTER TABLE "tenants" ADD COLUMN "allow_ui_overrides" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "tenants" ADD COLUMN "allow_backend_overrides" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "tenants" ADD COLUMN "allow_ssr" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/api/src/db/migrations/meta/0002_snapshot.json b/api/src/db/migrations/meta/0002_snapshot.json new file mode 100644 index 00000000..fda7cde0 --- /dev/null +++ b/api/src/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,176 @@ +{ + "id": "7b8a421a-d0c7-414a-96e8-0c259935178b", + "prevId": "3e88c17e-53d5-4809-b872-33d2f87a8a90", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "tenant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "allow_ui_overrides": { + "name": "allow_ui_overrides", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_backend_overrides": { + "name": "allow_backend_overrides", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allow_ssr": { + "name": "allow_ssr", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tenants_subdomain_idx": { + "name": "tenants_subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tenants_account_id_idx": { + "name": "tenants_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenants_subdomain_unique": { + "name": "tenants_subdomain_unique", + "nullsNotDistinct": false, + "columns": [ + "subdomain" + ] + }, + "tenants_account_id_unique": { + "name": "tenants_account_id_unique", + "nullsNotDistinct": false, + "columns": [ + "account_id" + ] + }, + "tenants_org_id_unique": { + "name": "tenants_org_id_unique", + "nullsNotDistinct": false, + "columns": [ + "org_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.tenant_status": { + "name": "tenant_status", + "schema": "public", + "values": [ + "active", + "pending", + "suspended", + "pending_deletion" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/api/src/db/migrations/meta/_journal.json b/api/src/db/migrations/meta/_journal.json index c939ec4a..9e5e42bb 100644 --- a/api/src/db/migrations/meta/_journal.json +++ b/api/src/db/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1786078137908, "tag": "0001_brief_magik", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1786645659094, + "tag": "0002_awesome_madame_web", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index b141f76b..c689380d 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -1,4 +1,12 @@ -import { pgEnum, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; +import { + boolean, + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; export const tenantStatus = pgEnum("tenant_status", [ "active", @@ -16,6 +24,9 @@ export const tenants = pgTable( orgId: text("org_id").notNull().unique(), name: text("name").notNull(), status: tenantStatus("status").default("active").notNull(), + allowUiOverrides: boolean("allow_ui_overrides").default(true).notNull(), + allowBackendOverrides: boolean("allow_backend_overrides").default(false).notNull(), + allowSsr: boolean("allow_ssr").default(false).notNull(), createdAt: timestamp("created_at", { mode: "date", withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { mode: "date", withTimezone: true }).defaultNow().notNull(), deletedAt: timestamp("deleted_at", { mode: "date", withTimezone: true }), diff --git a/api/src/index.ts b/api/src/index.ts index df235000..9ebbaed8 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -144,6 +144,9 @@ export default createPlugin.withPlugins()({ accountId: input.accountId, orgId: context.organization.activeOrganizationId, status: input.status, + allowUiOverrides: input.allowUiOverrides, + allowBackendOverrides: input.allowBackendOverrides, + allowSsr: input.allowSsr, }); }), @@ -159,6 +162,9 @@ export default createPlugin.withPlugins()({ subdomain: input.subdomain, accountId: input.accountId, status: input.status, + allowUiOverrides: input.allowUiOverrides, + allowBackendOverrides: input.allowBackendOverrides, + allowSsr: input.allowSsr, }); }), @@ -223,6 +229,10 @@ export default createPlugin.withPlugins()({ return tenant; }), + listTenantBindings: builder.listTenantBindings.handler(async () => + services.tenants.listBindings(), + ), + tenantPreflight: builder.tenantPreflight.use(requireAuth).handler(async ({ input }) => { const subdomainValid = SUBDOMAIN_SEGMENT_REGEX.test(input.subdomain); const accountId = `${input.subdomain}.${input.parentAccount}`; diff --git a/api/src/services/tenants.ts b/api/src/services/tenants.ts index 2754489d..4cdb1aa5 100644 --- a/api/src/services/tenants.ts +++ b/api/src/services/tenants.ts @@ -13,25 +13,46 @@ export interface TenantRecord { orgId: string; name: string; status: TenantStatus; + allowUiOverrides: boolean; + allowBackendOverrides: boolean; + allowSsr: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; } +export interface TenantBinding { + hostname: string; + accountId: string; + allowUiOverrides: boolean; + allowBackendOverrides: boolean; + allowSsr: boolean; + status: TenantStatus; +} + export interface TenantInput { subdomain: string; name: string; accountId: string; orgId: string; status?: TenantStatus; + allowUiOverrides?: boolean; + allowBackendOverrides?: boolean; + allowSsr?: boolean; } export interface TenantsService { listTenantsByOrgIds(orgIds: string[]): Promise; + listBindings(): Promise; createTenant(input: TenantInput): Promise; updateTenant( id: string, - input: Partial>, + input: Partial< + Pick< + TenantInput, + "name" | "subdomain" | "accountId" | "status" | "allowUiOverrides" | "allowBackendOverrides" | "allowSsr" + > + >, ): Promise; softDeleteTenant(id: string): Promise; suspendTenant(id: string): Promise; @@ -55,6 +76,9 @@ function toTenantRecord(row: TenantRow): TenantRecord { orgId: row.orgId, name: row.name, status: row.status, + allowUiOverrides: row.allowUiOverrides, + allowBackendOverrides: row.allowBackendOverrides, + allowSsr: row.allowSsr, createdAt: row.createdAt instanceof Date ? row.createdAt.toISOString() : String(row.createdAt), updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : String(row.updatedAt), deletedAt: row.deletedAt instanceof Date ? row.deletedAt.toISOString() : null, @@ -88,6 +112,27 @@ export const TenantsLive = Layer.effect( } }, + listBindings: async () => { + try { + const rows = await db + .select({ + subdomain: tenantsTable.subdomain, + accountId: tenantsTable.accountId, + allowUiOverrides: tenantsTable.allowUiOverrides, + allowBackendOverrides: tenantsTable.allowBackendOverrides, + allowSsr: tenantsTable.allowSsr, + status: tenantsTable.status, + }) + .from(tenantsTable); + return rows.map(({ subdomain, ...binding }) => ({ + ...binding, + hostname: subdomain, + })); + } catch (error) { + throw toOrpcError(error); + } + }, + createTenant: async (input) => { try { const [row] = await db @@ -98,6 +143,11 @@ export const TenantsLive = Layer.effect( accountId: input.accountId, orgId: input.orgId, ...(input.status !== undefined && { status: input.status }), + ...(input.allowUiOverrides !== undefined && { allowUiOverrides: input.allowUiOverrides }), + ...(input.allowBackendOverrides !== undefined && { + allowBackendOverrides: input.allowBackendOverrides, + }), + ...(input.allowSsr !== undefined && { allowSsr: input.allowSsr }), }) .onConflictDoNothing() .returning(); diff --git a/api/tests/unit/tenants-service.test.ts b/api/tests/unit/tenants-service.test.ts index 427ad07f..fd04bd4b 100644 --- a/api/tests/unit/tenants-service.test.ts +++ b/api/tests/unit/tenants-service.test.ts @@ -147,6 +147,62 @@ describe("TenantsService", () => { expect(await runService(layer, (svc) => svc.listTenantsByOrgIds([]))).toEqual([]); }); + it("lists active and non-deleted bindings with default permissions", async () => { + const layer = freshLayer(); + await runService(layer, (svc) => + svc.createTenant({ + ...baseInput, + subdomain: "acme", + accountId: "acme.example.near", + }), + ); + const suspended = await runService(layer, (svc) => + svc.createTenant({ + ...baseInput, + subdomain: "beta", + accountId: "beta.example.near", + orgId: "org-2", + }), + ); + await runService(layer, (svc) => svc.suspendTenant(suspended.id)); + + const bindings = await runService(layer, (svc) => svc.listBindings()); + + expect(bindings).toHaveLength(2); + expect(bindings.find((b) => b.hostname === "acme")).toMatchObject({ + hostname: "acme", + accountId: "acme.example.near", + allowUiOverrides: true, + allowBackendOverrides: false, + allowSsr: false, + status: "active", + }); + expect(bindings.find((b) => b.hostname === "beta")?.status).toBe("suspended"); + }); + + it("persists allow_* overrides on create and update", async () => { + const layer = freshLayer(); + const created = await runService(layer, (svc) => + svc.createTenant({ + ...baseInput, + allowUiOverrides: false, + allowBackendOverrides: true, + allowSsr: true, + }), + ); + expect(created).toMatchObject({ + allowUiOverrides: false, + allowBackendOverrides: true, + allowSsr: true, + }); + + const updated = await runService(layer, (svc) => + svc.updateTenant(created.id, { allowSsr: false }), + ); + expect(updated.allowSsr).toBe(false); + expect(updated.allowUiOverrides).toBe(false); + }); + it("updates a tenant name", async () => { const layer = freshLayer(); const created = await runService(layer, (svc) => svc.createTenant(baseInput)); diff --git a/bos.config.json b/bos.config.json index b31993ec..436021f2 100644 --- a/bos.config.json +++ b/bos.config.json @@ -18,9 +18,6 @@ "production": "https://elliot-braem-7253-host-everything-dev-nearbuilder-aa7d0803c-ze.zephyrcloud.app", "secrets": [ "CORS_ORIGIN", - "TENANT_WHITELIST", - "ALLOW_OVERRIDE", - "ALLOW_UNTRUSTED_SSR", "CSP_STRICT" ] }, diff --git a/host/.env.example b/host/.env.example index 1283ab78..f5d82332 100644 --- a/host/.env.example +++ b/host/.env.example @@ -1,8 +1,5 @@ # app.host CORS_ORIGIN=http://localhost:3000 -TENANT_WHITELIST= -ALLOW_OVERRIDE= -ALLOW_UNTRUSTED_SSR= CSP_STRICT= # app.api diff --git a/host/src/services/binding-resolver.ts b/host/src/services/binding-resolver.ts new file mode 100644 index 00000000..30ed5ee4 --- /dev/null +++ b/host/src/services/binding-resolver.ts @@ -0,0 +1,148 @@ +import { logger } from "../utils/logger"; +import { resolveDomain } from "../utils/normalize"; +import type { RuntimeConfig } from "./config"; + +export type TenantBindingStatus = "active" | "pending" | "suspended" | "pending_deletion"; + +export interface TenantBinding { + hostname: string; + accountId: string; + allowUiOverrides: boolean; + allowBackendOverrides: boolean; + allowSsr: boolean; + status: TenantBindingStatus; +} + +const BINDINGS_TTL_MS = 30_000; + +interface CachedBindings { + apiUrl: string; + expiresAt: number; + entries: Map; + refetching?: Promise>; +} + +let bindingsCache: CachedBindings | null = null; + +export function clearBindingResolverCache() { + bindingsCache = null; +} + +function isBaseHost(hostname: string, gatewayId: string): boolean { + const normalized = hostname.toLowerCase(); + return ( + normalized === gatewayId || + normalized === "localhost" || + normalized === "127.0.0.1" + ); +} + +async function fetchBindingsFromApi(apiUrl: string): Promise { + const endpoint = `${apiUrl.replace(/\/$/, "")}/api/tenants/bindings`; + let response: Response; + try { + response = await fetch(endpoint); + } catch (cause) { + throw new Error(`Failed to reach API at ${endpoint}: ${String(cause)}`, { cause }); + } + + if (!response.ok) { + throw new Error(`GET ${endpoint} failed with HTTP ${response.status}`); + } + + return (await response.json()) as TenantBinding[]; +} + +function resolveGatewayId(config: RuntimeConfig): string { + return resolveDomain(config.domain, config.host.url).toLowerCase(); +} + +function mapBindingsByHostname( + config: RuntimeConfig, + bindings: TenantBinding[], +): Map { + const gatewayId = resolveGatewayId(config); + const entries = new Map(); + for (const binding of bindings) { + entries.set(`${binding.hostname.toLowerCase()}.${gatewayId}`, binding); + } + return entries; +} + +function ensureBindingsLoaded( + config: RuntimeConfig, +): Promise> { + const apiUrl = config.api?.url; + if (!apiUrl) { + return Promise.resolve(new Map()); + } + + const now = Date.now(); + if ( + bindingsCache && + bindingsCache.apiUrl === apiUrl && + bindingsCache.expiresAt > now && + bindingsCache.entries.size > 0 + ) { + return Promise.resolve(bindingsCache.entries); + } + + if (bindingsCache?.apiUrl === apiUrl && bindingsCache.refetching) { + return bindingsCache.refetching; + } + + const staleEntries = + bindingsCache?.apiUrl === apiUrl ? bindingsCache.entries : null; + + const fetchPromise = fetchBindingsFromApi(apiUrl) + .then((bindings) => { + const entries = mapBindingsByHostname(config, bindings); + bindingsCache = { apiUrl, expiresAt: Date.now() + BINDINGS_TTL_MS, entries }; + return entries; + }) + .catch((cause) => { + if (staleEntries && staleEntries.size > 0) { + logger.error( + `[BindingResolver] Refresh failed, serving ${staleEntries.size} stale binding(s): ${cause instanceof Error ? cause.message : String(cause)}`, + ); + bindingsCache = { + apiUrl, + expiresAt: Date.now() + BINDINGS_TTL_MS, + entries: staleEntries, + }; + return staleEntries; + } + bindingsCache = null; + throw cause; + }); + + bindingsCache = { + apiUrl, + expiresAt: now + BINDINGS_TTL_MS, + entries: staleEntries ?? new Map(), + refetching: fetchPromise, + }; + + return fetchPromise; +} + +export interface BindingResolver { + resolve(hostname: string): Promise; + clear(): void; +} + +export function createBindingResolver(config: RuntimeConfig): BindingResolver { + return { + async resolve(hostname: string): Promise { + const normalized = hostname.toLowerCase(); + const gatewayId = resolveGatewayId(config); + if (isBaseHost(normalized, gatewayId)) { + return null; + } + + const entries = await ensureBindingsLoaded(config); + return entries.get(normalized) ?? null; + }, + clear: clearBindingResolverCache, + }; +} diff --git a/host/src/services/tenant-runtime.ts b/host/src/services/tenant-runtime.ts index 5909f09b..4f3b0cae 100644 --- a/host/src/services/tenant-runtime.ts +++ b/host/src/services/tenant-runtime.ts @@ -1,28 +1,25 @@ import { buildRuntimeConfig, - isRuntimeOverrideAllowed, loadRemoteConfig, - parseRuntimeOverrideTargets, type RuntimeConfig, } from "everything-dev/config"; import { verifySriForUrl } from "everything-dev/integrity"; import type { RuntimePlugin } from "../types"; import { logger } from "../utils/logger"; import { resolveDomain } from "../utils/normalize"; +import { createBindingResolver, type BindingResolver } from "./binding-resolver"; const REMOTE_CONFIG_TTL_MS = 30_000; const VERIFICATION_TTL_MS = 5 * 60_000; const MAX_REMOTE_CONFIG_CACHE_SIZE = 256; const MAX_VERIFICATION_CACHE_SIZE = 512; -const NEAR_ACCOUNT_ID_REGEX = - /^(?=.{2,64}$)([a-z0-9]+(?:[-_][a-z0-9]+)*)(\.([a-z0-9]+(?:[-_][a-z0-9]+)*))*$/; -type RuntimeOverrideTarget = ReturnType[number]; type BosEnv = "development" | "production" | "staging"; type IntegrityVerificationMode = "blocking" | "stale-while-revalidate"; interface ResolveRequestRuntimeOptions { verification?: IntegrityVerificationMode; + bindingResolver?: BindingResolver; } interface CachedRemoteConfig { @@ -55,9 +52,6 @@ export class TenantRuntimeError extends Error { const remoteConfigCache = new Map(); const verifiedUiCache = new Map(); -const unsupportedOverrideWarnings = new Set(); -let tenantWhitelistCache: { raw: string; value: Set } | null = null; -let allowedOverridesCache: { raw: string; value: RuntimeOverrideTarget[] } | null = null; function pruneExpiredCacheEntries( cache: Map, @@ -96,92 +90,6 @@ export function getTenantRuntimeErrorResponse(error: unknown): { status: number; export function clearTenantRuntimeCaches() { remoteConfigCache.clear(); verifiedUiCache.clear(); - unsupportedOverrideWarnings.clear(); - tenantWhitelistCache = null; - allowedOverridesCache = null; -} - -function parseBoolean(value: string | undefined): boolean { - if (!value) return false; - return ["1", "true", "yes", "on"].includes(value.toLowerCase()); -} - -function getTenantWhitelist(): Set { - const raw = process.env.TENANT_WHITELIST ?? ""; - if (tenantWhitelistCache?.raw === raw) { - return tenantWhitelistCache.value; - } - - const value = new Set( - raw - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean), - ); - tenantWhitelistCache = { raw, value }; - return value; -} - -function getAllowedOverrides(): RuntimeOverrideTarget[] { - const raw = process.env.ALLOW_OVERRIDE ?? ""; - if (allowedOverridesCache?.raw === raw) { - return allowedOverridesCache.value; - } - - const value = parseRuntimeOverrideTargets(raw); - allowedOverridesCache = { raw, value }; - return value; -} - -function warnUnsupportedOverrideTargets(targets: ReadonlyArray) { - for (const target of targets) { - if (target === "ui" || target === "plugins" || target.startsWith("plugins.")) { - continue; - } - - if (!unsupportedOverrideWarnings.has(target)) { - unsupportedOverrideWarnings.add(target); - logger.warn( - `[Tenant Runtime] Ignoring unsupported override target "${target}" in fixed-core mode`, - ); - } - } -} - -function resolveTenantAccountId( - hostname: string, - gatewayId: string, - namespaceAccountId: string, -): string | null { - const normalizedHost = hostname.toLowerCase(); - const normalizedGateway = gatewayId.toLowerCase(); - const normalizedNamespaceAccountId = namespaceAccountId.toLowerCase(); - - if ( - normalizedHost === normalizedGateway || - normalizedHost === "localhost" || - normalizedHost === "127.0.0.1" - ) { - return null; - } - - const suffix = `.${normalizedGateway}`; - if (!normalizedHost.endsWith(suffix)) { - return null; - } - - const tenantLabel = normalizedHost.slice(0, -suffix.length); - const tenantSegments = tenantLabel.split(".").filter(Boolean); - if (tenantSegments.length === 0 || tenantSegments.join(".") !== tenantLabel) { - throw new TenantRuntimeError(`Invalid tenant host: ${hostname}`, 404); - } - - const accountId = `${tenantSegments.join(".")}.${normalizedNamespaceAccountId}`; - if (!NEAR_ACCOUNT_ID_REGEX.test(accountId)) { - throw new TenantRuntimeError(`Invalid tenant account: ${accountId}`, 404); - } - - return accountId; } function getRemoteConfigCached(bosUrl: string, env: BosEnv) { @@ -347,16 +255,6 @@ async function verifyPluginUiIntegrity( ); } -function isPluginOverrideAllowed( - allowedOverrides: ReadonlyArray, - pluginKey: string, -): boolean { - return ( - isRuntimeOverrideAllowed(allowedOverrides, "plugins") || - isRuntimeOverrideAllowed(allowedOverrides, `plugins.${pluginKey}`) - ); -} - function buildEffectivePluginConfig( basePlugin: RuntimePlugin, tenantPlugin: RuntimePlugin, @@ -372,10 +270,9 @@ function buildEffectiveRuntimeConfig( baseConfig: RuntimeConfig, tenantConfig: RuntimeConfig, tenantAccountId: string, - allowedOverrides: ReadonlyArray, + allowUiOverrides: boolean, + allowBackendOverrides: boolean, ): RuntimeConfig { - warnUnsupportedOverrideTargets(allowedOverrides); - const effectiveConfig: RuntimeConfig = { ...baseConfig, account: tenantAccountId, @@ -385,7 +282,7 @@ function buildEffectiveRuntimeConfig( repository: tenantConfig.repository, }; - if (isRuntimeOverrideAllowed(allowedOverrides, "ui")) { + if (allowUiOverrides) { effectiveConfig.ui = tenantConfig.ui; } @@ -399,7 +296,7 @@ function buildEffectiveRuntimeConfig( continue; } - if (!isPluginOverrideAllowed(allowedOverrides, pluginKey)) { + if (!allowBackendOverrides) { continue; } @@ -412,29 +309,6 @@ function buildEffectiveRuntimeConfig( return effectiveConfig; } -function matchesTenantPattern(accountId: string, pattern: string): boolean { - if (pattern === accountId) return true; - if (pattern.startsWith("*.") && accountId.endsWith(pattern.slice(1))) return true; - return false; -} - -function isSsrAllowed(accountId: string): boolean { - if (parseBoolean(process.env.ALLOW_UNTRUSTED_SSR)) { - return true; - } - - const whitelist = getTenantWhitelist(); - for (const entry of whitelist) { - if (matchesTenantPattern(accountId, entry)) return true; - } - return false; -} - -function getTenantStatus(remoteConfig: Awaited>): string { - const raw = remoteConfig.rawConfig as { status?: string } | undefined; - return raw?.status ?? "active"; -} - export async function resolveRequestRuntime( baseConfig: RuntimeConfig, request: Request, @@ -443,8 +317,9 @@ export async function resolveRequestRuntime( const verificationMode = options?.verification ?? "blocking"; const url = new URL(request.url); const gatewayId = resolveDomain(baseConfig.domain, baseConfig.host.url); - const tenantAccountId = resolveTenantAccountId(url.hostname, gatewayId, baseConfig.account); - if (!tenantAccountId) { + const bindingResolver = options?.bindingResolver ?? createBindingResolver(baseConfig); + const binding = await bindingResolver.resolve(url.hostname); + if (!binding) { return { config: baseConfig, tenantAccountId: null, @@ -453,6 +328,14 @@ export async function resolveRequestRuntime( }; } + const tenantAccountId = binding.accountId; + if (binding.status === "suspended") { + throw new TenantRuntimeError("Tenant is suspended", 503); + } + if (binding.status === "pending_deletion") { + throw new TenantRuntimeError("Tenant has been deleted", 410); + } + const bosUrl = `bos://${tenantAccountId}/${gatewayId}`; const remoteConfig = await getRemoteConfigCached(bosUrl, "production"); const baseBosUrl = `bos://${baseConfig.account}/${gatewayId}`; @@ -483,17 +366,10 @@ export async function resolveRequestRuntime( baseConfig, tenantRuntimeConfig, tenantAccountId, - getAllowedOverrides(), + binding.allowUiOverrides, + binding.allowBackendOverrides, ); - const tenantStatus = getTenantStatus(remoteConfig); - if (tenantStatus === "suspended") { - throw new TenantRuntimeError("Tenant is suspended", 503); - } - if (tenantStatus === "pending_deletion") { - throw new TenantRuntimeError("Tenant has been deleted", 410); - } - if (effectiveConfig.ui.url !== baseConfig.ui.url) { await verifyUiIntegrity(effectiveConfig, verificationMode); } @@ -512,7 +388,7 @@ export async function resolveRequestRuntime( const ssrAllowed = Boolean(effectiveConfig.ui.ssrUrl) && Boolean(effectiveConfig.ui.ssrIntegrity) && - isSsrAllowed(tenantAccountId); + binding.allowSsr; return { config: ssrAllowed diff --git a/host/tests/integration/tenant-host-nested.test.ts b/host/tests/integration/tenant-host-nested.test.ts index e2891320..ccde7073 100644 --- a/host/tests/integration/tenant-host-nested.test.ts +++ b/host/tests/integration/tenant-host-nested.test.ts @@ -11,19 +11,6 @@ vi.mock("everything-dev/config", async () => { await vi.importActual("everything-dev/config"); return { ...actual, - parseRuntimeOverrideTargets: (value?: string | null) => - value - ? [ - ...new Set( - value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean), - ), - ] - : [], - isRuntimeOverrideAllowed: (targets: string[], target: string) => - targets.includes(target) || (target.startsWith("plugins.") && targets.includes("plugins.*")), loadRemoteConfig: loadRemoteConfigMock, buildRuntimeConfig: buildRuntimeConfigMock, }; @@ -39,6 +26,29 @@ vi.mock("everything-dev/integrity", async () => { }; }); +vi.mock("../../src/services/binding-resolver", async () => { + const actual = await vi.importActual( + "../../src/services/binding-resolver", + ); + return { + ...actual, + createBindingResolver: () => ({ + resolve: async (hostname: string) => + hostname === "chicago.alice.linktree.com" + ? { + hostname: "chicago.alice.linktree.com", + accountId: "chicago.alice.linktree.near", + allowUiOverrides: true, + allowBackendOverrides: false, + allowSsr: false, + status: "active", + } + : null, + clear: () => {}, + }), + }; +}); + const { runServer } = await import("../../src/program"); function createBaseConfig() { @@ -117,9 +127,6 @@ describe("tenant host nested integration", () => { const previousNodeEnv = process.env.NODE_ENV; const previousHost = process.env.HOST; const previousPort = process.env.PORT; - const previousAllowOverride = process.env.ALLOW_OVERRIDE; - const previousTenantWhitelist = process.env.TENANT_WHITELIST; - const previousAllowUntrustedSsr = process.env.ALLOW_UNTRUSTED_SSR; beforeAll(async () => { assetServer = await startStaticServer({}); @@ -129,9 +136,6 @@ describe("tenant host nested integration", () => { process.env.NODE_ENV = "production"; process.env.HOST = "127.0.0.1"; process.env.PORT = String(port); - process.env.ALLOW_OVERRIDE = "ui,plugins.*"; - process.env.TENANT_WHITELIST = "chicago.alice.linktree.near"; - process.env.ALLOW_UNTRUSTED_SSR = "false"; process.argv.push("--proxy"); const config = createBaseConfig(); @@ -162,9 +166,6 @@ describe("tenant host nested integration", () => { process.env.NODE_ENV = previousNodeEnv; process.env.HOST = previousHost; process.env.PORT = previousPort; - process.env.ALLOW_OVERRIDE = previousAllowOverride; - process.env.TENANT_WHITELIST = previousTenantWhitelist; - process.env.ALLOW_UNTRUSTED_SSR = previousAllowUntrustedSsr; const proxyIdx = process.argv.indexOf("--proxy"); if (proxyIdx !== -1) { diff --git a/host/tests/integration/tenant-runtime.test.ts b/host/tests/integration/tenant-runtime.test.ts index 9927d32e..1cbf21ec 100644 --- a/host/tests/integration/tenant-runtime.test.ts +++ b/host/tests/integration/tenant-runtime.test.ts @@ -7,19 +7,6 @@ const verifySriForUrlMock = vi.fn(); vi.mock("everything-dev/config", async () => { return { - parseRuntimeOverrideTargets: (value?: string | null) => - value - ? [ - ...new Set( - value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean), - ), - ] - : [], - isRuntimeOverrideAllowed: (targets: string[], target: string) => - targets.includes(target) || (target.startsWith("plugins.") && targets.includes("plugins.*")), loadRemoteConfig: loadRemoteConfigMock, buildRuntimeConfig: buildRuntimeConfigMock, }; @@ -32,6 +19,7 @@ vi.mock("everything-dev/integrity", () => ({ const { clearTenantRuntimeCaches, resolveRequestRuntime } = await import( "../../src/services/tenant-runtime" ); +import type { BindingResolver } from "../../src/services/binding-resolver"; function createDeferred() { let resolve!: (value: T | PromiseLike) => void; @@ -41,6 +29,34 @@ function createDeferred() { return { promise, resolve }; } +function createMockBindingResolver( + ...hostnames: Array<{ + hostname: string; + allowUiOverrides?: boolean; + allowBackendOverrides?: boolean; + allowSsr?: boolean; + status?: "active" | "pending" | "suspended" | "pending_deletion"; + }> +): BindingResolver { + const map = new Map( + hostnames.map((entry) => [ + entry.hostname, + { + hostname: entry.hostname, + accountId: entry.hostname.replace(/\.com$/, ".near"), + allowUiOverrides: entry.allowUiOverrides ?? true, + allowBackendOverrides: entry.allowBackendOverrides ?? false, + allowSsr: entry.allowSsr ?? false, + status: entry.status ?? "active", + }, + ]), + ); + return { + resolve: async (hostname: string) => map.get(hostname) ?? null, + clear: () => {}, + }; +} + function createBaseRuntimeConfig(): RuntimeConfig { return { env: "production", @@ -96,35 +112,27 @@ function createBaseRuntimeConfig(): RuntimeConfig { } describe("resolveRequestRuntime", () => { - const envSnapshot = { ...process.env }; - beforeEach(() => { vi.clearAllMocks(); clearTenantRuntimeCaches(); verifySriForUrlMock.mockResolvedValue(undefined); - process.env = { - ...envSnapshot, - ALLOW_OVERRIDE: "ui", - TENANT_WHITELIST: "alice.linktree.near", - ALLOW_UNTRUSTED_SSR: "false", - }; - }); - - afterEach(() => { - process.env = { ...envSnapshot }; }); it("returns the base runtime on the bare domain", async () => { const baseConfig = createBaseRuntimeConfig(); - const result = await resolveRequestRuntime(baseConfig, new Request("https://linktree.com/")); + const result = await resolveRequestRuntime( + baseConfig, + new Request("https://linktree.com/"), + { bindingResolver: createMockBindingResolver() }, + ); expect(result.config).toBe(baseConfig); expect(result.tenantAccountId).toBeNull(); expect(loadRemoteConfigMock).not.toHaveBeenCalled(); }); - it("derives tenant accounts relative to the active runtime account", async () => { + it("resolves the tenant account for a hostname via the binding resolver", async () => { loadRemoteConfigMock.mockResolvedValue({ source: "bos://alice.linktree.near/linktree.com", rawConfig: { @@ -155,6 +163,12 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( createBaseRuntimeConfig(), new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + }), + }, ); expect(result.tenantAccountId).toBe("alice.linktree.near"); @@ -164,7 +178,7 @@ describe("resolveRequestRuntime", () => { ); }); - it("supports nested tenant labels within the active runtime namespace", async () => { + it("resolves nested tenant hostnames via the binding resolver", async () => { loadRemoteConfigMock.mockResolvedValue({ source: "bos://chicago.alice.linktree.near/linktree.com", rawConfig: { @@ -198,6 +212,12 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( createBaseRuntimeConfig(), new Request("https://chicago.alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "chicago.alice.linktree.com", + allowUiOverrides: true, + }), + }, ); expect(result.tenantAccountId).toBe("chicago.alice.linktree.near"); @@ -230,61 +250,37 @@ describe("resolveRequestRuntime", () => { buildRuntimeConfigMock.mockResolvedValue(createBaseRuntimeConfig()); await expect( - resolveRequestRuntime(createBaseRuntimeConfig(), new Request("https://alice.linktree.com/")), + resolveRequestRuntime(createBaseRuntimeConfig(), new Request("https://alice.linktree.com/"), { + bindingResolver: createMockBindingResolver({ hostname: "alice.linktree.com" }), + }), ).rejects.toThrow("must extend bos://linktree.near/linktree.com"); }); - it("rejects a suspended tenant with 503 based on published config status", async () => { - loadRemoteConfigMock.mockResolvedValue({ - source: "bos://alice.linktree.near/linktree.com", - rawConfig: { - extends: "bos://linktree.near/linktree.com", - status: "suspended", - }, - config: { - account: "alice.linktree.near", - app: { - host: { development: "local:host", production: "https://host.example.com" }, - ui: { name: "ui", production: "https://cdn.example.com/alice-ui" }, - api: { name: "api", production: "https://api.example.com" }, - }, - }, - extendsChain: ["bos://alice.linktree.near/linktree.com", "bos://linktree.near/linktree.com"], - }); - - buildRuntimeConfigMock.mockResolvedValue(createBaseRuntimeConfig()); - + it("rejects a suspended tenant with 503 based on the binding status", async () => { await expect( - resolveRequestRuntime(createBaseRuntimeConfig(), new Request("https://alice.linktree.com/")), + resolveRequestRuntime(createBaseRuntimeConfig(), new Request("https://alice.linktree.com/"), { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + status: "suspended", + }), + }), ).rejects.toMatchObject({ status: 503, message: "Tenant is suspended" }); + expect(loadRemoteConfigMock).not.toHaveBeenCalled(); }); - it("rejects a pending_deletion tenant with 410 based on published config status", async () => { - loadRemoteConfigMock.mockResolvedValue({ - source: "bos://alice.linktree.near/linktree.com", - rawConfig: { - extends: "bos://linktree.near/linktree.com", - status: "pending_deletion", - }, - config: { - account: "alice.linktree.near", - app: { - host: { development: "local:host", production: "https://host.example.com" }, - ui: { name: "ui", production: "https://cdn.example.com/alice-ui" }, - api: { name: "api", production: "https://api.example.com" }, - }, - }, - extendsChain: ["bos://alice.linktree.near/linktree.com", "bos://linktree.near/linktree.com"], - }); - - buildRuntimeConfigMock.mockResolvedValue(createBaseRuntimeConfig()); - + it("rejects a pending_deletion tenant with 410 based on the binding status", async () => { await expect( - resolveRequestRuntime(createBaseRuntimeConfig(), new Request("https://alice.linktree.com/")), + resolveRequestRuntime(createBaseRuntimeConfig(), new Request("https://alice.linktree.com/"), { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + status: "pending_deletion", + }), + }), ).rejects.toMatchObject({ status: 410, message: "Tenant has been deleted" }); + expect(loadRemoteConfigMock).not.toHaveBeenCalled(); }); - it("applies a tenant UI override and allows SSR for whitelisted tenants", async () => { + it("applies a tenant UI override and allows SSR when the binding enables both", async () => { const baseConfig = createBaseRuntimeConfig(); loadRemoteConfigMock.mockResolvedValue({ @@ -325,6 +321,13 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( baseConfig, new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, ); expect(result.tenantAccountId).toBe("alice.linktree.near"); @@ -338,7 +341,7 @@ describe("resolveRequestRuntime", () => { ); }); - it("disables SSR for non-whitelisted tenants when untrusted SSR is off", async () => { + it("disables SSR when the binding does not allow it", async () => { const baseConfig = createBaseRuntimeConfig(); loadRemoteConfigMock.mockResolvedValue({ @@ -373,6 +376,13 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( baseConfig, new Request("https://bob.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "bob.linktree.com", + allowUiOverrides: true, + allowSsr: false, + }), + }, ); expect(result.ssrAllowed).toBe(false); @@ -380,7 +390,7 @@ describe("resolveRequestRuntime", () => { expect(result.config.ui.ssrIntegrity).toBeUndefined(); }); - it("disables SSR for whitelisted tenants when ssrIntegrity is missing", async () => { + it("disables SSR for SSR-enabled bindings when ssrIntegrity is missing", async () => { const baseConfig = createBaseRuntimeConfig(); loadRemoteConfigMock.mockResolvedValue({ @@ -419,6 +429,13 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( baseConfig, new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, ); expect(result.ssrAllowed).toBe(false); @@ -426,7 +443,7 @@ describe("resolveRequestRuntime", () => { expect(result.config.ui.ssrIntegrity).toBeUndefined(); }); - it("disables SSR for whitelisted tenants when ssrUrl is missing", async () => { + it("disables SSR for SSR-enabled bindings when ssrUrl is missing", async () => { const baseConfig = createBaseRuntimeConfig(); loadRemoteConfigMock.mockResolvedValue({ @@ -460,6 +477,13 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( baseConfig, new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, ); expect(result.ssrAllowed).toBe(false); @@ -467,7 +491,7 @@ describe("resolveRequestRuntime", () => { expect(result.config.ui.ssrIntegrity).toBeUndefined(); }); - it("allows SSR for whitelisted tenants when both ssrUrl and ssrIntegrity are present", async () => { + it("allows SSR when the binding enables SSR and both ssrUrl and ssrIntegrity are present", async () => { const baseConfig = createBaseRuntimeConfig(); loadRemoteConfigMock.mockResolvedValue({ @@ -508,6 +532,13 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( baseConfig, new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, ); expect(result.ssrAllowed).toBe(true); @@ -515,9 +546,8 @@ describe("resolveRequestRuntime", () => { expect(result.config.ui.ssrIntegrity).toBe("sha384-alice-ssr"); }); - it("applies existing plugin UI overrides when plugins are allowed", async () => { + it("applies existing plugin UI overrides when backend overrides are allowed", async () => { const baseConfig = createBaseRuntimeConfig(); - process.env.ALLOW_OVERRIDE = "ui,plugins.*"; loadRemoteConfigMock.mockResolvedValue({ source: "bos://alice.linktree.near/linktree.com", @@ -591,6 +621,14 @@ describe("resolveRequestRuntime", () => { const result = await resolveRequestRuntime( baseConfig, new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + allowBackendOverrides: true, + }), + }, ); expect(result.config.plugins?.apps.ui?.url).toBe("https://plugins.example.com/alice-apps-ui"); @@ -639,7 +677,17 @@ describe("resolveRequestRuntime", () => { }, }); - await resolveRequestRuntime(baseConfig, new Request("https://alice.linktree.com/")); + await resolveRequestRuntime( + baseConfig, + new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, + ); const refresh = createDeferred(); verifySriForUrlMock.mockImplementationOnce(() => refresh.promise); @@ -648,6 +696,11 @@ describe("resolveRequestRuntime", () => { await expect( resolveRequestRuntime(baseConfig, new Request("https://alice.linktree.com/asset.js"), { verification: "stale-while-revalidate", + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), }), ).resolves.toMatchObject({ tenantAccountId: "alice.linktree.near" }); expect(verifySriForUrlMock).toHaveBeenCalledTimes(2); @@ -655,6 +708,11 @@ describe("resolveRequestRuntime", () => { await expect( resolveRequestRuntime(baseConfig, new Request("https://alice.linktree.com/asset-2.js"), { verification: "stale-while-revalidate", + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), }), ).resolves.toMatchObject({ tenantAccountId: "alice.linktree.near" }); expect(verifySriForUrlMock).toHaveBeenCalledTimes(2); @@ -704,7 +762,17 @@ describe("resolveRequestRuntime", () => { }, }); - await resolveRequestRuntime(baseConfig, new Request("https://alice.linktree.com/")); + await resolveRequestRuntime( + baseConfig, + new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, + ); const refresh = createDeferred(); verifySriForUrlMock.mockImplementationOnce(() => refresh.promise); @@ -716,6 +784,11 @@ describe("resolveRequestRuntime", () => { new Request("https://alice.linktree.com/"), { verification: "blocking", + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), }, ).then(() => { settled = true; @@ -732,50 +805,109 @@ describe("resolveRequestRuntime", () => { } }); - it("recomputes the tenant whitelist when the env value changes", async () => { + it("gates SSR per tenant based on the binding allowSsr flag", async () => { const baseConfig = createBaseRuntimeConfig(); loadRemoteConfigMock.mockResolvedValue({ - source: "bos://bob.linktree.near/linktree.com", + source: "bos://alice.linktree.near/linktree.com", rawConfig: { extends: "bos://linktree.near/linktree.com", }, config: { - account: "bob.linktree.near", + account: "alice.linktree.near", app: { host: { development: "local:host", production: "https://host.example.com" }, - ui: { name: "ui", production: "https://cdn.example.com/bob-ui" }, + ui: { name: "ui", production: "https://cdn.example.com/alice-ui" }, api: { name: "api", production: "https://api.example.com" }, }, }, - extendsChain: ["bos://bob.linktree.near/linktree.com", "bos://linktree.near/linktree.com"], + extendsChain: ["bos://alice.linktree.near/linktree.com", "bos://linktree.near/linktree.com"], }); buildRuntimeConfigMock.mockResolvedValue({ ...baseConfig, - account: "bob.linktree.near", + account: "alice.linktree.near", ui: { ...baseConfig.ui, - url: "https://cdn.example.com/bob-ui", - entry: "https://cdn.example.com/bob-ui/mf-manifest.json", - integrity: "sha384-bob", - ssrUrl: "https://cdn.example.com/bob-ui-ssr", - ssrIntegrity: "sha384-bob-ssr", + url: "https://cdn.example.com/alice-ui", + entry: "https://cdn.example.com/alice-ui/mf-manifest.json", + integrity: "sha384-alice", + ssrUrl: "https://cdn.example.com/alice-ui-ssr", + ssrIntegrity: "sha384-alice-ssr", }, }); const blocked = await resolveRequestRuntime( baseConfig, - new Request("https://bob.linktree.com/"), + new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: false, + }), + }, ); expect(blocked.ssrAllowed).toBe(false); - - process.env.TENANT_WHITELIST = "bob.linktree.near"; + expect(blocked.config.ui.ssrUrl).toBeUndefined(); const allowed = await resolveRequestRuntime( baseConfig, - new Request("https://bob.linktree.com/"), + new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }, ); expect(allowed.ssrAllowed).toBe(true); + expect(allowed.config.ui.ssrUrl).toBe("https://cdn.example.com/alice-ui-ssr"); + }); + + it("does not apply tenant UI overrides when the binding disallows them", async () => { + const baseConfig = createBaseRuntimeConfig(); + + loadRemoteConfigMock.mockResolvedValue({ + source: "bos://alice.linktree.near/linktree.com", + rawConfig: { + extends: "bos://linktree.near/linktree.com", + }, + config: { + account: "alice.linktree.near", + app: { + host: { development: "local:host", production: "https://host.example.com" }, + ui: { name: "ui", production: "https://cdn.example.com/alice-ui" }, + api: { name: "api", production: "https://api.example.com" }, + }, + }, + extendsChain: ["bos://alice.linktree.near/linktree.com", "bos://linktree.near/linktree.com"], + }); + + buildRuntimeConfigMock.mockResolvedValue({ + ...baseConfig, + account: "alice.linktree.near", + ui: { + ...baseConfig.ui, + url: "https://cdn.example.com/alice-ui", + entry: "https://cdn.example.com/alice-ui/mf-manifest.json", + integrity: "sha384-alice", + }, + }); + + const result = await resolveRequestRuntime( + baseConfig, + new Request("https://alice.linktree.com/"), + { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: false, + }), + }, + ); + + expect(result.config.ui.url).toBe(baseConfig.ui.url); + expect(verifySriForUrlMock).not.toHaveBeenCalled(); }); }); diff --git a/tests/regression/http/tenant_bindings_test.go b/tests/regression/http/tenant_bindings_test.go new file mode 100644 index 00000000..7ca4c801 --- /dev/null +++ b/tests/regression/http/tenant_bindings_test.go @@ -0,0 +1,184 @@ +package regression + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + "everything.dev/regression/http/internal/regtest" +) + +func TestTenantBindingsPublicEndpoint(t *testing.T) { + client := regtest.NewCookieClient() + + t.Run("bindings_return_200_without_auth", func(t *testing.T) { + status, _, body := regtest.GetRaw(t, client, baseURL+"/api/tenants/bindings") + regtest.MustStatus(t, status, 200, body) + }) + + var bindings []struct { + Hostname string `json:"hostname"` + AccountID string `json:"accountId"` + AllowUiOverrides bool `json:"allowUiOverrides"` + AllowBackendOverrides bool `json:"allowBackendOverrides"` + AllowSsr bool `json:"allowSsr"` + Status string `json:"status"` + } + t.Run("bindings_decode_as_array", func(t *testing.T) { + status, _, body := regtest.GetRaw(t, client, baseURL+"/api/tenants/bindings") + regtest.MustStatus(t, status, 200, body) + if err := json.Unmarshal([]byte(body), &bindings); err != nil { + t.Fatalf("decoding bindings response: %v\nBody: %s", err, body) + } + }) + + t.Run("seeded_tenant_binding_present", func(t *testing.T) { + var seeded *struct { + Hostname string `json:"hostname"` + AccountID string `json:"accountId"` + AllowUiOverrides bool `json:"allowUiOverrides"` + AllowBackendOverrides bool `json:"allowBackendOverrides"` + AllowSsr bool `json:"allowSsr"` + Status string `json:"status"` + } + for i := range bindings { + if strings.HasPrefix(bindings[i].Hostname, "regression-tenant-") { + seeded = &bindings[i] + break + } + } + if seeded == nil { + t.Fatal("expected seeded tenant binding to be present") + } + if seeded.Status != "active" { + t.Fatalf("expected seeded tenant status 'active', got %q", seeded.Status) + } + if seeded.AccountID != fmt.Sprintf("%s.testnet", seeded.Hostname) { + t.Fatalf("expected account id derived from hostname, got %q", seeded.AccountID) + } + + // The seeded tenant uses the default permission columns. + if !seeded.AllowUiOverrides { + t.Fatal("expected allowUiOverrides to default to true") + } + if seeded.AllowBackendOverrides { + t.Fatal("expected allowBackendOverrides to default to false") + } + if seeded.AllowSsr { + t.Fatal("expected allowSsr to default to false") + } + }) +} + +func TestTenantBindingsReflectAllowFlags(t *testing.T) { + client := regtest.NewCookieClient() + + t.Run("sign_in", func(t *testing.T) { + status, _, body := regtest.PostEmpty(t, client, baseURL+"/api/auth/sign-in/anonymous") + regtest.MustStatus(t, status, 200, body) + }) + + orgName := fmt.Sprintf("regression-flags-org-%d", os.Getpid()) + t.Run("create_org", func(t *testing.T) { + status, _, body := regtest.PostJSON(t, client, baseURL+"/api/auth/organization/create", map[string]string{ + "name": orgName, + "slug": orgName, + }, map[string]string{ + "Origin": "http://localhost:4100", + }) + regtest.MustStatus(t, status, 200, body) + var result struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(body), &result); err != nil { + t.Fatalf("decoding org response: %v\nBody: %s", err, body) + } + if result.ID == "" { + t.Fatal("expected non-empty org id") + } + + status, _, body = regtest.PostJSON(t, client, baseURL+"/api/auth/organization/set-active", map[string]string{ + "organizationId": result.ID, + }, map[string]string{ + "Origin": "http://localhost:4100", + }) + regtest.MustStatus(t, status, 200, body) + }) + + subdomain := fmt.Sprintf("regression-flags-%d", os.Getpid()) + t.Run("create_tenant_with_explicit_flags", func(t *testing.T) { + status, _, body := regtest.PostJSON(t, client, baseURL+"/api/tenants", map[string]any{ + "subdomain": subdomain, + "name": "Flags Tenant", + "accountId": fmt.Sprintf("%s.testnet", subdomain), + "allowUiOverrides": false, + "allowBackendOverrides": true, + "allowSsr": true, + }, nil) + regtest.MustStatus(t, status, 200, body) + + var result struct { + ID string `json:"id"` + AllowUiOverrides bool `json:"allowUiOverrides"` + AllowBackendOverrides bool `json:"allowBackendOverrides"` + AllowSsr bool `json:"allowSsr"` + } + if err := json.Unmarshal([]byte(body), &result); err != nil { + t.Fatalf("decoding tenant response: %v\nBody: %s", err, body) + } + if result.ID == "" { + t.Fatal("expected non-empty tenant id") + } + if result.AllowUiOverrides { + t.Fatal("expected allowUiOverrides to be false") + } + if !result.AllowBackendOverrides { + t.Fatal("expected allowBackendOverrides to be true") + } + if !result.AllowSsr { + t.Fatal("expected allowSsr to be true") + } + }) + + t.Run("bindings_reflect_flags", func(t *testing.T) { + status, _, body := regtest.GetRaw(t, client, baseURL+"/api/tenants/bindings") + regtest.MustStatus(t, status, 200, body) + + var bindings []struct { + Hostname string `json:"hostname"` + AllowUiOverrides bool `json:"allowUiOverrides"` + AllowBackendOverrides bool `json:"allowBackendOverrides"` + AllowSsr bool `json:"allowSsr"` + } + if err := json.Unmarshal([]byte(body), &bindings); err != nil { + t.Fatalf("decoding bindings response: %v\nBody: %s", err, body) + } + + var found *struct { + Hostname string `json:"hostname"` + AllowUiOverrides bool `json:"allowUiOverrides"` + AllowBackendOverrides bool `json:"allowBackendOverrides"` + AllowSsr bool `json:"allowSsr"` + } + for i := range bindings { + if bindings[i].Hostname == subdomain { + found = &bindings[i] + break + } + } + if found == nil { + t.Fatal("expected created tenant to appear in bindings") + } + if found.AllowUiOverrides { + t.Fatal("expected binding allowUiOverrides to be false") + } + if !found.AllowBackendOverrides { + t.Fatal("expected binding allowBackendOverrides to be true") + } + if !found.AllowSsr { + t.Fatal("expected binding allowSsr to be true") + } + }) +} diff --git a/ui/src/routes/_layout.tsx b/ui/src/routes/_layout.tsx index db4a8ec0..c0a44b0a 100644 --- a/ui/src/routes/_layout.tsx +++ b/ui/src/routes/_layout.tsx @@ -1,97 +1,14 @@ -import { useQuery } from "@tanstack/react-query"; -import { createFileRoute, Link, Outlet, useRouterState } from "@tanstack/react-router"; -import { ClipboardList, Compass, Globe, Home, Menu, Shield, X } from "lucide-react"; +import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router"; +import { X } from "lucide-react"; import { useEffect, useState } from "react"; -import { - getAccount, - getActiveRuntime, - getAppName, - sessionQueryOptions, - useAuthClient, -} from "@/app"; -import builtOn from "@/assets/built_on.png"; -import builtOnRev from "@/assets/built_on_rev.png"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { UserNav } from "@/components/user-nav"; -import { cn } from "@/lib/utils"; - -type SidebarRole = "anon" | "member" | "admin"; - -interface SidebarItem { - icon: React.ComponentType<{ className?: string }>; - label: string; - to: string; - roleRequired: SidebarRole; -} - -function filterSidebarByRole(items: SidebarItem[], userRole: SidebarRole): SidebarItem[] { - return items.filter((item) => { - if (item.roleRequired === "anon") return true; - if (item.roleRequired === "member" && userRole !== "anon") return true; - if (item.roleRequired === "admin" && userRole === "admin") return true; - return false; - }); -} - -function getUserRole(isAuthenticated: boolean, isAdmin: boolean): SidebarRole { - if (isAdmin) return "admin"; - if (isAuthenticated) return "member"; - return "anon"; -} +import { TooltipProvider } from "@/components/ui/tooltip"; export const Route = createFileRoute("/_layout")({ - beforeLoad: async ({ context }) => { - const { queryClient, authClient, apiClient } = context; - const session = await queryClient.ensureQueryData( - sessionQueryOptions(authClient, context.session), - ); - - const accountId = getAccount(context.runtimeConfig); - const tenant = await apiClient.resolveTenant({ accountId }); - - return { - runtimeConfig: context.runtimeConfig, - session, - tenant, - }; - }, component: Layout, }); function Layout() { - const pathname = useRouterState({ select: (s) => s.location.pathname }); const isNavigating = useRouterState({ select: (s) => s.status === "pending" }); - const { runtimeConfig, session, tenant } = Route.useRouteContext(); - const appName = getAppName(runtimeConfig); - const runtime = getActiveRuntime(runtimeConfig); - const account = getAccount(runtimeConfig); - const auth = useAuthClient(); - const isAuthenticated = !!session?.user; - const userRole = getUserRole(isAuthenticated, session?.user?.role === "admin"); - - const { data: liveSession } = useQuery({ - ...sessionQueryOptions(auth, session), - initialData: session, - }); - const activeOrgId = liveSession?.session?.activeOrganizationId ?? null; - const liveIsTenantMember = !!tenant && !!activeOrgId && activeOrgId === tenant.orgId; - - const hideSidebar = pathname === "/login"; - - const sidebarItems: SidebarItem[] = [ - { icon: Home, label: "home", to: "/home", roleRequired: "anon" }, - { icon: Globe, label: "apps", to: "/apps", roleRequired: "anon" }, - ...(liveIsTenantMember - ? ([{ icon: Shield, label: "admin", to: "/admin", roleRequired: "member" }] as SidebarItem[]) - : []), - ]; - const visibleItems = filterSidebarByRole(sidebarItems, userRole); - - const isActive = (item: SidebarItem) => { - return pathname === item.to || (item.to !== "/" && pathname.startsWith(`${item.to}/`)); - }; const [betaBannerDismissed, setBetaBannerDismissed] = useState(() => { if (typeof window === "undefined") return false; @@ -134,297 +51,8 @@ function Layout() { )} -
-
- {isAuthenticated ? ( -
- - - {appName} - - - - -
- {tenant && ( - <> - {tenant.name} - / - - )} - {runtime?.accountId ?? account} - / - - {pathname === "/" ? "home" : pathname.slice(1).split("/").join(" / ")} - -
-
- ) : ( - - - {appName} - - - - )} - -
- -
-
-
- {hideSidebar && } - -
- - -
-
- -
-
-
- -
- -
- - {!isAuthenticated && !hideSidebar && ( -
- -
- )} - -
- -
+ ); } - -function MinimalHeader() { - return ( -
- -
- ); -} - -function NearBranding() { - return ( - - Built on NEAR - Built on NEAR - - ); -} - -function MobileTabBar({ - isAuthenticated, - visibleItems, - isActive, -}: { - isAuthenticated: boolean; - visibleItems: SidebarItem[]; - isActive: (item: SidebarItem) => boolean; -}) { - const [drawerOpen, setDrawerOpen] = useState(false); - const pathname = useRouterState({ select: (s) => s.location.pathname }); - const tabActive = (to: string) => (to === "/" ? pathname === "/" : pathname.startsWith(to)); - - return ( - - ); -} - -function TabItem({ - to, - icon: Icon, - label, - active, -}: { - to: string; - icon: React.ComponentType<{ className?: string }>; - label: string; - active: boolean; -}) { - return ( - - - {label} - - ); -} diff --git a/ui/src/routes/_layout/_authenticated.tsx b/ui/src/routes/_layout/_authenticated.tsx index f6c8d6de..869f23e3 100644 --- a/ui/src/routes/_layout/_authenticated.tsx +++ b/ui/src/routes/_layout/_authenticated.tsx @@ -1,5 +1,14 @@ -import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; -import { type SessionData, sessionQueryOptions } from "@/app"; +import { createFileRoute, Link, Outlet, redirect, useRouterState } from "@tanstack/react-router"; +import { ClipboardList, Compass, Globe, Home, Menu, MessageSquare, Shield } from "lucide-react"; +import { useState } from "react"; +import type { SessionData } from "@/app"; +import { getAccount, getActiveRuntime, getAppName, sessionQueryOptions } from "@/app"; +import { NearBranding } from "@/components/near-branding"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { UserNav } from "@/components/user-nav"; +import { cn } from "@/lib/utils"; interface AuthContext { isAuthenticated: boolean; @@ -11,6 +20,30 @@ interface AuthContext { isBanned: boolean; } +type SidebarRole = "anon" | "member" | "admin"; + +interface SidebarItem { + icon: React.ComponentType<{ className?: string }>; + label: string; + to: string; + roleRequired: SidebarRole; +} + +function filterSidebarByRole(items: SidebarItem[], userRole: SidebarRole): SidebarItem[] { + return items.filter((item) => { + if (item.roleRequired === "anon") return true; + if (item.roleRequired === "member" && userRole !== "anon") return true; + if (item.roleRequired === "admin" && userRole === "admin") return true; + return false; + }); +} + +function getUserRole(isAuthenticated: boolean, isAdmin: boolean): SidebarRole { + if (isAdmin) return "admin"; + if (isAuthenticated) return "member"; + return "anon"; +} + export const Route = createFileRoute("/_layout/_authenticated")({ beforeLoad: async ({ context, location }) => { const { queryClient, authClient } = context; @@ -53,9 +86,219 @@ export const Route = createFileRoute("/_layout/_authenticated")({ }); function AuthenticatedLayout() { + const pathname = useRouterState({ select: (s) => s.location.pathname }); + const { runtimeConfig, session } = Route.useRouteContext(); + const appName = getAppName(runtimeConfig); + const runtime = getActiveRuntime(runtimeConfig); + const account = getAccount(runtimeConfig); + const isAdmin = session?.user?.role === "admin"; + + const sidebarItems: SidebarItem[] = [ + { icon: Home, label: "home", to: "/home", roleRequired: "anon" }, + { icon: Shield, label: "admin", to: "/admin", roleRequired: "admin" }, + { icon: MessageSquare, label: "nostr", to: "/nostr", roleRequired: "admin" }, + ]; + const visibleItems = filterSidebarByRole(sidebarItems, getUserRole(true, isAdmin)); + + const isActive = (item: SidebarItem) => { + return pathname === item.to || (item.to !== "/" && pathname.startsWith(`${item.to}/`)); + }; + return ( -
- +
+ + +
+
+
+
+ + + {appName} + + + + +
+ {runtime?.accountId ?? account} + / + + {pathname === "/" ? "home" : pathname.slice(1).split("/").join(" / ")} + +
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+ +
); } + +function MobileTabBar({ + visibleItems, + isActive, +}: { + visibleItems: SidebarItem[]; + isActive: (item: SidebarItem) => boolean; +}) { + const [drawerOpen, setDrawerOpen] = useState(false); + const pathname = useRouterState({ select: (s) => s.location.pathname }); + const tabActive = (to: string) => (to === "/" ? pathname === "/" : pathname.startsWith(to)); + + return ( + + ); +} + +function TabItem({ + to, + icon: Icon, + label, + active, +}: { + to: string; + icon: React.ComponentType<{ className?: string }>; + label: string; + active: boolean; +}) { + return ( + + + {label} + + ); +} diff --git a/ui/src/routes/_layout/_authenticated/admin/index.tsx b/ui/src/routes/_layout/_authenticated/admin/index.tsx new file mode 100644 index 00000000..65c3c2d3 --- /dev/null +++ b/ui/src/routes/_layout/_authenticated/admin/index.tsx @@ -0,0 +1,104 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import { Building2, Settings, Users } from "lucide-react"; +import { getAccount } from "@/app"; +import { Button, Card } from "@/components"; +import { PageContainer } from "@/components/layout/page-container"; + +export const Route = createFileRoute("/_layout/_authenticated/admin/")({ + head: () => ({ + meta: [{ title: "Admin Dashboard | app" }], + }), + component: AdminDashboard, +}); + +function AdminDashboard() { + const { auth } = Route.useRouteContext(); + const account = getAccount(); + const user = auth?.user ?? null; + + return ( + +
+
+
+
+

+ Dashboard +

+

+ Signed in as {account} +

+
+
+
+ +
+ + + + +
+ +
+

Manage

+
+ +
+ +
+

Organizations

+

+ Manage organizations, members, roles, and invitations. +

+ +
+ + +
+ +
+

Settings

+

+ Update your profile, auth methods, and security preferences. +

+ +
+
+
+
+
+ ); +} + +function StatCard({ + label, + value, + mono, +}: { + label: string; + value: React.ReactNode; + mono?: boolean; +}) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} diff --git a/ui/src/routes/_layout/_authenticated/admin/system.tsx b/ui/src/routes/_layout/_authenticated/admin/system.tsx new file mode 100644 index 00000000..a98296af --- /dev/null +++ b/ui/src/routes/_layout/_authenticated/admin/system.tsx @@ -0,0 +1,73 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app"; +import { Card } from "@/components"; +import { PageContainer } from "@/components/layout/page-container"; +import { InfoRow } from "@/components/ui/info-row"; + +export const Route = createFileRoute("/_layout/_authenticated/admin/system")({ + loader: async ({ context }) => ({ + runtimeConfig: context.runtimeConfig, + }), + head: () => ({ + meta: [{ title: "Admin System | app" }], + }), + component: AdminSystem, +}); + +function AdminSystem() { + const { runtimeConfig } = Route.useLoaderData(); + const account = getAccount(runtimeConfig); + const appName = getAppName(runtimeConfig); + const repository = getRepository(runtimeConfig); + const runtime = getActiveRuntime(runtimeConfig); + + const env = runtimeConfig?.env; + const networkId = runtimeConfig?.networkId; + const hostUrl = runtimeConfig?.hostUrl; + const apiBase = runtimeConfig?.apiBase; + const rpcBase = runtimeConfig?.rpcBase; + const assetsUrl = runtimeConfig?.assetsUrl; + const runtimeBasePath = runtime?.runtimeBasePath; + + return ( + +
+
+
+

+ System +

+

+ Runtime configuration for this deployment. +

+
+
+ +
+ +

Runtime

+ + + + +
+ + +

Deployment

+ + + + +
+ + +

Endpoints

+ + + +
+
+
+
+ ); +} diff --git a/ui/src/routes/_layout/_public.tsx b/ui/src/routes/_layout/_public.tsx new file mode 100644 index 00000000..1fd9a871 --- /dev/null +++ b/ui/src/routes/_layout/_public.tsx @@ -0,0 +1,53 @@ +import { createFileRoute, Link, Outlet, useRouterState } from "@tanstack/react-router"; +import { getAppName } from "@/app"; +import { NearBranding } from "@/components/near-branding"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { UserNav } from "@/components/user-nav"; + +export const Route = createFileRoute("/_layout/_public")({ + component: PublicLayout, +}); + +function PublicLayout() { + const { runtimeConfig } = Route.useRouteContext(); + const appName = getAppName(runtimeConfig); + const pathname = useRouterState({ select: (s) => s.location.pathname }); + + return ( +
+
+
+ + + {appName} + + + + +
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ ); +} diff --git a/ui/src/routes/_layout/about.tsx b/ui/src/routes/_layout/_public/about.tsx similarity index 99% rename from ui/src/routes/_layout/about.tsx rename to ui/src/routes/_layout/_public/about.tsx index ea4d4b4e..b960d03e 100644 --- a/ui/src/routes/_layout/about.tsx +++ b/ui/src/routes/_layout/_public/about.tsx @@ -41,7 +41,7 @@ async function fetchRepositoryReadme(repositoryUrl: string): Promise { const repository = getRepository(context.runtimeConfig); const description = diff --git a/ui/src/routes/_layout/_public/index.tsx b/ui/src/routes/_layout/_public/index.tsx new file mode 100644 index 00000000..8cbb4f06 --- /dev/null +++ b/ui/src/routes/_layout/_public/index.tsx @@ -0,0 +1,111 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import { ArrowRight, Building2, FileCode2, Lock, Sparkles } from "lucide-react"; +import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app"; +import { Button, Card, PageContainer } from "@/components"; + +export const Route = createFileRoute("/_layout/_public/")({ + loader: async ({ context }) => ({ + runtimeConfig: context.runtimeConfig, + }), + head: () => ({ + meta: [ + { title: "Welcome | app" }, + { + name: "description", + content: "A modern starter app built with TanStack Router and Better Auth on NEAR.", + }, + ], + }), + component: LandingPage, +}); + +function LandingPage() { + const { runtimeConfig } = Route.useLoaderData(); + const appName = getAppName(runtimeConfig); + const account = getAccount(runtimeConfig); + const runtime = getActiveRuntime(runtimeConfig); + const repository = getRepository(runtimeConfig); + + const accountId = runtime?.accountId ?? account; + + return ( + +
+
+
+ + {accountId} +
+ +
+

+ {appName} +

+

+ A production-ready starter built on TanStack Router, Better Auth, and Effect — with + organization management, a guarded dashboard, and a fully typed API. +

+
+ +
+ + +
+
+ +
+ +
+ +
+

Secure authentication

+

+ NEAR wallet sign-in, email, and passkeys via Better Auth — with an authenticated + layout guard and session-aware routing. +

+
+ + +
+ +
+

Organizations

+

+ Create and manage organizations with members, roles, invitations, and API keys — all + backed by typed oRPC endpoints. +

+
+ + +
+ +
+

Typed end to end

+

+ One contract drives the API and the client — schemas, validation, and types stay in + sync across the whole stack. +

+
+
+ + {repository && ( +
+

Fork the template and make it yours.

+ +
+ )} +
+
+ ); +} diff --git a/ui/src/routes/_layout/login.tsx b/ui/src/routes/_layout/_public/login.tsx similarity index 86% rename from ui/src/routes/_layout/login.tsx rename to ui/src/routes/_layout/_public/login.tsx index 96284716..fdc771d9 100644 --- a/ui/src/routes/_layout/login.tsx +++ b/ui/src/routes/_layout/_public/login.tsx @@ -3,18 +3,15 @@ import { createFileRoute, Navigate, redirect, useNavigate } from "@tanstack/reac import { useEffect, useState } from "react"; import { toast } from "sonner"; import { getAppName, sessionQueryOptions, useAuthClient } from "@/app"; -import builtOn from "@/assets/built_on.png"; -import builtOnRev from "@/assets/built_on_rev.png"; import { BrandElement } from "@/components/brand-element"; import { Button } from "@/components/ui/button"; -import { NetworkToggle } from "@/components/ui/network-toggle"; import { UnderConstruction } from "@/components/under-construction"; type SearchParams = { redirect?: string; }; -export const Route = createFileRoute("/_layout/login")({ +export const Route = createFileRoute("/_layout/_public/login")({ ssr: false, validateSearch: (search: Record): SearchParams => ({ redirect: typeof search.redirect === "string" ? search.redirect : undefined, @@ -120,8 +117,7 @@ function LoginPage() { const isPending = nearPending || anonPending; return ( -
- +
@@ -142,7 +138,25 @@ function LoginPage() {
- -
); } diff --git a/ui/src/routes/_layout/index.tsx b/ui/src/routes/_layout/index.tsx deleted file mode 100644 index f998f7f7..00000000 --- a/ui/src/routes/_layout/index.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { createFileRoute, Link, redirect } from "@tanstack/react-router"; -import { Building2, Plus, Shield } from "lucide-react"; -import { sessionQueryOptions, useApiClient, useAuthClient } from "@/app"; -import { Button, Card } from "@/components"; -import { PageContainer } from "@/components/layout/page-container"; - -export const Route = createFileRoute("/_layout/")({ - beforeLoad: async ({ context }) => { - const { authClient } = context; - const session = await context.queryClient.ensureQueryData( - sessionQueryOptions(authClient, context.session), - ); - if (!session?.user) { - throw redirect({ to: "/login" }); - } - }, - component: TenantListPage, -}); - -function TenantListPage() { - const apiClient = useApiClient(); - const auth = useAuthClient(); - const { data: session } = useQuery(sessionQueryOptions(auth, undefined)); - - const { data: tenants = [], isLoading } = useQuery({ - queryKey: ["tenants"], - queryFn: () => apiClient.listTenants(), - staleTime: 30_000, - }); - - return ( - -
-
-
- - Tenants -
-
-
-

- {session?.user?.name || session?.user?.email || "Your"} Tenants -

-
- -
-
- - {isLoading ? ( -
- {[1, 2, 3].map((n) => ( - -
-
-
- - ))} -
- ) : tenants.length === 0 ? ( - - -

No tenants yet.

-

- Create a tenant to deploy your own app with custom UI and API. -

- -
- ) : ( -
- {tenants.map((tenant) => ( - -
-
{tenant.name}
-
- {tenant.subdomain} -
-
-
- - -
-
- -
-
- ))} -
- )} -
- - ); -} - -function TenantMeta({ label, value, mono }: { label: string; value: string; mono?: boolean }) { - return ( -
- {label} - - {value} - -
- ); -} diff --git a/ui/src/routes/_layout/skill.tsx b/ui/src/routes/_layout/skill.tsx deleted file mode 100644 index db047a71..00000000 --- a/ui/src/routes/_layout/skill.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { Check, Copy, ExternalLink, FileText } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; -import { getAccount, getActiveRuntime, getAppName } from "@/app"; -import { PageContainer } from "@/components/layout/page-container"; -import { Button } from "@/components/ui/button"; -import { Markdown } from "@/components/ui/markdown"; - -const INTENT_REGISTRY_URL = "https://tanstack.com/intent/registry/everything-dev"; - -export const Route = createFileRoute("/_layout/skill")({ - loader: async ({ context }) => { - const runtimeConfig = context.runtimeConfig; - - const skill = await fetch("/skill.md") - .then(async (response) => { - if (!response.ok) { - throw new Error(`Failed to load skill: ${response.status}`); - } - - return response.text(); - }) - .catch(() => null); - - return { - runtimeConfig, - skill, - intentRegistryUrl: INTENT_REGISTRY_URL, - }; - }, - head: () => ({ - meta: [ - { title: "Skill | app" }, - { - name: "description", - content: "Agent-oriented instructions for running, editing, and publishing this runtime.", - }, - ], - }), - component: SkillPage, -}); - -function SkillPage() { - const { skill, runtimeConfig, intentRegistryUrl } = Route.useLoaderData(); - const runtime = getActiveRuntime(runtimeConfig); - const account = getAccount(runtimeConfig); - const appName = getAppName(runtimeConfig); - const [copied, setCopied] = useState(false); - - const accountId = runtime?.accountId ?? account; - - const handleCopy = async () => { - if (!skill) { - toast.error("Skill prompt unavailable"); - return; - } - - await navigator.clipboard.writeText(skill); - setCopied(true); - toast.success("Skill prompt copied"); - setTimeout(() => setCopied(false), 2000); - }; - - return ( - -
-
-
-
-
- -
-
-
- {accountId} - / - {appName} -
-

- Agent-ready prompt for TanStack Intent, local development, UI changes, and publish - flow. -

-
-
- -
- - - -
-
- -
- Best entry points: `npx @tanstack/intent@latest load everything-dev`, `/skill.md`, and - the registry page above. -
-
- - {skill ? ( -
- -
- ) : ( -
- -

Skill prompt unavailable.

-
- )} -
-
- ); -} From 4f8f6e907d497bfecbef26649fc00554288635b5 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 13 Aug 2026 14:20:22 -0500 Subject: [PATCH 02/24] upgrade bindings --- AGENTS.md | 2 +- LLM.txt | 2 +- README.md | 2 +- host/README.md | 16 +++++----------- host/src/services/tenant-runtime.ts | 3 ++- host/tests/integration/tenant-runtime.test.ts | 2 +- .../skills/plugin-client/SKILL.md | 2 +- .../skills/extends-config/SKILL.md | 13 +++---------- .../skills/init-upgrade/SKILL.md | 10 ++-------- .../everything-dev/skills/super-app/SKILL.md | 19 ++++++------------- 10 files changed, 23 insertions(+), 48 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8453be7f..ddca75f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,7 +168,7 @@ Current fixed-core host rules: - the shared host still boots once from one base runtime snapshot - child runtime config must extend the active BOS runtime - supported request-scoped overrides are `ui` and existing `plugins..ui` -- tenant SSR is gated by `TENANT_WHITELIST` and `ALLOW_UNTRUSTED_SSR` +- tenant SSR is gated per-tenant by the `allowSsr` column on the tenant record; the host's BindingResolver reads permissions from the API's `GET /tenants/bindings` endpoint (cached for 30s) - nested label routing and account-relative tenant derivation are the intended architecture direction, but not the complete resolver behavior today For full per-request host/plugin/auth/api swapping, start from `plans/runtime-config-hot-swap.md`. diff --git a/LLM.txt b/LLM.txt index 3b5f2820..5eb2fff3 100644 --- a/LLM.txt +++ b/LLM.txt @@ -48,7 +48,7 @@ Practical consequence: - Path-based runtime metadata works today. - Shared-host tenant UI mode now works today: the host can resolve a tenant config per request, enforce that it extends the base BOS runtime, and apply request-scoped UI overrides while keeping auth/API/plugin routers fixed. - Supported tenant overrides today are `app.ui`, existing `plugins..ui`, and existing `plugins..sidebar`. -- Tenant SSR is gated by `TENANT_WHITELIST` and `ALLOW_UNTRUSTED_SSR`. +- Tenant SSR is gated per-tenant by the `allow_ssr` column on the tenant record; the host's BindingResolver reads these permissions from the API's `GET /tenants/bindings` endpoint (cached for 30s). - True wildcard-domain full runtime swapping for host/plugin/auth/api still needs dynamic scoped app rebuilding in the host. - The active design doc for that work is `plans/runtime-config-hot-swap.md`. diff --git a/README.md b/README.md index 25b62e71..cf47c93f 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ What works today: - `app.ui` - existing `plugins..ui` - existing `plugins..sidebar` -- Tenant SSR is gated by `TENANT_WHITELIST` and `ALLOW_UNTRUSTED_SSR`. +- Tenant SSR is gated per-tenant by the `allow_ssr` column on the tenant record; the host's BindingResolver reads these permissions from the API's `GET /tenants/bindings` endpoint (cached for 30s). Design direction: - nested labels compose onto the active runtime account, such as `chicago.pizza.com -> bos://chicago.pizza.pingpayio.near/pizza.com` diff --git a/host/README.md b/host/README.md index 41c8db50..2f47fe65 100644 --- a/host/README.md +++ b/host/README.md @@ -96,9 +96,6 @@ For the temporary publish registry, use `bos publish` or `bos publish --deploy`. | `API_SOURCE` | `local` or `remote` | Based on NODE_ENV | | `API_PROXY` | Proxy API requests to another host URL | - | | `NETWORK_ID` | Tenant account suffix resolution: `mainnet` or `testnet` | `mainnet` | -| `ALLOW_OVERRIDE` | Comma-separated tenant override targets like `ui`, `plugins.*`, `plugins.apps` | - | -| `TENANT_WHITELIST` | Comma-separated tenant account IDs allowed to SSR | - | -| `ALLOW_UNTRUSTED_SSR` | Allow tenant SSR without whitelist | `false` | | `HOST_DATABASE_URL` | SQLite database URL for auth | `file:./database.db` | | `HOST_DATABASE_AUTH_TOKEN` | Auth token for remote database | - | | `BETTER_AUTH_SECRET` | Secret for session encryption | - | @@ -112,7 +109,7 @@ For the temporary publish registry, use `bos publish` or `bos publish --deploy`. - Current: tenant config must extend the base BOS runtime - Current: tenant accounts derive relative to the active runtime account namespace - Current: supported tenant overrides are `app.ui`, existing `plugins..ui`, and existing `plugins..sidebar` -- Current: tenant SSR is opt-in via `TENANT_WHITELIST` or `ALLOW_UNTRUSTED_SSR=true` +- Current: tenant SSR is gated per-tenant by the `allowSsr` column on the tenant record; the host's BindingResolver reads these permissions from the API's `GET /tenants/bindings` endpoint (cached for 30s) - Not yet implemented: tenant API/auth overrides in fixed-core mode - Not yet implemented: dynamic new plugin IDs per tenant @@ -123,9 +120,6 @@ Example deployment: ```bash BOS_ACCOUNT=linktree.near BOS_GATEWAY=linktree.com -ALLOW_OVERRIDE=ui,plugins.* -TENANT_WHITELIST=alice.linktree.near,bob.linktree.near -ALLOW_UNTRUSTED_SSR=false bos start --no-interactive ``` @@ -139,7 +133,8 @@ Example tenant behavior: Tenant config rules: - must extend the base BOS runtime -- may only override targets allowed by `ALLOW_OVERRIDE` +- may only override the tenant `ui` when `allow_ui_overrides` is true on the tenant record +- plugin UI overrides require `allow_backend_overrides` on the tenant record - in fixed-core mode, only UI-facing overrides are applied - custom UI remotes must provide integrity - custom plugin UI remotes must provide integrity @@ -147,9 +142,8 @@ Tenant config rules: Tenant SSR rules: -- if `ALLOW_UNTRUSTED_SSR=true`, any valid tenant UI with SSR config may SSR -- otherwise the tenant account must appear in `TENANT_WHITELIST` -- non-whitelisted tenants fall back to client rendering +- tenant SSR is allowed only when the tenant record has `allow_ssr` set +- tenants without `allow_ssr` fall back to client rendering ### Proxy Mode diff --git a/host/src/services/tenant-runtime.ts b/host/src/services/tenant-runtime.ts index 4f3b0cae..ca3adcf7 100644 --- a/host/src/services/tenant-runtime.ts +++ b/host/src/services/tenant-runtime.ts @@ -7,7 +7,7 @@ import { verifySriForUrl } from "everything-dev/integrity"; import type { RuntimePlugin } from "../types"; import { logger } from "../utils/logger"; import { resolveDomain } from "../utils/normalize"; -import { createBindingResolver, type BindingResolver } from "./binding-resolver"; +import { clearBindingResolverCache, createBindingResolver, type BindingResolver } from "./binding-resolver"; const REMOTE_CONFIG_TTL_MS = 30_000; const VERIFICATION_TTL_MS = 5 * 60_000; @@ -90,6 +90,7 @@ export function getTenantRuntimeErrorResponse(error: unknown): { status: number; export function clearTenantRuntimeCaches() { remoteConfigCache.clear(); verifiedUiCache.clear(); + clearBindingResolverCache(); } function getRemoteConfigCached(bosUrl: string, env: BosEnv) { diff --git a/host/tests/integration/tenant-runtime.test.ts b/host/tests/integration/tenant-runtime.test.ts index 1cbf21ec..96c536bb 100644 --- a/host/tests/integration/tenant-runtime.test.ts +++ b/host/tests/integration/tenant-runtime.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { RuntimeConfig } from "../../src/services/config"; const loadRemoteConfigMock = vi.fn(); diff --git a/packages/every-plugin/skills/plugin-client/SKILL.md b/packages/every-plugin/skills/plugin-client/SKILL.md index 7c8125b8..5a9f9d30 100644 --- a/packages/every-plugin/skills/plugin-client/SKILL.md +++ b/packages/every-plugin/skills/plugin-client/SKILL.md @@ -437,7 +437,7 @@ This writes `bos.config.json` with `extends`, scaffolds the project, and generat ### What Can Be Overridden -Child configs can override `app.ui` (custom UI CDN) and `plugins.` (custom plugin URLs). The host's `ALLOW_OVERRIDE` env var controls which sections tenants can customize (`ui`, `plugins`, `plugins.`). +Child configs can override `app.ui` (custom UI CDN) and `plugins.` (custom plugin URLs). Which sections a tenant can customize is controlled per-tenant by the `allow_ui_overrides` and `allow_backend_overrides` flags on the tenant record, resolved by the host from the API's `GET /tenants/bindings` endpoint (cached for 30s). See the `extends-config` skill for deep merge semantics, per-environment extends, and canonical field ordering. diff --git a/packages/everything-dev/skills/extends-config/SKILL.md b/packages/everything-dev/skills/extends-config/SKILL.md index 65fe9041..c8fc8d0a 100644 --- a/packages/everything-dev/skills/extends-config/SKILL.md +++ b/packages/everything-dev/skills/extends-config/SKILL.md @@ -73,13 +73,7 @@ Use this mental model: - `domain` is the public ingress for this runtime - a runtime can be a child in lineage and still become a new tenant root on its own domain -With host env like: - -```bash -ALLOW_OVERRIDE=ui,plugins.* -TENANT_WHITELIST=pizza.pingpayio.near -ALLOW_UNTRUSTED_SSR=false -``` +Tenant permissions come from the DB, not env vars. The host fetches the tenant bindings map from the API's `GET /tenants/bindings` endpoint (cached for 30s), and the tenant record's allow flags gate what may be overridden and whether SSR is permitted. Design target for request mapping: - `pizza.com` -> base runtime `bos://pizza.pingpayio.near/pizza.com` @@ -101,9 +95,8 @@ The tenant config must extend the base BOS runtime. Tenant API/auth overrides an ### SSR behavior Tenant SSR is gated separately from inheritance: -- if `ALLOW_UNTRUSTED_SSR=true`, any valid tenant with SSR config may SSR -- otherwise the tenant account must be listed in `TENANT_WHITELIST` -- non-whitelisted tenants fall back to client rendering +- a tenant may SSR only when its record has `allow_ssr` set +- tenants without `allow_ssr` fall back to client rendering ## Resolved Config: `.bos/bos.resolved-config.json` diff --git a/packages/everything-dev/skills/init-upgrade/SKILL.md b/packages/everything-dev/skills/init-upgrade/SKILL.md index df40b85e..dad27afa 100644 --- a/packages/everything-dev/skills/init-upgrade/SKILL.md +++ b/packages/everything-dev/skills/init-upgrade/SKILL.md @@ -81,15 +81,9 @@ Child apps can either run as their own base runtime on their own domain, or as r Shared-host children must extend the base runtime and do not introduce new server-side plugin IDs dynamically. -### 4. Host deployment env for shared-host mode +### 4. Host resolution for shared-host mode -The shared host uses these env vars to resolve descendant requests: - -```bash -ALLOW_OVERRIDE=ui,plugins.* -TENANT_WHITELIST=pizza.pingpayio.near,chicago.pizza.pingpayio.near -ALLOW_UNTRUSTED_SSR=false -``` +The shared host resolves descendant requests via a DB-backed binding map. It fetches tenant permissions from the API's `GET /tenants/bindings` endpoint (cached for 30s) — no env vars are needed: Design target, for example: - `pingpay.io` -> base runtime `bos://pingpayio.near/pingpay.io` diff --git a/packages/everything-dev/skills/super-app/SKILL.md b/packages/everything-dev/skills/super-app/SKILL.md index 4adae2f3..bc32b536 100644 --- a/packages/everything-dev/skills/super-app/SKILL.md +++ b/packages/everything-dev/skills/super-app/SKILL.md @@ -87,20 +87,13 @@ This runtime is still a lineage child because it extends the parent, but it is a You can also override existing plugin UIs and sidebar entries for tenant-specific navigation. -## Host Env For Tenant Mode +## Host Resolution For Tenant Mode -The shared host uses these env vars to resolve tenants: +The shared host resolves tenant permissions from the API, not env vars. It fetches the tenant bindings map from the API's `GET /tenants/bindings` endpoint (cached for 30s) and applies DB-backed per-tenant permissions: -```bash -ALLOW_OVERRIDE=ui,plugins.* -TENANT_WHITELIST=pizza.pingpayio.near,chicago.pizza.pingpayio.near -ALLOW_UNTRUSTED_SSR=false -``` - -Meaning: -- `ALLOW_OVERRIDE` controls which tenant config sections can affect request-scoped composition. Format: comma-separated list (e.g., `ui,plugins.*`) where `plugins.*` is a glob matching all plugin overrides -- `TENANT_WHITELIST` controls which tenants may use SSR -- `ALLOW_UNTRUSTED_SSR=true` allows SSR for any valid tenant with SSR config +- `allow_ui_overrides` controls whether the tenant `ui` override can affect request-scoped composition +- `allow_backend_overrides` controls whether tenant plugin UI overrides (`plugins..ui`, `plugins..sidebar`) are applied +- `allow_ssr` controls whether the tenant may use SSR ## Resolution Rules @@ -141,7 +134,7 @@ If tenant overrides are not applying as expected: 1. **Verify the tenant config exists in FastKV**: The config must be published to `{tenantAccount}/bos/gateways/{gateway}/bos.config.json` 2. **Check extends chain**: The tenant config must extend the base runtime via its `extends` field 3. **Check host logs**: Run `cat .bos/logs/host.log` and look for tenant resolution messages — the host logs which runtime config it resolved for each request -4. **Check env vars**: `ALLOW_OVERRIDE` must include the sections you're overriding (e.g., `ui` or `plugins.*`) +4. **Check tenant permissions**: the tenant record must have the relevant allow flag set in the database (e.g., `allow_ui_overrides` for a `ui` override, `allow_backend_overrides` for plugin UI overrides) 5. **Verify integrity**: If tenant remote URLs have integrity hashes, the host validates them — mismatches cause rejection 6. **Missing tenant config**: If the tenant config is not found in FastKV, the host silently serves the base runtime config without tenant overrides — no error is surfaced to the browser From d944f55df0415055187c0c785c16da6cfc8c8231 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 13 Aug 2026 14:24:09 -0500 Subject: [PATCH 03/24] add branding --- .env.example | 6 +- ui/src/components/near-branding.tsx | 24 +++ ui/src/routeTree.gen.ts | 223 ++++++++++++++++++---------- 3 files changed, 168 insertions(+), 85 deletions(-) create mode 100644 ui/src/components/near-branding.tsx diff --git a/.env.example b/.env.example index 452789fd..dca67750 100644 --- a/.env.example +++ b/.env.example @@ -2,13 +2,15 @@ # Update values as needed for your local environment # app.host -CORS_ORIGIN=http://localhost:4100 +CORS_ORIGIN=http://localhost:3000 CSP_STRICT= # app.api API_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5432/api_db # app.auth +NEAR_SUB_ACCOUNT_PARENT_KEY_MAINNET= +NEAR_SUB_ACCOUNT_PARENT_KEY_TESTNET= AUTH_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5433/auth_db BETTER_AUTH_SECRET= GITHUB_CLIENT_SECRET= @@ -19,8 +21,6 @@ TWILIO_AUTH_TOKEN= TWILIO_PHONE_NUMBER= RESEND_API_KEY= NEAR_RELAYER_PRIVATE_KEY= -NEAR_SUB_ACCOUNT_PARENT_KEY_MAINNET= -NEAR_SUB_ACCOUNT_PARENT_KEY_TESTNET= # plugins.template TEMPLATE_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5434/template_db diff --git a/ui/src/components/near-branding.tsx b/ui/src/components/near-branding.tsx new file mode 100644 index 00000000..e3454dcf --- /dev/null +++ b/ui/src/components/near-branding.tsx @@ -0,0 +1,24 @@ +import builtOn from "@/assets/built_on.png"; +import builtOnRev from "@/assets/built_on_rev.png"; + +export function NearBranding() { + return ( + + Built on NEAR + Built on NEAR + + ); +} diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 8ee2490c..c7659df3 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -10,21 +10,22 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LayoutRouteImport } from './routes/_layout' -import { Route as LayoutIndexRouteImport } from './routes/_layout/index' -import { Route as LayoutSkillRouteImport } from './routes/_layout/skill' -import { Route as LayoutLoginRouteImport } from './routes/_layout/login' -import { Route as LayoutAboutRouteImport } from './routes/_layout/about' +import { Route as LayoutPublicRouteImport } from './routes/_layout/_public' import { Route as LayoutAuthenticatedRouteImport } from './routes/_layout/_authenticated' import { Route as LayoutThingsIndexRouteImport } from './routes/_layout/things/index' import { Route as LayoutAppsIndexRouteImport } from './routes/_layout/apps/index' +import { Route as LayoutPublicIndexRouteImport } from './routes/_layout/_public/index' import { Route as LayoutThingsLiveRouteImport } from './routes/_layout/things/live' import { Route as LayoutThingsThingIdRouteImport } from './routes/_layout/things/$thingId' +import { Route as LayoutPublicLoginRouteImport } from './routes/_layout/_public/login' +import { Route as LayoutPublicAboutRouteImport } from './routes/_layout/_public/about' import { Route as LayoutAuthenticatedSettingsRouteImport } from './routes/_layout/_authenticated/settings' import { Route as LayoutAuthenticatedHomeRouteImport } from './routes/_layout/_authenticated/home' import { Route as LayoutAuthenticatedAdminRouteImport } from './routes/_layout/_authenticated/admin' import { Route as LayoutAppsAccountIdIndexRouteImport } from './routes/_layout/apps/$accountId/index' import { Route as LayoutAuthenticatedSettingsIndexRouteImport } from './routes/_layout/_authenticated/settings/index' import { Route as LayoutAuthenticatedOrganizationsIndexRouteImport } from './routes/_layout/_authenticated/organizations/index' +import { Route as LayoutAuthenticatedAdminIndexRouteImport } from './routes/_layout/_authenticated/admin/index' import { Route as LayoutAppsAccountIdGatewayIdRouteImport } from './routes/_layout/apps/$accountId/$gatewayId' import { Route as LayoutAuthenticatedThingsNewRouteImport } from './routes/_layout/_authenticated/things/new' import { Route as LayoutAuthenticatedTenantNewRouteImport } from './routes/_layout/_authenticated/tenant/new' @@ -34,30 +35,15 @@ import { Route as LayoutAuthenticatedSettingsProfileRouteImport } from './routes import { Route as LayoutAuthenticatedSettingsAuthMethodsRouteImport } from './routes/_layout/_authenticated/settings/auth-methods' import { Route as LayoutAuthenticatedOrganizationsNewRouteImport } from './routes/_layout/_authenticated/organizations/new' import { Route as LayoutAuthenticatedOrganizationsSlugRouteImport } from './routes/_layout/_authenticated/organizations/$slug' +import { Route as LayoutAuthenticatedAdminSystemRouteImport } from './routes/_layout/_authenticated/admin/system' import { Route as LayoutAuthenticatedAcceptInvitationIdRouteImport } from './routes/_layout/_authenticated/accept-invitation.$id' const LayoutRoute = LayoutRouteImport.update({ id: '/_layout', getParentRoute: () => rootRouteImport, } as any) -const LayoutIndexRoute = LayoutIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => LayoutRoute, -} as any) -const LayoutSkillRoute = LayoutSkillRouteImport.update({ - id: '/skill', - path: '/skill', - getParentRoute: () => LayoutRoute, -} as any) -const LayoutLoginRoute = LayoutLoginRouteImport.update({ - id: '/login', - path: '/login', - getParentRoute: () => LayoutRoute, -} as any) -const LayoutAboutRoute = LayoutAboutRouteImport.update({ - id: '/about', - path: '/about', +const LayoutPublicRoute = LayoutPublicRouteImport.update({ + id: '/_public', getParentRoute: () => LayoutRoute, } as any) const LayoutAuthenticatedRoute = LayoutAuthenticatedRouteImport.update({ @@ -74,6 +60,11 @@ const LayoutAppsIndexRoute = LayoutAppsIndexRouteImport.update({ path: '/apps/', getParentRoute: () => LayoutRoute, } as any) +const LayoutPublicIndexRoute = LayoutPublicIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => LayoutPublicRoute, +} as any) const LayoutThingsLiveRoute = LayoutThingsLiveRouteImport.update({ id: '/things/live', path: '/things/live', @@ -84,6 +75,16 @@ const LayoutThingsThingIdRoute = LayoutThingsThingIdRouteImport.update({ path: '/things/$thingId', getParentRoute: () => LayoutRoute, } as any) +const LayoutPublicLoginRoute = LayoutPublicLoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => LayoutPublicRoute, +} as any) +const LayoutPublicAboutRoute = LayoutPublicAboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => LayoutPublicRoute, +} as any) const LayoutAuthenticatedSettingsRoute = LayoutAuthenticatedSettingsRouteImport.update({ id: '/settings', @@ -119,6 +120,12 @@ const LayoutAuthenticatedOrganizationsIndexRoute = path: '/organizations/', getParentRoute: () => LayoutAuthenticatedRoute, } as any) +const LayoutAuthenticatedAdminIndexRoute = + LayoutAuthenticatedAdminIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => LayoutAuthenticatedAdminRoute, + } as any) const LayoutAppsAccountIdGatewayIdRoute = LayoutAppsAccountIdGatewayIdRouteImport.update({ id: '/apps/$accountId/$gatewayId', @@ -173,6 +180,12 @@ const LayoutAuthenticatedOrganizationsSlugRoute = path: '/organizations/$slug', getParentRoute: () => LayoutAuthenticatedRoute, } as any) +const LayoutAuthenticatedAdminSystemRoute = + LayoutAuthenticatedAdminSystemRouteImport.update({ + id: '/system', + path: '/system', + getParentRoute: () => LayoutAuthenticatedAdminRoute, + } as any) const LayoutAuthenticatedAcceptInvitationIdRoute = LayoutAuthenticatedAcceptInvitationIdRouteImport.update({ id: '/accept-invitation/$id', @@ -181,18 +194,18 @@ const LayoutAuthenticatedAcceptInvitationIdRoute = } as any) export interface FileRoutesByFullPath { - '/': typeof LayoutIndexRoute - '/about': typeof LayoutAboutRoute - '/login': typeof LayoutLoginRoute - '/skill': typeof LayoutSkillRoute - '/admin': typeof LayoutAuthenticatedAdminRoute + '/': typeof LayoutPublicIndexRoute + '/admin': typeof LayoutAuthenticatedAdminRouteWithChildren '/home': typeof LayoutAuthenticatedHomeRoute '/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren + '/about': typeof LayoutPublicAboutRoute + '/login': typeof LayoutPublicLoginRoute '/things/$thingId': typeof LayoutThingsThingIdRoute '/things/live': typeof LayoutThingsLiveRoute '/apps/': typeof LayoutAppsIndexRoute '/things/': typeof LayoutThingsIndexRoute '/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute + '/admin/system': typeof LayoutAuthenticatedAdminSystemRoute '/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute '/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -202,22 +215,22 @@ export interface FileRoutesByFullPath { '/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/things/new': typeof LayoutAuthenticatedThingsNewRoute '/apps/$accountId/$gatewayId': typeof LayoutAppsAccountIdGatewayIdRoute + '/admin/': typeof LayoutAuthenticatedAdminIndexRoute '/organizations/': typeof LayoutAuthenticatedOrganizationsIndexRoute '/settings/': typeof LayoutAuthenticatedSettingsIndexRoute '/apps/$accountId/': typeof LayoutAppsAccountIdIndexRoute } export interface FileRoutesByTo { - '/': typeof LayoutIndexRoute - '/about': typeof LayoutAboutRoute - '/login': typeof LayoutLoginRoute - '/skill': typeof LayoutSkillRoute - '/admin': typeof LayoutAuthenticatedAdminRoute + '/': typeof LayoutPublicIndexRoute '/home': typeof LayoutAuthenticatedHomeRoute + '/about': typeof LayoutPublicAboutRoute + '/login': typeof LayoutPublicLoginRoute '/things/$thingId': typeof LayoutThingsThingIdRoute '/things/live': typeof LayoutThingsLiveRoute '/apps': typeof LayoutAppsIndexRoute '/things': typeof LayoutThingsIndexRoute '/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute + '/admin/system': typeof LayoutAuthenticatedAdminSystemRoute '/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute '/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -227,6 +240,7 @@ export interface FileRoutesByTo { '/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/things/new': typeof LayoutAuthenticatedThingsNewRoute '/apps/$accountId/$gatewayId': typeof LayoutAppsAccountIdGatewayIdRoute + '/admin': typeof LayoutAuthenticatedAdminIndexRoute '/organizations': typeof LayoutAuthenticatedOrganizationsIndexRoute '/settings': typeof LayoutAuthenticatedSettingsIndexRoute '/apps/$accountId': typeof LayoutAppsAccountIdIndexRoute @@ -235,18 +249,19 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/_layout': typeof LayoutRouteWithChildren '/_layout/_authenticated': typeof LayoutAuthenticatedRouteWithChildren - '/_layout/about': typeof LayoutAboutRoute - '/_layout/login': typeof LayoutLoginRoute - '/_layout/skill': typeof LayoutSkillRoute - '/_layout/': typeof LayoutIndexRoute - '/_layout/_authenticated/admin': typeof LayoutAuthenticatedAdminRoute + '/_layout/_public': typeof LayoutPublicRouteWithChildren + '/_layout/_authenticated/admin': typeof LayoutAuthenticatedAdminRouteWithChildren '/_layout/_authenticated/home': typeof LayoutAuthenticatedHomeRoute '/_layout/_authenticated/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren + '/_layout/_public/about': typeof LayoutPublicAboutRoute + '/_layout/_public/login': typeof LayoutPublicLoginRoute '/_layout/things/$thingId': typeof LayoutThingsThingIdRoute '/_layout/things/live': typeof LayoutThingsLiveRoute + '/_layout/_public/': typeof LayoutPublicIndexRoute '/_layout/apps/': typeof LayoutAppsIndexRoute '/_layout/things/': typeof LayoutThingsIndexRoute '/_layout/_authenticated/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute + '/_layout/_authenticated/admin/system': typeof LayoutAuthenticatedAdminSystemRoute '/_layout/_authenticated/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute '/_layout/_authenticated/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute '/_layout/_authenticated/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -256,6 +271,7 @@ export interface FileRoutesById { '/_layout/_authenticated/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/_layout/_authenticated/things/new': typeof LayoutAuthenticatedThingsNewRoute '/_layout/apps/$accountId/$gatewayId': typeof LayoutAppsAccountIdGatewayIdRoute + '/_layout/_authenticated/admin/': typeof LayoutAuthenticatedAdminIndexRoute '/_layout/_authenticated/organizations/': typeof LayoutAuthenticatedOrganizationsIndexRoute '/_layout/_authenticated/settings/': typeof LayoutAuthenticatedSettingsIndexRoute '/_layout/apps/$accountId/': typeof LayoutAppsAccountIdIndexRoute @@ -264,17 +280,17 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' - | '/about' - | '/login' - | '/skill' | '/admin' | '/home' | '/settings' + | '/about' + | '/login' | '/things/$thingId' | '/things/live' | '/apps/' | '/things/' | '/accept-invitation/$id' + | '/admin/system' | '/organizations/$slug' | '/organizations/new' | '/settings/auth-methods' @@ -284,22 +300,22 @@ export interface FileRouteTypes { | '/tenant/new' | '/things/new' | '/apps/$accountId/$gatewayId' + | '/admin/' | '/organizations/' | '/settings/' | '/apps/$accountId/' fileRoutesByTo: FileRoutesByTo to: | '/' + | '/home' | '/about' | '/login' - | '/skill' - | '/admin' - | '/home' | '/things/$thingId' | '/things/live' | '/apps' | '/things' | '/accept-invitation/$id' + | '/admin/system' | '/organizations/$slug' | '/organizations/new' | '/settings/auth-methods' @@ -309,6 +325,7 @@ export interface FileRouteTypes { | '/tenant/new' | '/things/new' | '/apps/$accountId/$gatewayId' + | '/admin' | '/organizations' | '/settings' | '/apps/$accountId' @@ -316,18 +333,19 @@ export interface FileRouteTypes { | '__root__' | '/_layout' | '/_layout/_authenticated' - | '/_layout/about' - | '/_layout/login' - | '/_layout/skill' - | '/_layout/' + | '/_layout/_public' | '/_layout/_authenticated/admin' | '/_layout/_authenticated/home' | '/_layout/_authenticated/settings' + | '/_layout/_public/about' + | '/_layout/_public/login' | '/_layout/things/$thingId' | '/_layout/things/live' + | '/_layout/_public/' | '/_layout/apps/' | '/_layout/things/' | '/_layout/_authenticated/accept-invitation/$id' + | '/_layout/_authenticated/admin/system' | '/_layout/_authenticated/organizations/$slug' | '/_layout/_authenticated/organizations/new' | '/_layout/_authenticated/settings/auth-methods' @@ -337,6 +355,7 @@ export interface FileRouteTypes { | '/_layout/_authenticated/tenant/new' | '/_layout/_authenticated/things/new' | '/_layout/apps/$accountId/$gatewayId' + | '/_layout/_authenticated/admin/' | '/_layout/_authenticated/organizations/' | '/_layout/_authenticated/settings/' | '/_layout/apps/$accountId/' @@ -355,32 +374,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutRouteImport parentRoute: typeof rootRouteImport } - '/_layout/': { - id: '/_layout/' - path: '/' + '/_layout/_public': { + id: '/_layout/_public' + path: '' fullPath: '/' - preLoaderRoute: typeof LayoutIndexRouteImport - parentRoute: typeof LayoutRoute - } - '/_layout/skill': { - id: '/_layout/skill' - path: '/skill' - fullPath: '/skill' - preLoaderRoute: typeof LayoutSkillRouteImport - parentRoute: typeof LayoutRoute - } - '/_layout/login': { - id: '/_layout/login' - path: '/login' - fullPath: '/login' - preLoaderRoute: typeof LayoutLoginRouteImport - parentRoute: typeof LayoutRoute - } - '/_layout/about': { - id: '/_layout/about' - path: '/about' - fullPath: '/about' - preLoaderRoute: typeof LayoutAboutRouteImport + preLoaderRoute: typeof LayoutPublicRouteImport parentRoute: typeof LayoutRoute } '/_layout/_authenticated': { @@ -404,6 +402,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAppsIndexRouteImport parentRoute: typeof LayoutRoute } + '/_layout/_public/': { + id: '/_layout/_public/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof LayoutPublicIndexRouteImport + parentRoute: typeof LayoutPublicRoute + } '/_layout/things/live': { id: '/_layout/things/live' path: '/things/live' @@ -418,6 +423,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutThingsThingIdRouteImport parentRoute: typeof LayoutRoute } + '/_layout/_public/login': { + id: '/_layout/_public/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LayoutPublicLoginRouteImport + parentRoute: typeof LayoutPublicRoute + } + '/_layout/_public/about': { + id: '/_layout/_public/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof LayoutPublicAboutRouteImport + parentRoute: typeof LayoutPublicRoute + } '/_layout/_authenticated/settings': { id: '/_layout/_authenticated/settings' path: '/settings' @@ -460,6 +479,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedOrganizationsIndexRouteImport parentRoute: typeof LayoutAuthenticatedRoute } + '/_layout/_authenticated/admin/': { + id: '/_layout/_authenticated/admin/' + path: '/' + fullPath: '/admin/' + preLoaderRoute: typeof LayoutAuthenticatedAdminIndexRouteImport + parentRoute: typeof LayoutAuthenticatedAdminRoute + } '/_layout/apps/$accountId/$gatewayId': { id: '/_layout/apps/$accountId/$gatewayId' path: '/apps/$accountId/$gatewayId' @@ -523,6 +549,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedOrganizationsSlugRouteImport parentRoute: typeof LayoutAuthenticatedRoute } + '/_layout/_authenticated/admin/system': { + id: '/_layout/_authenticated/admin/system' + path: '/system' + fullPath: '/admin/system' + preLoaderRoute: typeof LayoutAuthenticatedAdminSystemRouteImport + parentRoute: typeof LayoutAuthenticatedAdminRoute + } '/_layout/_authenticated/accept-invitation/$id': { id: '/_layout/_authenticated/accept-invitation/$id' path: '/accept-invitation/$id' @@ -533,6 +566,22 @@ declare module '@tanstack/react-router' { } } +interface LayoutAuthenticatedAdminRouteChildren { + LayoutAuthenticatedAdminSystemRoute: typeof LayoutAuthenticatedAdminSystemRoute + LayoutAuthenticatedAdminIndexRoute: typeof LayoutAuthenticatedAdminIndexRoute +} + +const LayoutAuthenticatedAdminRouteChildren: LayoutAuthenticatedAdminRouteChildren = + { + LayoutAuthenticatedAdminSystemRoute: LayoutAuthenticatedAdminSystemRoute, + LayoutAuthenticatedAdminIndexRoute: LayoutAuthenticatedAdminIndexRoute, + } + +const LayoutAuthenticatedAdminRouteWithChildren = + LayoutAuthenticatedAdminRoute._addFileChildren( + LayoutAuthenticatedAdminRouteChildren, + ) + interface LayoutAuthenticatedSettingsRouteChildren { LayoutAuthenticatedSettingsAuthMethodsRoute: typeof LayoutAuthenticatedSettingsAuthMethodsRoute LayoutAuthenticatedSettingsProfileRoute: typeof LayoutAuthenticatedSettingsProfileRoute @@ -558,7 +607,7 @@ const LayoutAuthenticatedSettingsRouteWithChildren = ) interface LayoutAuthenticatedRouteChildren { - LayoutAuthenticatedAdminRoute: typeof LayoutAuthenticatedAdminRoute + LayoutAuthenticatedAdminRoute: typeof LayoutAuthenticatedAdminRouteWithChildren LayoutAuthenticatedHomeRoute: typeof LayoutAuthenticatedHomeRoute LayoutAuthenticatedSettingsRoute: typeof LayoutAuthenticatedSettingsRouteWithChildren LayoutAuthenticatedAcceptInvitationIdRoute: typeof LayoutAuthenticatedAcceptInvitationIdRoute @@ -571,7 +620,7 @@ interface LayoutAuthenticatedRouteChildren { } const LayoutAuthenticatedRouteChildren: LayoutAuthenticatedRouteChildren = { - LayoutAuthenticatedAdminRoute: LayoutAuthenticatedAdminRoute, + LayoutAuthenticatedAdminRoute: LayoutAuthenticatedAdminRouteWithChildren, LayoutAuthenticatedHomeRoute: LayoutAuthenticatedHomeRoute, LayoutAuthenticatedSettingsRoute: LayoutAuthenticatedSettingsRouteWithChildren, @@ -592,12 +641,25 @@ const LayoutAuthenticatedRouteChildren: LayoutAuthenticatedRouteChildren = { const LayoutAuthenticatedRouteWithChildren = LayoutAuthenticatedRoute._addFileChildren(LayoutAuthenticatedRouteChildren) +interface LayoutPublicRouteChildren { + LayoutPublicAboutRoute: typeof LayoutPublicAboutRoute + LayoutPublicLoginRoute: typeof LayoutPublicLoginRoute + LayoutPublicIndexRoute: typeof LayoutPublicIndexRoute +} + +const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { + LayoutPublicAboutRoute: LayoutPublicAboutRoute, + LayoutPublicLoginRoute: LayoutPublicLoginRoute, + LayoutPublicIndexRoute: LayoutPublicIndexRoute, +} + +const LayoutPublicRouteWithChildren = LayoutPublicRoute._addFileChildren( + LayoutPublicRouteChildren, +) + interface LayoutRouteChildren { LayoutAuthenticatedRoute: typeof LayoutAuthenticatedRouteWithChildren - LayoutAboutRoute: typeof LayoutAboutRoute - LayoutLoginRoute: typeof LayoutLoginRoute - LayoutSkillRoute: typeof LayoutSkillRoute - LayoutIndexRoute: typeof LayoutIndexRoute + LayoutPublicRoute: typeof LayoutPublicRouteWithChildren LayoutThingsThingIdRoute: typeof LayoutThingsThingIdRoute LayoutThingsLiveRoute: typeof LayoutThingsLiveRoute LayoutAppsIndexRoute: typeof LayoutAppsIndexRoute @@ -608,10 +670,7 @@ interface LayoutRouteChildren { const LayoutRouteChildren: LayoutRouteChildren = { LayoutAuthenticatedRoute: LayoutAuthenticatedRouteWithChildren, - LayoutAboutRoute: LayoutAboutRoute, - LayoutLoginRoute: LayoutLoginRoute, - LayoutSkillRoute: LayoutSkillRoute, - LayoutIndexRoute: LayoutIndexRoute, + LayoutPublicRoute: LayoutPublicRouteWithChildren, LayoutThingsThingIdRoute: LayoutThingsThingIdRoute, LayoutThingsLiveRoute: LayoutThingsLiveRoute, LayoutAppsIndexRoute: LayoutAppsIndexRoute, From e1a050495936b14d91109fed6591c375a21f067f Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Thu, 13 Aug 2026 17:23:21 -0500 Subject: [PATCH 04/24] working --- .env.example | 2 +- api/src/contract.ts | 2 +- api/src/db/migrations/0003_brief_freak.sql | 1 + api/src/db/migrations/meta/0002_snapshot.json | 21 +- api/src/db/migrations/meta/0003_snapshot.json | 165 +++++++++++++++ api/src/db/migrations/meta/_journal.json | 9 +- api/src/db/schema.ts | 12 +- api/src/index.ts | 15 +- api/src/services/tenants.ts | 16 +- host/src/services/binding-resolver.ts | 13 +- host/src/services/tenant-runtime.ts | 12 +- .../integration/ssr-bundled-runtime.test.ts | 2 +- host/tests/integration/tenant-runtime.test.ts | 45 ++-- package.json | 2 +- .../skills/api-and-auth/SKILL.md | 48 +++++ packages/everything-dev/src/infra/planner.ts | 3 + packages/everything-dev/src/ui/router.ts | 51 ++--- plugins/_template/src/db/schema.ts | 18 ++ .../regression/browser/helpers/page-ready.ts | 18 ++ tests/regression/browser/helpers/seeded.ts | 2 +- .../browser/specs/auth-redirect.spec.ts | 4 +- tests/regression/browser/specs/logout.spec.ts | 12 +- tests/regression/browser/specs/theme.spec.ts | 15 +- tests/regression/http/metadata_test.go | 28 +++ ui/rsbuild.config.ts | 2 +- ui/src/components/index.ts | 1 + ui/src/components/user-nav.tsx | 82 ++++++-- ui/src/lib/near-profile.ts | 17 ++ ui/src/routeTree.gen.ts | 42 ++++ ui/src/routes/__root.tsx | 3 +- ui/src/routes/_layout.tsx | 5 +- ui/src/routes/_layout/_authenticated/home.tsx | 19 +- ui/src/routes/_layout/_public/$accountId.tsx | 197 ++++++++++++++++++ ui/src/routes/_layout/_public/skill.tsx | 126 +++++++++++ 34 files changed, 857 insertions(+), 153 deletions(-) create mode 100644 api/src/db/migrations/0003_brief_freak.sql create mode 100644 api/src/db/migrations/meta/0003_snapshot.json create mode 100644 ui/src/lib/near-profile.ts create mode 100644 ui/src/routes/_layout/_public/$accountId.tsx create mode 100644 ui/src/routes/_layout/_public/skill.tsx diff --git a/.env.example b/.env.example index dca67750..e9e9b5e3 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ # Update values as needed for your local environment # app.host -CORS_ORIGIN=http://localhost:3000 +CORS_ORIGIN=http://localhost:4100 CSP_STRICT= # app.api diff --git a/api/src/contract.ts b/api/src/contract.ts index 92bc79b2..3c9cd6e8 100644 --- a/api/src/contract.ts +++ b/api/src/contract.ts @@ -17,7 +17,7 @@ export const TenantSchema = z.object({ id: z.string(), subdomain: z.string(), accountId: z.string(), - orgId: z.string(), + orgId: z.string().nullable(), name: z.string(), status: TenantStatusSchema, allowUiOverrides: z.boolean(), diff --git a/api/src/db/migrations/0003_brief_freak.sql b/api/src/db/migrations/0003_brief_freak.sql new file mode 100644 index 00000000..1a294453 --- /dev/null +++ b/api/src/db/migrations/0003_brief_freak.sql @@ -0,0 +1 @@ +ALTER TABLE "tenants" ALTER COLUMN "org_id" DROP NOT NULL; \ No newline at end of file diff --git a/api/src/db/migrations/meta/0002_snapshot.json b/api/src/db/migrations/meta/0002_snapshot.json index fda7cde0..4351ee45 100644 --- a/api/src/db/migrations/meta/0002_snapshot.json +++ b/api/src/db/migrations/meta/0002_snapshot.json @@ -127,23 +127,17 @@ "tenants_subdomain_unique": { "name": "tenants_subdomain_unique", "nullsNotDistinct": false, - "columns": [ - "subdomain" - ] + "columns": ["subdomain"] }, "tenants_account_id_unique": { "name": "tenants_account_id_unique", "nullsNotDistinct": false, - "columns": [ - "account_id" - ] + "columns": ["account_id"] }, "tenants_org_id_unique": { "name": "tenants_org_id_unique", "nullsNotDistinct": false, - "columns": [ - "org_id" - ] + "columns": ["org_id"] } }, "policies": {}, @@ -155,12 +149,7 @@ "public.tenant_status": { "name": "tenant_status", "schema": "public", - "values": [ - "active", - "pending", - "suspended", - "pending_deletion" - ] + "values": ["active", "pending", "suspended", "pending_deletion"] } }, "schemas": {}, @@ -173,4 +162,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/api/src/db/migrations/meta/0003_snapshot.json b/api/src/db/migrations/meta/0003_snapshot.json new file mode 100644 index 00000000..b013ab2b --- /dev/null +++ b/api/src/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,165 @@ +{ + "id": "85c9d130-a4b6-4026-a2df-890fddfa7518", + "prevId": "7b8a421a-d0c7-414a-96e8-0c259935178b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "tenant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "allow_ui_overrides": { + "name": "allow_ui_overrides", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_backend_overrides": { + "name": "allow_backend_overrides", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allow_ssr": { + "name": "allow_ssr", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tenants_subdomain_idx": { + "name": "tenants_subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tenants_account_id_idx": { + "name": "tenants_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tenants_subdomain_unique": { + "name": "tenants_subdomain_unique", + "nullsNotDistinct": false, + "columns": ["subdomain"] + }, + "tenants_account_id_unique": { + "name": "tenants_account_id_unique", + "nullsNotDistinct": false, + "columns": ["account_id"] + }, + "tenants_org_id_unique": { + "name": "tenants_org_id_unique", + "nullsNotDistinct": false, + "columns": ["org_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.tenant_status": { + "name": "tenant_status", + "schema": "public", + "values": ["active", "pending", "suspended", "pending_deletion"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/api/src/db/migrations/meta/_journal.json b/api/src/db/migrations/meta/_journal.json index 9e5e42bb..74597e55 100644 --- a/api/src/db/migrations/meta/_journal.json +++ b/api/src/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1786645659094, "tag": "0002_awesome_madame_web", "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1786653436589, + "tag": "0003_brief_freak", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index c689380d..24aedf51 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -1,12 +1,4 @@ -import { - boolean, - pgEnum, - pgTable, - text, - timestamp, - uniqueIndex, - uuid, -} from "drizzle-orm/pg-core"; +import { boolean, pgEnum, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"; export const tenantStatus = pgEnum("tenant_status", [ "active", @@ -21,7 +13,7 @@ export const tenants = pgTable( id: uuid("id").defaultRandom().primaryKey(), subdomain: text("subdomain").notNull().unique(), accountId: text("account_id").notNull().unique(), - orgId: text("org_id").notNull().unique(), + orgId: text("org_id").unique(), name: text("name").notNull(), status: tenantStatus("status").default("active").notNull(), allowUiOverrides: boolean("allow_ui_overrides").default(true).notNull(), diff --git a/api/src/index.ts b/api/src/index.ts index 9ebbaed8..cf2d04d0 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -89,7 +89,10 @@ export default createPlugin.withPlugins()({ const authorizedTenant = async ( input: { tenantId: string }, - context: { organization: { activeOrganizationId: string } }, + context: { + organization: { activeOrganizationId: string }; + near?: { primaryAccountId: string | null }; + }, ) => { const activeOrgId = context.organization.activeOrganizationId; const tenant = await services.tenants.resolveTenantById(input.tenantId); @@ -99,6 +102,16 @@ export default createPlugin.withPlugins()({ data: { resource: "tenant", resourceId: input.tenantId }, }); } + if (tenant.orgId === null) { + const isOwner = + !!context.near?.primaryAccountId && context.near.primaryAccountId === tenant.accountId; + if (!isOwner) { + throw new ORPCError("FORBIDDEN", { + message: "You do not own this personal tenant", + }); + } + return tenant; + } if (tenant.orgId !== activeOrgId) { throw new ORPCError("FORBIDDEN", { message: "You are not a member of this tenant's organization", diff --git a/api/src/services/tenants.ts b/api/src/services/tenants.ts index 4cdb1aa5..34f6231b 100644 --- a/api/src/services/tenants.ts +++ b/api/src/services/tenants.ts @@ -10,7 +10,7 @@ export interface TenantRecord { id: string; subdomain: string; accountId: string; - orgId: string; + orgId: string | null; name: string; status: TenantStatus; allowUiOverrides: boolean; @@ -34,7 +34,7 @@ export interface TenantInput { subdomain: string; name: string; accountId: string; - orgId: string; + orgId: string | null; status?: TenantStatus; allowUiOverrides?: boolean; allowBackendOverrides?: boolean; @@ -50,7 +50,13 @@ export interface TenantsService { input: Partial< Pick< TenantInput, - "name" | "subdomain" | "accountId" | "status" | "allowUiOverrides" | "allowBackendOverrides" | "allowSsr" + | "name" + | "subdomain" + | "accountId" + | "status" + | "allowUiOverrides" + | "allowBackendOverrides" + | "allowSsr" > >, ): Promise; @@ -143,7 +149,9 @@ export const TenantsLive = Layer.effect( accountId: input.accountId, orgId: input.orgId, ...(input.status !== undefined && { status: input.status }), - ...(input.allowUiOverrides !== undefined && { allowUiOverrides: input.allowUiOverrides }), + ...(input.allowUiOverrides !== undefined && { + allowUiOverrides: input.allowUiOverrides, + }), ...(input.allowBackendOverrides !== undefined && { allowBackendOverrides: input.allowBackendOverrides, }), diff --git a/host/src/services/binding-resolver.ts b/host/src/services/binding-resolver.ts index 30ed5ee4..0a97472d 100644 --- a/host/src/services/binding-resolver.ts +++ b/host/src/services/binding-resolver.ts @@ -30,11 +30,7 @@ export function clearBindingResolverCache() { function isBaseHost(hostname: string, gatewayId: string): boolean { const normalized = hostname.toLowerCase(); - return ( - normalized === gatewayId || - normalized === "localhost" || - normalized === "127.0.0.1" - ); + return normalized === gatewayId || normalized === "localhost" || normalized === "127.0.0.1"; } async function fetchBindingsFromApi(apiUrl: string): Promise { @@ -69,9 +65,7 @@ function mapBindingsByHostname( return entries; } -function ensureBindingsLoaded( - config: RuntimeConfig, -): Promise> { +function ensureBindingsLoaded(config: RuntimeConfig): Promise> { const apiUrl = config.api?.url; if (!apiUrl) { return Promise.resolve(new Map()); @@ -91,8 +85,7 @@ function ensureBindingsLoaded( return bindingsCache.refetching; } - const staleEntries = - bindingsCache?.apiUrl === apiUrl ? bindingsCache.entries : null; + const staleEntries = bindingsCache?.apiUrl === apiUrl ? bindingsCache.entries : null; const fetchPromise = fetchBindingsFromApi(apiUrl) .then((bindings) => { diff --git a/host/src/services/tenant-runtime.ts b/host/src/services/tenant-runtime.ts index ca3adcf7..69c1aa2a 100644 --- a/host/src/services/tenant-runtime.ts +++ b/host/src/services/tenant-runtime.ts @@ -1,13 +1,13 @@ -import { - buildRuntimeConfig, - loadRemoteConfig, - type RuntimeConfig, -} from "everything-dev/config"; +import { buildRuntimeConfig, loadRemoteConfig, type RuntimeConfig } from "everything-dev/config"; import { verifySriForUrl } from "everything-dev/integrity"; import type { RuntimePlugin } from "../types"; import { logger } from "../utils/logger"; import { resolveDomain } from "../utils/normalize"; -import { clearBindingResolverCache, createBindingResolver, type BindingResolver } from "./binding-resolver"; +import { + type BindingResolver, + clearBindingResolverCache, + createBindingResolver, +} from "./binding-resolver"; const REMOTE_CONFIG_TTL_MS = 30_000; const VERIFICATION_TTL_MS = 5 * 60_000; diff --git a/host/tests/integration/ssr-bundled-runtime.test.ts b/host/tests/integration/ssr-bundled-runtime.test.ts index d26b3177..c880184a 100644 --- a/host/tests/integration/ssr-bundled-runtime.test.ts +++ b/host/tests/integration/ssr-bundled-runtime.test.ts @@ -57,7 +57,7 @@ describe("bundled host SSR runtime", () => { const html = await response.text(); expect(response.status).toBe(200); - expect(html).toContain("connect to everything"); + expect(html).toContain("everything.dev"); expect(html).not.toContain("SSR unavailable, showing client app."); expect(html).not.toContain("

Loading...

"); diff --git a/host/tests/integration/tenant-runtime.test.ts b/host/tests/integration/tenant-runtime.test.ts index 96c536bb..1831a817 100644 --- a/host/tests/integration/tenant-runtime.test.ts +++ b/host/tests/integration/tenant-runtime.test.ts @@ -19,6 +19,7 @@ vi.mock("everything-dev/integrity", () => ({ const { clearTenantRuntimeCaches, resolveRequestRuntime } = await import( "../../src/services/tenant-runtime" ); + import type { BindingResolver } from "../../src/services/binding-resolver"; function createDeferred() { @@ -121,11 +122,9 @@ describe("resolveRequestRuntime", () => { it("returns the base runtime on the bare domain", async () => { const baseConfig = createBaseRuntimeConfig(); - const result = await resolveRequestRuntime( - baseConfig, - new Request("https://linktree.com/"), - { bindingResolver: createMockBindingResolver() }, - ); + const result = await resolveRequestRuntime(baseConfig, new Request("https://linktree.com/"), { + bindingResolver: createMockBindingResolver(), + }); expect(result.config).toBe(baseConfig); expect(result.tenantAccountId).toBeNull(); @@ -677,17 +676,13 @@ describe("resolveRequestRuntime", () => { }, }); - await resolveRequestRuntime( - baseConfig, - new Request("https://alice.linktree.com/"), - { - bindingResolver: createMockBindingResolver({ - hostname: "alice.linktree.com", - allowUiOverrides: true, - allowSsr: true, - }), - }, - ); + await resolveRequestRuntime(baseConfig, new Request("https://alice.linktree.com/"), { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }); const refresh = createDeferred(); verifySriForUrlMock.mockImplementationOnce(() => refresh.promise); @@ -762,17 +757,13 @@ describe("resolveRequestRuntime", () => { }, }); - await resolveRequestRuntime( - baseConfig, - new Request("https://alice.linktree.com/"), - { - bindingResolver: createMockBindingResolver({ - hostname: "alice.linktree.com", - allowUiOverrides: true, - allowSsr: true, - }), - }, - ); + await resolveRequestRuntime(baseConfig, new Request("https://alice.linktree.com/"), { + bindingResolver: createMockBindingResolver({ + hostname: "alice.linktree.com", + allowUiOverrides: true, + allowSsr: true, + }), + }); const refresh = createDeferred(); verifySriForUrlMock.mockImplementationOnce(() => refresh.promise); diff --git a/package.json b/package.json index 073c58bc..1f52d071 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "test:api": "cd api && bun run test tests/integration/ tests/unit/", "test:integration": "cd api && bun run test tests/integration/", "test:e2e": "bun run --cwd host test:e2e", - "regression:start:dev": "BOS_NO_PERSIST_PORTS=1 bun packages/everything-dev/src/cli.ts dev --no-interactive --port 4100 --api-port 4101 --auth-port 4102 --ui-port 4103 --plugin-port-start 4110", + "regression:start:dev": "BOS_NO_PERSIST_PORTS=1 bun packages/everything-dev/src/cli.ts dev --no-interactive --ssr --port 4100 --api-port 4101 --auth-port 4102 --ui-port 4103 --plugin-port-start 4110", "regression:start:prod": "BOS_NO_PERSIST_PORTS=1 PORT=4100 bun ./node_modules/everything-dev/dist/cli.mjs start --no-interactive", "test:regression:http:dev": "REGRESSION_MODE=dev sh -lc 'cd tests/regression/http && go test ./... -count=1 -v -timeout 10m'", "test:regression:http:prod": "REGRESSION_MODE=prod sh -lc 'cd tests/regression/http && go test ./... -count=1 -v -timeout 10m'", diff --git a/packages/everything-dev/skills/api-and-auth/SKILL.md b/packages/everything-dev/skills/api-and-auth/SKILL.md index 06416252..e302ff66 100644 --- a/packages/everything-dev/skills/api-and-auth/SKILL.md +++ b/packages/everything-dev/skills/api-and-auth/SKILL.md @@ -356,6 +356,54 @@ export function createPluginsClient(result, context) { } ``` +### Tenant-Scoped Data + +A **tenant** is a deployment record (subdomain + NEAR account + UI/backend/SSR override +permissions) — distinct from an organization (a group of users) and from a user's own data. +If your plugin stores data that belongs to a specific tenant deployment (not just a user or +org), resolve the tenant and scope every query to it. + +The `api` plugin owns the `tenants` table and exposes public lookup routes (no auth required — +tenant lookups are read-only and safe to expose): + +```ts +// From any plugin, via pluginsClient.api (injected through withPlugins()) +const tenant = context.organization?.activeOrganizationId + ? await plugins.api().resolveTenantByOrgId({ orgId: context.organization.activeOrganizationId }) + : await plugins.api().resolveTenant({ accountId: context.near?.primaryAccountId ?? "" }); +``` + +Build a local `requireTenant` middleware in your own plugin (mirrors `requireOrganization`): + +```ts +const requireTenant = builder.middleware(async ({ context, next }) => { + const activeOrgId = context.organization?.activeOrganizationId; + const tenant = activeOrgId + ? await plugins.api().resolveTenantByOrgId({ orgId: activeOrgId }).catch(() => null) + : null; + if (!tenant) { + throw new ORPCError("FORBIDDEN", { message: "No tenant found for this organization" }); + } + return next({ context: { ...context, tenant } }); +}); + +builder.listReports.use(requireTenant).handler(async ({ context }) => { + return await services.reports.listByTenant(context.tenant.id); // always filtered +}); +``` + +**Row-level convention (interim isolation)**: add a `tenantId` column to any table holding +tenant-specific application data, resolved server-side and never trusted from client input — +same discipline the `tenants` table itself uses for `orgId` scoping. See +`plugins/_template/src/db/schema.ts` for a commented example table. + +**Forward path**: the target architecture (see `plans/beta-v2-tenants.md`) is per-tenant-per-plugin +Postgres schema isolation (`tenant__plugin_`, `search_path` injected per request). That +requires request-scoped DB access instead of the current initialize-time singleton pattern — a +larger change, only worth it once there's a real multi-tenant plugin ecosystem to isolate. The +`tenantId` column convention above is forward-compatible: when schema isolation lands, the column +becomes redundant and can be dropped without reworking query logic. + ## Generated Types See `references/generated-types.md` for the full table — files, contents, and regeneration triggers. diff --git a/packages/everything-dev/src/infra/planner.ts b/packages/everything-dev/src/infra/planner.ts index 2efb3939..c0dacc98 100644 --- a/packages/everything-dev/src/infra/planner.ts +++ b/packages/everything-dev/src/infra/planner.ts @@ -453,6 +453,9 @@ export function planInfra(input: InfraInput): Effect.Effect { const scriptMap = new Map(); for (const match of router.state.matches) { - const route = - ( - router as AnyRouter & { - routesById?: Record unknown } }>; - } - ).routesById?.[(match as { routeId: string }).routeId] ?? - (match as { route?: { options?: { head?: (...args: unknown[]) => unknown } } }).route; - const headFn = route?.options?.head; - if (!headFn) continue; + const matchData = match as { + meta?: HeadMeta[]; + links?: HeadLink[]; + headScripts?: HeadScript[]; + }; - try { - const headResult = (await headFn({ - loaderData: match.loaderData, - matches: router.state.matches, - match, - params: match.params, - })) as { - meta?: HeadMeta[]; - links?: HeadLink[]; - scripts?: HeadScript[]; - }; - - if (headResult?.meta) { - for (const meta of headResult.meta) { - metaMap.set(getMetaKey(meta), meta); - } + if (matchData.meta) { + for (const meta of matchData.meta) { + metaMap.set(getMetaKey(meta), meta); } - if (headResult?.links) { - for (const link of headResult.links) { - linkMap.set(getLinkKey(link), link); - } + } + if (matchData.links) { + for (const link of matchData.links) { + linkMap.set(getLinkKey(link), link); } - if (headResult?.scripts) { - for (const script of headResult.scripts) { - scriptMap.set(getScriptKey(script), script); - } + } + if (matchData.headScripts) { + for (const script of matchData.headScripts) { + scriptMap.set(getScriptKey(script), script); } - } catch (error) { - console.warn(`[collectHeadData] head() failed for ${match.routeId}:`, error); } } diff --git a/plugins/_template/src/db/schema.ts b/plugins/_template/src/db/schema.ts index b355bb35..51574d0f 100644 --- a/plugins/_template/src/db/schema.ts +++ b/plugins/_template/src/db/schema.ts @@ -14,3 +14,21 @@ export const things = pgTable( }, (table) => [index("things_type_idx").on(table.type)], ); + +// Tenant-scoped table convention (interim, row-level isolation): +// +// If your plugin stores data that belongs to a specific tenant deployment +// (not just a user or org), add a `tenantId` column and filter every query +// by it. Resolve the tenant via the API plugin's public lookup routes +// (`pluginsClient.api.resolveTenantByOrgId` / `resolveTenant`) and never +// trust a tenantId passed directly from client input. +// +// export const reports = pgTable("reports", { +// id: uuid("id").defaultRandom().primaryKey(), +// tenantId: text("tenant_id").notNull(), // resolved server-side, always filtered +// title: text("title").notNull(), +// createdAt: timestamp("created_at", { mode: "date", withTimezone: true }).defaultNow().notNull(), +// }, (table) => [index("reports_tenant_id_idx").on(table.tenantId)]); +// +// See the api-and-auth skill's "Tenant-Scoped Data" section for the full +// middleware pattern and the forward path to per-tenant schema isolation. diff --git a/tests/regression/browser/helpers/page-ready.ts b/tests/regression/browser/helpers/page-ready.ts index f508dec3..dab7f790 100644 --- a/tests/regression/browser/helpers/page-ready.ts +++ b/tests/regression/browser/helpers/page-ready.ts @@ -33,6 +33,24 @@ export async function waitForApp(page: Page): Promise { return typeof window.__RUNTIME_CONFIG__ !== "undefined"; }); expect(hasRuntimeConfig).toBeTruthy(); + + try { + await page.waitForFunction( + () => { + const p = (window as { __EVERYTHING_DEV_HYDRATE_PROMISE__?: Promise }) + .__EVERYTHING_DEV_HYDRATE_PROMISE__; + return p !== undefined; + }, + { timeout: 5000 }, + ); + await page.evaluate(async () => { + const p = (window as { __EVERYTHING_DEV_HYDRATE_PROMISE__?: Promise }) + .__EVERYTHING_DEV_HYDRATE_PROMISE__; + if (p) await p; + }); + } catch { + // Hydration promise never surfaced (e.g. client-shell fallback path) — proceed. + } } export function expectNoHydrationFailure(errors: PageErrors) { diff --git a/tests/regression/browser/helpers/seeded.ts b/tests/regression/browser/helpers/seeded.ts index 26c1efce..560e0aad 100644 --- a/tests/regression/browser/helpers/seeded.ts +++ b/tests/regression/browser/helpers/seeded.ts @@ -45,5 +45,5 @@ export async function verifyAuthenticated(page: Page) { await page.goto("/home", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(500); await page.waitForLoadState("networkidle"); - await expect(page.locator("button[title='menu']")).toBeVisible({ timeout: 10000 }); + await expect(page.locator("button[title='account menu']")).toBeVisible({ timeout: 10000 }); } diff --git a/tests/regression/browser/specs/auth-redirect.spec.ts b/tests/regression/browser/specs/auth-redirect.spec.ts index 94a79c21..f0b3aede 100644 --- a/tests/regression/browser/specs/auth-redirect.spec.ts +++ b/tests/regression/browser/specs/auth-redirect.spec.ts @@ -26,8 +26,8 @@ test.describe("Auth redirect", () => { expectNoHydrationFailure(pageErrors); }); - test("unauthenticated / redirects to /login", async ({ page }) => { - await page.goto("/", { waitUntil: "domcontentloaded" }); + test("unauthenticated /home redirects to /login", async ({ page }) => { + await page.goto("/home", { waitUntil: "domcontentloaded" }); await waitForApp(page); await page.waitForURL(/\/login/, { timeout: 15000 }); diff --git a/tests/regression/browser/specs/logout.spec.ts b/tests/regression/browser/specs/logout.spec.ts index c207bb95..cef410e9 100644 --- a/tests/regression/browser/specs/logout.spec.ts +++ b/tests/regression/browser/specs/logout.spec.ts @@ -8,7 +8,7 @@ test.describe("logout", () => { pageErrors = collectErrors(page); }); - test("sign out redirects to login and session is cleared", async ({ page }) => { + test("sign out lands on public page and session is cleared", async ({ page }) => { await page.goto("/login", { waitUntil: "domcontentloaded" }); await waitForApp(page); @@ -28,18 +28,20 @@ test.describe("logout", () => { await page.waitForURL(/\/home$/, { timeout: 15000 }); await page.waitForLoadState("networkidle"); await waitForApp(page); - await page.locator("button[title='menu']").click(); + await page.locator("button[title='account menu']").click(); const signOutItem = page.getByRole("menuitem", { name: "sign out" }); await expect(signOutItem).toBeVisible({ timeout: 5000 }); await signOutItem.click(); - await page.waitForURL(/\/login/, { timeout: 15000 }); - await expect(page.getByText("continue anonymously")).toBeVisible({ timeout: 10000 }); + await page.waitForURL(/\/$/, { timeout: 15000 }); + await expect(page.getByText("Get started")).toBeVisible({ timeout: 10000 }); + await expect(page.locator("button[title='account menu']")).toHaveCount(0); await page.reload({ waitUntil: "domcontentloaded" }); await waitForApp(page); - await expect(page.getByText("continue anonymously")).toBeVisible({ timeout: 10000 }); + await expect(page.getByText("Get started")).toBeVisible({ timeout: 10000 }); + await expect(page.locator("button[title='account menu']")).toHaveCount(0); expectNoHydrationFailure(pageErrors); }); diff --git a/tests/regression/browser/specs/theme.spec.ts b/tests/regression/browser/specs/theme.spec.ts index c8bd569a..0b9a3db8 100644 --- a/tests/regression/browser/specs/theme.spec.ts +++ b/tests/regression/browser/specs/theme.spec.ts @@ -49,19 +49,8 @@ test.describe("theme", () => { await expect(html).toHaveClass(/dark/); - const darkToggle = page.locator("button[aria-label='Switch to light theme']").first(); - await expect(darkToggle).toBeVisible({ timeout: 10000 }); - await darkToggle.click(); - - await expect(html).not.toHaveClass(/dark/); - - const storedAfterLight = await page.evaluate(() => localStorage.getItem("theme")); - expect(storedAfterLight).toBe("light"); - - await page.reload({ waitUntil: "domcontentloaded" }); - await waitForApp(page); - - await expect(html).not.toHaveClass(/dark/); + const storedAfterReload = await page.evaluate(() => localStorage.getItem("theme")); + expect(storedAfterReload).toBe("dark"); expectNoHydrationFailure(pageErrors); }); diff --git a/tests/regression/http/metadata_test.go b/tests/regression/http/metadata_test.go index 856cea48..7382a275 100644 --- a/tests/regression/http/metadata_test.go +++ b/tests/regression/http/metadata_test.go @@ -1,6 +1,7 @@ package regression import ( + "strings" "testing" "everything.dev/regression/http/internal/regtest" @@ -33,3 +34,30 @@ func TestRouteMetadata(t *testing.T) { t.Fatal("route HTML missing tag") } } + +func TestProfileMetadata(t *testing.T) { + const accountID = "root.near" + + client := regtest.NewCookieClient() + status, _, body := regtest.GetRaw(t, client, baseURL+"/"+accountID) + regtest.MustStatus(t, status, 200, body) + + if !regtest.HTMLContainsTitle(body) { + t.Fatal("profile HTML missing <title> tag") + } + if !strings.Contains(body, accountID) { + t.Fatalf("profile HTML missing account id %q", accountID) + } + + for _, property := range []string{"og:title", "og:description", "og:type"} { + if !regtest.HTMLContainsMetaProperty(body, property) { + t.Fatalf("profile HTML missing %s meta property", property) + } + } + + for _, name := range []string{"twitter:card", "twitter:title", "twitter:description"} { + if !regtest.HTMLContainsMetaName(body, name) { + t.Fatalf("profile HTML missing %s meta name", name) + } + } +} diff --git a/ui/rsbuild.config.ts b/ui/rsbuild.config.ts index 8a37905c..b829ffe5 100644 --- a/ui/rsbuild.config.ts +++ b/ui/rsbuild.config.ts @@ -260,7 +260,7 @@ function createServerConfig() { }, }, server: { - port: 3004, + port: Number(process.env.PORT) || 3004, printUrls: ({ urls }) => urls.filter((url) => url.includes("localhost")), headers: { "Access-Control-Allow-Origin": "*", diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 476a1331..f7bcfd5c 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -9,6 +9,7 @@ export { ConfirmDialog } from "./confirm-dialog"; export { EmptyState } from "./empty-state"; export { PageContainer } from "./layout/page-container"; export { OrgSwitcher } from "./org-switcher"; +export { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; export { Badge } from "./ui/badge"; export { Button } from "./ui/button"; export { diff --git a/ui/src/components/user-nav.tsx b/ui/src/components/user-nav.tsx index a77e1c08..5929d4c4 100644 --- a/ui/src/components/user-nav.tsx +++ b/ui/src/components/user-nav.tsx @@ -1,17 +1,18 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link, useNavigate, useRouter } from "@tanstack/react-router"; +import { Building2, Home, LogOut, Settings, User } from "lucide-react"; import { useMemo } from "react"; import type { Organization } from "@/app"; import { sessionQueryOptions, useAuthClient } from "@/app"; -import { OrgSwitcher } from "@/components"; +import { Avatar, AvatarFallback, AvatarImage, OrgSwitcher } from "@/components"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { getNearInitials, resolveNearImageUrl } from "@/lib/near-profile"; export function UserNav() { const auth = useAuthClient(); @@ -20,6 +21,8 @@ export function UserNav() { const router = useRouter(); const { data: session } = useQuery(sessionQueryOptions(auth)); const user = session?.user; + const nearAccountId = auth.near.getAccountId(); + const { data: organizations } = useQuery({ queryKey: ["organizations"], queryFn: async () => { @@ -35,6 +38,16 @@ export function UserNav() { return organizations?.find((org) => org.id === activeOrgId); }, [organizations, activeOrgId]); + const { data: nearProfile } = useQuery({ + queryKey: ["near-profile", nearAccountId], + queryFn: async () => { + const { data } = await auth.near.getProfile(nearAccountId ?? undefined); + return data ?? null; + }, + enabled: !!nearAccountId, + staleTime: 5 * 60 * 1000, + }); + const signOutMutation = useMutation({ mutationFn: async () => { const { error } = await auth.signOut(); @@ -71,6 +84,28 @@ export function UserNav() { await queryClient.invalidateQueries({ queryKey: ["organizations"] }); }; + const avatarSrc = resolveNearImageUrl(nearProfile?.image) ?? user.image ?? undefined; + const validEmail = !user.isAnonymous && user.email ? user.email : null; + const displayName = nearProfile?.name || user.name || nearAccountId || validEmail || "guest"; + const handle = nearAccountId || validEmail || "anonymous session"; + const showHandle = handle !== displayName; + const initials = getNearInitials(nearProfile?.name || user.name || nearAccountId); + + const identityContent = ( + <> + <Avatar className="size-9 shrink-0 ring-1 ring-border"> + {avatarSrc ? <AvatarImage src={avatarSrc} alt="" /> : null} + <AvatarFallback className="text-xs font-semibold"> + {initials || <User className="size-4" />} + </AvatarFallback> + </Avatar> + <div className="min-w-0 flex-1"> + <p className="truncate text-sm font-medium text-foreground">{displayName}</p> + {showHandle && <p className="truncate text-xs text-muted-foreground">{handle}</p>} + </div> + </> + ); + return ( <div className="flex items-center gap-2"> {organizations && organizations.length > 0 && ( @@ -85,30 +120,48 @@ export function UserNav() { <DropdownMenuTrigger asChild> <button type="button" - className="w-6 h-6 rounded-full! bg-foreground transition-all duration-200 ease-out hover:shadow-lg hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" - title="menu" - /> + aria-label={displayName} + className="rounded-full! ring-1 ring-border transition-transform duration-200 ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 hover:scale-105" + title="account menu" + > + <Avatar className="size-8"> + {avatarSrc ? <AvatarImage src={avatarSrc} alt="" /> : null} + <AvatarFallback className="text-xs font-semibold"> + {initials || <User className="size-4" />} + </AvatarFallback> + </Avatar> + </button> </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-56"> - <DropdownMenuLabel> - <div className="space-y-1"> - <p className="text-xs text-muted-foreground">signed in as</p> - <p className="truncate text-sm font-normal">{user.email || user.id}</p> - </div> - </DropdownMenuLabel> + <DropdownMenuContent align="end" className="w-64"> + <DropdownMenuItem asChild> + {nearAccountId ? ( + <Link to="/$accountId" params={{ accountId: nearAccountId }}> + {identityContent} + </Link> + ) : ( + <Link to="/settings/profile">{identityContent}</Link> + )} + </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem asChild> - <Link to="/home">workspace</Link> + <Link to="/home"> + <Home /> + workspace + </Link> </DropdownMenuItem> {activeOrg && ( <DropdownMenuItem asChild> <Link to="/organizations/$slug" params={{ slug: activeOrg.slug }}> + <Building2 /> {activeOrg.name} </Link> </DropdownMenuItem> )} <DropdownMenuItem asChild> - <Link to="/settings">settings</Link> + <Link to="/settings"> + <Settings /> + settings + </Link> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem @@ -119,6 +172,7 @@ export function UserNav() { }} disabled={signOutMutation.isPending} > + <LogOut /> {signOutMutation.isPending ? "signing out..." : "sign out"} </DropdownMenuItem> </DropdownMenuContent> diff --git a/ui/src/lib/near-profile.ts b/ui/src/lib/near-profile.ts new file mode 100644 index 00000000..deb01fa8 --- /dev/null +++ b/ui/src/lib/near-profile.ts @@ -0,0 +1,17 @@ +export interface NearProfileImage { + url?: string; + ipfs_cid?: string; +} + +export function resolveNearImageUrl(image?: NearProfileImage | null): string | undefined { + if (image?.url) return image.url; + if (image?.ipfs_cid) return `https://ipfs.near.social/ipfs/${image.ipfs_cid}`; + return undefined; +} + +export function getNearInitials(name?: string | null): string { + if (!name) return ""; + const parts = name.trim().split(/\s+/).filter(Boolean); + if (parts.length >= 2) return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); + return name.trim().slice(0, 2).toUpperCase(); +} diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index c7659df3..7bb4ab92 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -17,8 +17,10 @@ import { Route as LayoutAppsIndexRouteImport } from './routes/_layout/apps/index import { Route as LayoutPublicIndexRouteImport } from './routes/_layout/_public/index' import { Route as LayoutThingsLiveRouteImport } from './routes/_layout/things/live' import { Route as LayoutThingsThingIdRouteImport } from './routes/_layout/things/$thingId' +import { Route as LayoutPublicSkillRouteImport } from './routes/_layout/_public/skill' import { Route as LayoutPublicLoginRouteImport } from './routes/_layout/_public/login' import { Route as LayoutPublicAboutRouteImport } from './routes/_layout/_public/about' +import { Route as LayoutPublicAccountIdRouteImport } from './routes/_layout/_public/$accountId' import { Route as LayoutAuthenticatedSettingsRouteImport } from './routes/_layout/_authenticated/settings' import { Route as LayoutAuthenticatedHomeRouteImport } from './routes/_layout/_authenticated/home' import { Route as LayoutAuthenticatedAdminRouteImport } from './routes/_layout/_authenticated/admin' @@ -75,6 +77,11 @@ const LayoutThingsThingIdRoute = LayoutThingsThingIdRouteImport.update({ path: '/things/$thingId', getParentRoute: () => LayoutRoute, } as any) +const LayoutPublicSkillRoute = LayoutPublicSkillRouteImport.update({ + id: '/skill', + path: '/skill', + getParentRoute: () => LayoutPublicRoute, +} as any) const LayoutPublicLoginRoute = LayoutPublicLoginRouteImport.update({ id: '/login', path: '/login', @@ -85,6 +92,11 @@ const LayoutPublicAboutRoute = LayoutPublicAboutRouteImport.update({ path: '/about', getParentRoute: () => LayoutPublicRoute, } as any) +const LayoutPublicAccountIdRoute = LayoutPublicAccountIdRouteImport.update({ + id: '/$accountId', + path: '/$accountId', + getParentRoute: () => LayoutPublicRoute, +} as any) const LayoutAuthenticatedSettingsRoute = LayoutAuthenticatedSettingsRouteImport.update({ id: '/settings', @@ -198,8 +210,10 @@ export interface FileRoutesByFullPath { '/admin': typeof LayoutAuthenticatedAdminRouteWithChildren '/home': typeof LayoutAuthenticatedHomeRoute '/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren + '/$accountId': typeof LayoutPublicAccountIdRoute '/about': typeof LayoutPublicAboutRoute '/login': typeof LayoutPublicLoginRoute + '/skill': typeof LayoutPublicSkillRoute '/things/$thingId': typeof LayoutThingsThingIdRoute '/things/live': typeof LayoutThingsLiveRoute '/apps/': typeof LayoutAppsIndexRoute @@ -223,8 +237,10 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof LayoutPublicIndexRoute '/home': typeof LayoutAuthenticatedHomeRoute + '/$accountId': typeof LayoutPublicAccountIdRoute '/about': typeof LayoutPublicAboutRoute '/login': typeof LayoutPublicLoginRoute + '/skill': typeof LayoutPublicSkillRoute '/things/$thingId': typeof LayoutThingsThingIdRoute '/things/live': typeof LayoutThingsLiveRoute '/apps': typeof LayoutAppsIndexRoute @@ -253,8 +269,10 @@ export interface FileRoutesById { '/_layout/_authenticated/admin': typeof LayoutAuthenticatedAdminRouteWithChildren '/_layout/_authenticated/home': typeof LayoutAuthenticatedHomeRoute '/_layout/_authenticated/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren + '/_layout/_public/$accountId': typeof LayoutPublicAccountIdRoute '/_layout/_public/about': typeof LayoutPublicAboutRoute '/_layout/_public/login': typeof LayoutPublicLoginRoute + '/_layout/_public/skill': typeof LayoutPublicSkillRoute '/_layout/things/$thingId': typeof LayoutThingsThingIdRoute '/_layout/things/live': typeof LayoutThingsLiveRoute '/_layout/_public/': typeof LayoutPublicIndexRoute @@ -283,8 +301,10 @@ export interface FileRouteTypes { | '/admin' | '/home' | '/settings' + | '/$accountId' | '/about' | '/login' + | '/skill' | '/things/$thingId' | '/things/live' | '/apps/' @@ -308,8 +328,10 @@ export interface FileRouteTypes { to: | '/' | '/home' + | '/$accountId' | '/about' | '/login' + | '/skill' | '/things/$thingId' | '/things/live' | '/apps' @@ -337,8 +359,10 @@ export interface FileRouteTypes { | '/_layout/_authenticated/admin' | '/_layout/_authenticated/home' | '/_layout/_authenticated/settings' + | '/_layout/_public/$accountId' | '/_layout/_public/about' | '/_layout/_public/login' + | '/_layout/_public/skill' | '/_layout/things/$thingId' | '/_layout/things/live' | '/_layout/_public/' @@ -423,6 +447,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutThingsThingIdRouteImport parentRoute: typeof LayoutRoute } + '/_layout/_public/skill': { + id: '/_layout/_public/skill' + path: '/skill' + fullPath: '/skill' + preLoaderRoute: typeof LayoutPublicSkillRouteImport + parentRoute: typeof LayoutPublicRoute + } '/_layout/_public/login': { id: '/_layout/_public/login' path: '/login' @@ -437,6 +468,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicAboutRouteImport parentRoute: typeof LayoutPublicRoute } + '/_layout/_public/$accountId': { + id: '/_layout/_public/$accountId' + path: '/$accountId' + fullPath: '/$accountId' + preLoaderRoute: typeof LayoutPublicAccountIdRouteImport + parentRoute: typeof LayoutPublicRoute + } '/_layout/_authenticated/settings': { id: '/_layout/_authenticated/settings' path: '/settings' @@ -642,14 +680,18 @@ const LayoutAuthenticatedRouteWithChildren = LayoutAuthenticatedRoute._addFileChildren(LayoutAuthenticatedRouteChildren) interface LayoutPublicRouteChildren { + LayoutPublicAccountIdRoute: typeof LayoutPublicAccountIdRoute LayoutPublicAboutRoute: typeof LayoutPublicAboutRoute LayoutPublicLoginRoute: typeof LayoutPublicLoginRoute + LayoutPublicSkillRoute: typeof LayoutPublicSkillRoute LayoutPublicIndexRoute: typeof LayoutPublicIndexRoute } const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { + LayoutPublicAccountIdRoute: LayoutPublicAccountIdRoute, LayoutPublicAboutRoute: LayoutPublicAboutRoute, LayoutPublicLoginRoute: LayoutPublicLoginRoute, + LayoutPublicSkillRoute: LayoutPublicSkillRoute, LayoutPublicIndexRoute: LayoutPublicIndexRoute, } diff --git a/ui/src/routes/__root.tsx b/ui/src/routes/__root.tsx index 20446243..ea983de9 100644 --- a/ui/src/routes/__root.tsx +++ b/ui/src/routes/__root.tsx @@ -16,7 +16,7 @@ import { Scripts, } from "@tanstack/react-router"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; -import { getRemoteScripts } from "everything-dev/ui/head"; +import { getRemoteScripts, getThemeInitScript } from "everything-dev/ui/head"; import { getSocialImageMeta } from "everything-dev/ui/metadata"; import { ThemeProvider } from "next-themes"; import type { RouterContext } from "@/app"; @@ -123,6 +123,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({ ...(typeof window === "undefined" ? [{ children: "window.__EVERYTHING_DEV_SSR__=true" }] : []), + getThemeInitScript(), ...getRemoteScripts({ runtimeConfig: runtimeConfig ?? undefined, containerName: "ui", diff --git a/ui/src/routes/_layout.tsx b/ui/src/routes/_layout.tsx index c0a44b0a..3f68e524 100644 --- a/ui/src/routes/_layout.tsx +++ b/ui/src/routes/_layout.tsx @@ -10,6 +10,9 @@ export const Route = createFileRoute("/_layout")({ function Layout() { const isNavigating = useRouterState({ select: (s) => s.status === "pending" }); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + const [betaBannerDismissed, setBetaBannerDismissed] = useState(() => { if (typeof window === "undefined") return false; return localStorage.getItem("beta-banner-dismissed") === "true"; @@ -45,7 +48,7 @@ function Layout() { </div> )} - {isNavigating && ( + {mounted && isNavigating && ( <div className="fixed top-0 left-0 right-0 h-[2px] z-50 overflow-hidden pointer-events-none"> <div className="h-full bg-foreground animate-progress-bar" style={{ width: "100%" }} /> </div> diff --git a/ui/src/routes/_layout/_authenticated/home.tsx b/ui/src/routes/_layout/_authenticated/home.tsx index e28c6796..1fccff0e 100644 --- a/ui/src/routes/_layout/_authenticated/home.tsx +++ b/ui/src/routes/_layout/_authenticated/home.tsx @@ -2,12 +2,29 @@ import { useQuery } from "@tanstack/react-query"; import { createFileRoute, Link } from "@tanstack/react-router"; import { Home as HomeIcon, Settings } from "lucide-react"; import { useMemo } from "react"; -import { type Passkey, type SessionData, sessionQueryOptions, useAuthClient } from "@/app"; +import { + getAccount, + type Passkey, + type SessionData, + sessionQueryOptions, + useAuthClient, +} from "@/app"; import { Card } from "@/components"; import { PageContainer } from "@/components/layout/page-container"; import { InfoRow } from "@/components/ui/info-row"; export const Route = createFileRoute("/_layout/_authenticated/home")({ + beforeLoad: async ({ context }) => { + const { apiClient, runtimeConfig } = context; + const accountId = getAccount(runtimeConfig); + let tenant: Awaited<ReturnType<typeof apiClient.resolveTenant>> | null = null; + try { + tenant = await apiClient.resolveTenant({ accountId }); + } catch { + tenant = null; + } + return { tenant }; + }, head: () => ({ meta: [{ title: "Workspace | app" }, { name: "description", content: "Your workspace." }], }), diff --git a/ui/src/routes/_layout/_public/$accountId.tsx b/ui/src/routes/_layout/_public/$accountId.tsx new file mode 100644 index 00000000..ff1d96e1 --- /dev/null +++ b/ui/src/routes/_layout/_public/$accountId.tsx @@ -0,0 +1,197 @@ +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import { getSocialImageMeta } from "everything-dev/ui/metadata"; +import { ExternalLink, Globe, User } from "lucide-react"; +import { useApiClient, useAuthClient } from "@/app"; +import { Avatar, AvatarFallback, AvatarImage, Badge, PageContainer } from "@/components"; +import { getNearInitials, resolveNearImageUrl } from "@/lib/near-profile"; + +export const Route = createFileRoute("/_layout/_public/$accountId")({ + loader: async ({ params, context }) => { + const { queryClient, authClient, apiClient, runtimeConfig } = context; + const accountId = params.accountId; + + await Promise.all([ + queryClient.prefetchQuery({ + queryKey: ["near-profile", accountId], + queryFn: async () => { + const { data } = await authClient.near.getProfile(accountId); + return data ?? null; + }, + staleTime: 5 * 60 * 1000, + }), + queryClient.prefetchQuery({ + queryKey: ["apps-account", accountId], + queryFn: () => apiClient.apps.getRegistryAppsByAccount({ accountId }), + staleTime: 30_000, + }), + ]); + + return { accountId, hostUrl: runtimeConfig?.hostUrl ?? "" }; + }, + head: ({ loaderData, params }) => { + const accountId = params.accountId; + const hostUrl = (loaderData?.hostUrl ?? "").replace(/\/$/, ""); + const siteUrl = hostUrl ? `${hostUrl}/${accountId}` : ""; + const title = `${accountId} | everything.dev`; + const description = `${accountId}'s public profile on everything.dev.`; + + return { + meta: [ + { title }, + { name: "description", content: description }, + ...getSocialImageMeta({ + imageUrl: hostUrl ? `${hostUrl}/metadata.png` : "/metadata.png", + title, + description, + siteName: "everything.dev", + siteUrl, + type: "profile", + alt: description, + }), + ], + }; + }, + component: AccountProfilePage, +}); + +function AccountProfilePage() { + const { accountId } = Route.useLoaderData(); + const authClient = useAuthClient(); + const apiClient = useApiClient(); + + const { data: profile } = useQuery({ + queryKey: ["near-profile", accountId], + queryFn: async () => { + const { data } = await authClient.near.getProfile(accountId); + return data ?? null; + }, + staleTime: 5 * 60 * 1000, + }); + + const { data: appsData } = useQuery({ + queryKey: ["apps-account", accountId], + queryFn: () => apiClient.apps.getRegistryAppsByAccount({ accountId }), + staleTime: 30_000, + }); + + const apps = appsData?.data ?? []; + const backgroundUrl = resolveNearImageUrl(profile?.backgroundImage); + const avatarUrl = resolveNearImageUrl(profile?.image); + const displayName = profile?.name || accountId; + const initials = getNearInitials(profile?.name || accountId); + const linktree = profile?.linktree ? Object.entries(profile.linktree) : []; + + return ( + <PageContainer variant="default"> + <div className="space-y-6"> + <div className="overflow-hidden rounded-[12px] border border-border bg-card"> + <div + className="h-32 sm:h-44 w-full bg-muted" + style={ + backgroundUrl + ? { + backgroundImage: `url(${backgroundUrl})`, + backgroundSize: "cover", + backgroundPosition: "center", + } + : undefined + } + /> + <div className="px-6 pb-6"> + <Avatar className="-mt-10 size-20 border-4 border-card ring-1 ring-border bg-card"> + {avatarUrl ? <AvatarImage src={avatarUrl} alt="" /> : null} + <AvatarFallback className="text-xl font-semibold"> + {initials || <User className="size-8" />} + </AvatarFallback> + </Avatar> + + <div className="mt-3 space-y-1"> + <h1 className="text-xl font-bold text-foreground">{displayName}</h1> + <p className="font-mono text-sm text-muted-foreground">{accountId}</p> + </div> + + {profile?.description && ( + <p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted-foreground"> + {profile.description} + </p> + )} + + {linktree.length > 0 && ( + <div className="mt-4 flex flex-wrap gap-2"> + {linktree.map(([label, url]) => ( + <a + key={label} + href={url} + target="_blank" + rel="noopener noreferrer" + className="inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-3 py-1 text-xs font-medium text-foreground transition-colors hover:bg-border" + > + <Globe className="size-3" /> + {label} + </a> + ))} + </div> + )} + </div> + </div> + + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <h2 className="text-[11px] font-bold uppercase tracking-wider text-muted-foreground"> + Published Gateways + </h2> + {apps.length > 0 && ( + <Link + to="/apps/$accountId" + params={{ accountId }} + className="text-xs font-medium text-muted-foreground hover:text-foreground transition-colors" + > + view all + </Link> + )} + </div> + + {apps.length === 0 ? ( + <div className="rounded-[12px] border border-border bg-card px-6 py-10 text-center text-sm text-muted-foreground"> + No published gateways for{" "} + <span className="font-mono text-foreground">{accountId}</span>. + </div> + ) : ( + <div className="divide-y divide-border overflow-hidden rounded-[12px] border border-border bg-card"> + {apps.slice(0, 5).map((app) => ( + <Link + key={app.gatewayId} + to="/apps/$accountId/$gatewayId" + params={{ accountId, gatewayId: app.gatewayId }} + className="flex items-center justify-between gap-4 px-4 py-3 transition-colors hover:bg-muted/40" + > + <div className="min-w-0 space-y-0.5"> + <div className="flex items-center gap-1.5"> + <span + className={`inline-block h-1.5 w-1.5 shrink-0 rounded-full ${ + app.status === "ready" ? "bg-green-500" : "bg-destructive" + }`} + /> + <span className="truncate font-mono text-sm font-semibold text-foreground"> + {app.metadata?.title ?? app.gatewayId} + </span> + </div> + {app.domain && ( + <Badge variant="secondary" className="font-mono text-[10px]"> + {app.domain} + </Badge> + )} + </div> + {app.openUrl && ( + <ExternalLink className="size-3.5 shrink-0 text-muted-foreground" /> + )} + </Link> + ))} + </div> + )} + </div> + </div> + </PageContainer> + ); +} diff --git a/ui/src/routes/_layout/_public/skill.tsx b/ui/src/routes/_layout/_public/skill.tsx new file mode 100644 index 00000000..d2f83c68 --- /dev/null +++ b/ui/src/routes/_layout/_public/skill.tsx @@ -0,0 +1,126 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { Check, Copy, ExternalLink, FileText } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { getAccount, getActiveRuntime, getAppName } from "@/app"; +import { PageContainer } from "@/components/layout/page-container"; +import { Button } from "@/components/ui/button"; +import { Markdown } from "@/components/ui/markdown"; + +const INTENT_REGISTRY_URL = "https://tanstack.com/intent/registry/everything-dev"; + +export const Route = createFileRoute("/_layout/_public/skill")({ + loader: async ({ context }) => { + const runtimeConfig = context.runtimeConfig; + + const skill = await fetch("/skill.md") + .then(async (response) => { + if (!response.ok) { + throw new Error(`Failed to load skill: ${response.status}`); + } + + return response.text(); + }) + .catch(() => null); + + return { + runtimeConfig, + skill, + intentRegistryUrl: INTENT_REGISTRY_URL, + }; + }, + head: () => ({ + meta: [ + { title: "Skill | app" }, + { + name: "description", + content: "Agent-oriented instructions for running, editing, and publishing this runtime.", + }, + ], + }), + component: SkillPage, +}); + +function SkillPage() { + const { skill, runtimeConfig, intentRegistryUrl } = Route.useLoaderData(); + const runtime = getActiveRuntime(runtimeConfig); + const account = getAccount(runtimeConfig); + const appName = getAppName(runtimeConfig); + const [copied, setCopied] = useState(false); + + const accountId = runtime?.accountId ?? account; + + const handleCopy = async () => { + if (!skill) { + toast.error("Skill prompt unavailable"); + return; + } + + await navigator.clipboard.writeText(skill); + setCopied(true); + toast.success("Skill prompt copied"); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + <PageContainer variant="default"> + <div className="space-y-4"> + <div className="rounded-[12px] border border-border bg-card p-6 space-y-4"> + <div className="flex items-start justify-between gap-4 flex-wrap"> + <div className="flex items-center gap-3 min-w-0"> + <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px] bg-foreground text-background"> + <FileText size={18} /> + </div> + <div className="min-w-0"> + <div className="flex items-center gap-2 flex-wrap"> + <span className="text-xs font-mono text-muted-foreground">{accountId}</span> + <span className="text-muted-foreground">/</span> + <span className="text-base font-semibold text-foreground">{appName}</span> + </div> + <p className="mt-1 text-sm text-muted-foreground"> + Agent-ready prompt for TanStack Intent, local development, UI changes, and publish + flow. + </p> + </div> + </div> + + <div className="flex flex-wrap items-center gap-2"> + <Button variant="outline" onClick={handleCopy} disabled={!skill}> + {copied ? <Check size={14} /> : <Copy size={14} />} + {copied ? "Copied" : "Copy prompt"} + </Button> + <Button variant="outline" asChild> + <a href="/skill.md" target="_blank" rel="noopener noreferrer"> + <ExternalLink size={14} /> + raw skill.md + </a> + </Button> + <Button asChild> + <a href={intentRegistryUrl} target="_blank" rel="noopener noreferrer"> + <ExternalLink size={14} /> + TanStack Intent + </a> + </Button> + </div> + </div> + + <div className="rounded-[8px] border border-border bg-muted px-3.5 py-3 text-sm text-muted-foreground"> + Best entry points: `npx @tanstack/intent@latest load everything-dev`, `/skill.md`, and + the registry page above. + </div> + </div> + + {skill ? ( + <div className="rounded-[12px] border border-border bg-card p-8"> + <Markdown content={skill} /> + </div> + ) : ( + <div className="flex flex-col items-center justify-center gap-3 rounded-[12px] border border-border bg-card px-8 py-16 text-muted-foreground"> + <FileText size={32} className="text-border" /> + <p className="text-sm text-muted-foreground">Skill prompt unavailable.</p> + </div> + )} + </div> + </PageContainer> + ); +} From daee1041afa7a4de51e7f9283018601fd3040ba8 Mon Sep 17 00:00:00 2001 From: Elliot Braem <elliot@ejlbraem.com> Date: Thu, 13 Aug 2026 17:41:36 -0500 Subject: [PATCH 05/24] refactor(ui): reorganize routes into mount-point layouts, rename home to dashboard - add _admin pathless layout gating on admin role; tenant admin dashboard and system pages render as children via Outlet - rename authenticated /home route to /dashboard; update sidebar, mobile tabs, user nav, and login redirect fallbacks - move apps and things routes under the public layout - changeset: ui-layout-mounts --- .changeset/ui-layout-mounts.md | 9 + ui/src/components/user-nav.tsx | 2 +- ui/src/routeTree.gen.ts | 421 +++++++++--------- ui/src/routes/_layout/_admin.tsx | 62 +++ .../{_authenticated => _admin}/admin.tsx | 54 +-- ui/src/routes/_layout/_admin/admin/index.tsx | 152 +++++++ ui/src/routes/_layout/_admin/admin/system.tsx | 70 +++ ui/src/routes/_layout/_authenticated.tsx | 4 +- .../_layout/_authenticated/admin/index.tsx | 104 ----- .../_layout/_authenticated/admin/system.tsx | 73 --- .../{home.tsx => dashboard.tsx} | 2 +- .../apps/$accountId/$gatewayId.tsx | 2 +- .../{ => _public}/apps/$accountId/index.tsx | 2 +- .../_layout/{ => _public}/apps/index.tsx | 2 +- ui/src/routes/_layout/_public/login.tsx | 6 +- .../_layout/{ => _public}/things/$thingId.tsx | 2 +- .../_layout/{ => _public}/things/index.tsx | 2 +- .../_layout/{ => _public}/things/live.tsx | 2 +- 18 files changed, 532 insertions(+), 439 deletions(-) create mode 100644 .changeset/ui-layout-mounts.md create mode 100644 ui/src/routes/_layout/_admin.tsx rename ui/src/routes/_layout/{_authenticated => _admin}/admin.tsx (65%) create mode 100644 ui/src/routes/_layout/_admin/admin/index.tsx create mode 100644 ui/src/routes/_layout/_admin/admin/system.tsx delete mode 100644 ui/src/routes/_layout/_authenticated/admin/index.tsx delete mode 100644 ui/src/routes/_layout/_authenticated/admin/system.tsx rename ui/src/routes/_layout/_authenticated/{home.tsx => dashboard.tsx} (98%) rename ui/src/routes/_layout/{ => _public}/apps/$accountId/$gatewayId.tsx (99%) rename ui/src/routes/_layout/{ => _public}/apps/$accountId/index.tsx (99%) rename ui/src/routes/_layout/{ => _public}/apps/index.tsx (99%) rename ui/src/routes/_layout/{ => _public}/things/$thingId.tsx (98%) rename ui/src/routes/_layout/{ => _public}/things/index.tsx (97%) rename ui/src/routes/_layout/{ => _public}/things/live.tsx (98%) diff --git a/.changeset/ui-layout-mounts.md b/.changeset/ui-layout-mounts.md new file mode 100644 index 00000000..b9e8dd19 --- /dev/null +++ b/.changeset/ui-layout-mounts.md @@ -0,0 +1,9 @@ +--- +"ui": minor +--- + +Reorganize UI routes into mount-point layouts and rename the authenticated workspace. + +- Move admin routes under a new `/_layout/_admin` pathless layout that gates on the admin role and redirects non-admins to `/dashboard`. The tenant admin dashboard (`admin/admin/index.tsx`) and system page (`admin/admin/system.tsx`) now render as children of the admin layout through an `Outlet`. +- Rename the authenticated `/home` route to `/dashboard`, updating the sidebar, mobile tab bar, user nav, and login redirect fallbacks. +- Move the apps and things routes under the public layout (`_layout/_public/apps`, `_layout/_public/things`) so they render inside the shared public shell instead of the top-level layout. diff --git a/ui/src/components/user-nav.tsx b/ui/src/components/user-nav.tsx index 5929d4c4..ba6a6107 100644 --- a/ui/src/components/user-nav.tsx +++ b/ui/src/components/user-nav.tsx @@ -144,7 +144,7 @@ export function UserNav() { </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem asChild> - <Link to="/home"> + <Link to="/dashboard"> <Home /> workspace </Link> diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 7bb4ab92..359bd53d 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -12,23 +12,22 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LayoutRouteImport } from './routes/_layout' import { Route as LayoutPublicRouteImport } from './routes/_layout/_public' import { Route as LayoutAuthenticatedRouteImport } from './routes/_layout/_authenticated' -import { Route as LayoutThingsIndexRouteImport } from './routes/_layout/things/index' -import { Route as LayoutAppsIndexRouteImport } from './routes/_layout/apps/index' +import { Route as LayoutAdminRouteImport } from './routes/_layout/_admin' import { Route as LayoutPublicIndexRouteImport } from './routes/_layout/_public/index' -import { Route as LayoutThingsLiveRouteImport } from './routes/_layout/things/live' -import { Route as LayoutThingsThingIdRouteImport } from './routes/_layout/things/$thingId' import { Route as LayoutPublicSkillRouteImport } from './routes/_layout/_public/skill' import { Route as LayoutPublicLoginRouteImport } from './routes/_layout/_public/login' import { Route as LayoutPublicAboutRouteImport } from './routes/_layout/_public/about' import { Route as LayoutPublicAccountIdRouteImport } from './routes/_layout/_public/$accountId' import { Route as LayoutAuthenticatedSettingsRouteImport } from './routes/_layout/_authenticated/settings' -import { Route as LayoutAuthenticatedHomeRouteImport } from './routes/_layout/_authenticated/home' -import { Route as LayoutAuthenticatedAdminRouteImport } from './routes/_layout/_authenticated/admin' -import { Route as LayoutAppsAccountIdIndexRouteImport } from './routes/_layout/apps/$accountId/index' +import { Route as LayoutAuthenticatedDashboardRouteImport } from './routes/_layout/_authenticated/dashboard' +import { Route as LayoutAdminAdminRouteImport } from './routes/_layout/_admin/admin' +import { Route as LayoutPublicThingsIndexRouteImport } from './routes/_layout/_public/things/index' +import { Route as LayoutPublicAppsIndexRouteImport } from './routes/_layout/_public/apps/index' import { Route as LayoutAuthenticatedSettingsIndexRouteImport } from './routes/_layout/_authenticated/settings/index' import { Route as LayoutAuthenticatedOrganizationsIndexRouteImport } from './routes/_layout/_authenticated/organizations/index' -import { Route as LayoutAuthenticatedAdminIndexRouteImport } from './routes/_layout/_authenticated/admin/index' -import { Route as LayoutAppsAccountIdGatewayIdRouteImport } from './routes/_layout/apps/$accountId/$gatewayId' +import { Route as LayoutAdminAdminIndexRouteImport } from './routes/_layout/_admin/admin/index' +import { Route as LayoutPublicThingsLiveRouteImport } from './routes/_layout/_public/things/live' +import { Route as LayoutPublicThingsThingIdRouteImport } from './routes/_layout/_public/things/$thingId' import { Route as LayoutAuthenticatedThingsNewRouteImport } from './routes/_layout/_authenticated/things/new' import { Route as LayoutAuthenticatedTenantNewRouteImport } from './routes/_layout/_authenticated/tenant/new' import { Route as LayoutAuthenticatedTenantTenantIdRouteImport } from './routes/_layout/_authenticated/tenant/$tenantId' @@ -37,8 +36,10 @@ import { Route as LayoutAuthenticatedSettingsProfileRouteImport } from './routes import { Route as LayoutAuthenticatedSettingsAuthMethodsRouteImport } from './routes/_layout/_authenticated/settings/auth-methods' import { Route as LayoutAuthenticatedOrganizationsNewRouteImport } from './routes/_layout/_authenticated/organizations/new' import { Route as LayoutAuthenticatedOrganizationsSlugRouteImport } from './routes/_layout/_authenticated/organizations/$slug' -import { Route as LayoutAuthenticatedAdminSystemRouteImport } from './routes/_layout/_authenticated/admin/system' import { Route as LayoutAuthenticatedAcceptInvitationIdRouteImport } from './routes/_layout/_authenticated/accept-invitation.$id' +import { Route as LayoutAdminAdminSystemRouteImport } from './routes/_layout/_admin/admin/system' +import { Route as LayoutPublicAppsAccountIdIndexRouteImport } from './routes/_layout/_public/apps/$accountId/index' +import { Route as LayoutPublicAppsAccountIdGatewayIdRouteImport } from './routes/_layout/_public/apps/$accountId/$gatewayId' const LayoutRoute = LayoutRouteImport.update({ id: '/_layout', @@ -52,14 +53,8 @@ const LayoutAuthenticatedRoute = LayoutAuthenticatedRouteImport.update({ id: '/_authenticated', getParentRoute: () => LayoutRoute, } as any) -const LayoutThingsIndexRoute = LayoutThingsIndexRouteImport.update({ - id: '/things/', - path: '/things/', - getParentRoute: () => LayoutRoute, -} as any) -const LayoutAppsIndexRoute = LayoutAppsIndexRouteImport.update({ - id: '/apps/', - path: '/apps/', +const LayoutAdminRoute = LayoutAdminRouteImport.update({ + id: '/_admin', getParentRoute: () => LayoutRoute, } as any) const LayoutPublicIndexRoute = LayoutPublicIndexRouteImport.update({ @@ -67,16 +62,6 @@ const LayoutPublicIndexRoute = LayoutPublicIndexRouteImport.update({ path: '/', getParentRoute: () => LayoutPublicRoute, } as any) -const LayoutThingsLiveRoute = LayoutThingsLiveRouteImport.update({ - id: '/things/live', - path: '/things/live', - getParentRoute: () => LayoutRoute, -} as any) -const LayoutThingsThingIdRoute = LayoutThingsThingIdRouteImport.update({ - id: '/things/$thingId', - path: '/things/$thingId', - getParentRoute: () => LayoutRoute, -} as any) const LayoutPublicSkillRoute = LayoutPublicSkillRouteImport.update({ id: '/skill', path: '/skill', @@ -103,23 +88,27 @@ const LayoutAuthenticatedSettingsRoute = path: '/settings', getParentRoute: () => LayoutAuthenticatedRoute, } as any) -const LayoutAuthenticatedHomeRoute = LayoutAuthenticatedHomeRouteImport.update({ - id: '/home', - path: '/home', - getParentRoute: () => LayoutAuthenticatedRoute, -} as any) -const LayoutAuthenticatedAdminRoute = - LayoutAuthenticatedAdminRouteImport.update({ - id: '/admin', - path: '/admin', +const LayoutAuthenticatedDashboardRoute = + LayoutAuthenticatedDashboardRouteImport.update({ + id: '/dashboard', + path: '/dashboard', getParentRoute: () => LayoutAuthenticatedRoute, } as any) -const LayoutAppsAccountIdIndexRoute = - LayoutAppsAccountIdIndexRouteImport.update({ - id: '/apps/$accountId/', - path: '/apps/$accountId/', - getParentRoute: () => LayoutRoute, - } as any) +const LayoutAdminAdminRoute = LayoutAdminAdminRouteImport.update({ + id: '/admin', + path: '/admin', + getParentRoute: () => LayoutAdminRoute, +} as any) +const LayoutPublicThingsIndexRoute = LayoutPublicThingsIndexRouteImport.update({ + id: '/things/', + path: '/things/', + getParentRoute: () => LayoutPublicRoute, +} as any) +const LayoutPublicAppsIndexRoute = LayoutPublicAppsIndexRouteImport.update({ + id: '/apps/', + path: '/apps/', + getParentRoute: () => LayoutPublicRoute, +} as any) const LayoutAuthenticatedSettingsIndexRoute = LayoutAuthenticatedSettingsIndexRouteImport.update({ id: '/', @@ -132,17 +121,21 @@ const LayoutAuthenticatedOrganizationsIndexRoute = path: '/organizations/', getParentRoute: () => LayoutAuthenticatedRoute, } as any) -const LayoutAuthenticatedAdminIndexRoute = - LayoutAuthenticatedAdminIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => LayoutAuthenticatedAdminRoute, - } as any) -const LayoutAppsAccountIdGatewayIdRoute = - LayoutAppsAccountIdGatewayIdRouteImport.update({ - id: '/apps/$accountId/$gatewayId', - path: '/apps/$accountId/$gatewayId', - getParentRoute: () => LayoutRoute, +const LayoutAdminAdminIndexRoute = LayoutAdminAdminIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => LayoutAdminAdminRoute, +} as any) +const LayoutPublicThingsLiveRoute = LayoutPublicThingsLiveRouteImport.update({ + id: '/things/live', + path: '/things/live', + getParentRoute: () => LayoutPublicRoute, +} as any) +const LayoutPublicThingsThingIdRoute = + LayoutPublicThingsThingIdRouteImport.update({ + id: '/things/$thingId', + path: '/things/$thingId', + getParentRoute: () => LayoutPublicRoute, } as any) const LayoutAuthenticatedThingsNewRoute = LayoutAuthenticatedThingsNewRouteImport.update({ @@ -192,34 +185,41 @@ const LayoutAuthenticatedOrganizationsSlugRoute = path: '/organizations/$slug', getParentRoute: () => LayoutAuthenticatedRoute, } as any) -const LayoutAuthenticatedAdminSystemRoute = - LayoutAuthenticatedAdminSystemRouteImport.update({ - id: '/system', - path: '/system', - getParentRoute: () => LayoutAuthenticatedAdminRoute, - } as any) const LayoutAuthenticatedAcceptInvitationIdRoute = LayoutAuthenticatedAcceptInvitationIdRouteImport.update({ id: '/accept-invitation/$id', path: '/accept-invitation/$id', getParentRoute: () => LayoutAuthenticatedRoute, } as any) +const LayoutAdminAdminSystemRoute = LayoutAdminAdminSystemRouteImport.update({ + id: '/system', + path: '/system', + getParentRoute: () => LayoutAdminAdminRoute, +} as any) +const LayoutPublicAppsAccountIdIndexRoute = + LayoutPublicAppsAccountIdIndexRouteImport.update({ + id: '/apps/$accountId/', + path: '/apps/$accountId/', + getParentRoute: () => LayoutPublicRoute, + } as any) +const LayoutPublicAppsAccountIdGatewayIdRoute = + LayoutPublicAppsAccountIdGatewayIdRouteImport.update({ + id: '/apps/$accountId/$gatewayId', + path: '/apps/$accountId/$gatewayId', + getParentRoute: () => LayoutPublicRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof LayoutPublicIndexRoute - '/admin': typeof LayoutAuthenticatedAdminRouteWithChildren - '/home': typeof LayoutAuthenticatedHomeRoute + '/admin': typeof LayoutAdminAdminRouteWithChildren + '/dashboard': typeof LayoutAuthenticatedDashboardRoute '/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren '/$accountId': typeof LayoutPublicAccountIdRoute '/about': typeof LayoutPublicAboutRoute '/login': typeof LayoutPublicLoginRoute '/skill': typeof LayoutPublicSkillRoute - '/things/$thingId': typeof LayoutThingsThingIdRoute - '/things/live': typeof LayoutThingsLiveRoute - '/apps/': typeof LayoutAppsIndexRoute - '/things/': typeof LayoutThingsIndexRoute + '/admin/system': typeof LayoutAdminAdminSystemRoute '/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute - '/admin/system': typeof LayoutAuthenticatedAdminSystemRoute '/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute '/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -228,25 +228,25 @@ export interface FileRoutesByFullPath { '/tenant/$tenantId': typeof LayoutAuthenticatedTenantTenantIdRoute '/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/things/new': typeof LayoutAuthenticatedThingsNewRoute - '/apps/$accountId/$gatewayId': typeof LayoutAppsAccountIdGatewayIdRoute - '/admin/': typeof LayoutAuthenticatedAdminIndexRoute + '/things/$thingId': typeof LayoutPublicThingsThingIdRoute + '/things/live': typeof LayoutPublicThingsLiveRoute + '/admin/': typeof LayoutAdminAdminIndexRoute '/organizations/': typeof LayoutAuthenticatedOrganizationsIndexRoute '/settings/': typeof LayoutAuthenticatedSettingsIndexRoute - '/apps/$accountId/': typeof LayoutAppsAccountIdIndexRoute + '/apps/': typeof LayoutPublicAppsIndexRoute + '/things/': typeof LayoutPublicThingsIndexRoute + '/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute + '/apps/$accountId/': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRoutesByTo { '/': typeof LayoutPublicIndexRoute - '/home': typeof LayoutAuthenticatedHomeRoute + '/dashboard': typeof LayoutAuthenticatedDashboardRoute '/$accountId': typeof LayoutPublicAccountIdRoute '/about': typeof LayoutPublicAboutRoute '/login': typeof LayoutPublicLoginRoute '/skill': typeof LayoutPublicSkillRoute - '/things/$thingId': typeof LayoutThingsThingIdRoute - '/things/live': typeof LayoutThingsLiveRoute - '/apps': typeof LayoutAppsIndexRoute - '/things': typeof LayoutThingsIndexRoute + '/admin/system': typeof LayoutAdminAdminSystemRoute '/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute - '/admin/system': typeof LayoutAuthenticatedAdminSystemRoute '/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute '/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -255,31 +255,32 @@ export interface FileRoutesByTo { '/tenant/$tenantId': typeof LayoutAuthenticatedTenantTenantIdRoute '/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/things/new': typeof LayoutAuthenticatedThingsNewRoute - '/apps/$accountId/$gatewayId': typeof LayoutAppsAccountIdGatewayIdRoute - '/admin': typeof LayoutAuthenticatedAdminIndexRoute + '/things/$thingId': typeof LayoutPublicThingsThingIdRoute + '/things/live': typeof LayoutPublicThingsLiveRoute + '/admin': typeof LayoutAdminAdminIndexRoute '/organizations': typeof LayoutAuthenticatedOrganizationsIndexRoute '/settings': typeof LayoutAuthenticatedSettingsIndexRoute - '/apps/$accountId': typeof LayoutAppsAccountIdIndexRoute + '/apps': typeof LayoutPublicAppsIndexRoute + '/things': typeof LayoutPublicThingsIndexRoute + '/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute + '/apps/$accountId': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/_layout': typeof LayoutRouteWithChildren + '/_layout/_admin': typeof LayoutAdminRouteWithChildren '/_layout/_authenticated': typeof LayoutAuthenticatedRouteWithChildren '/_layout/_public': typeof LayoutPublicRouteWithChildren - '/_layout/_authenticated/admin': typeof LayoutAuthenticatedAdminRouteWithChildren - '/_layout/_authenticated/home': typeof LayoutAuthenticatedHomeRoute + '/_layout/_admin/admin': typeof LayoutAdminAdminRouteWithChildren + '/_layout/_authenticated/dashboard': typeof LayoutAuthenticatedDashboardRoute '/_layout/_authenticated/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren '/_layout/_public/$accountId': typeof LayoutPublicAccountIdRoute '/_layout/_public/about': typeof LayoutPublicAboutRoute '/_layout/_public/login': typeof LayoutPublicLoginRoute '/_layout/_public/skill': typeof LayoutPublicSkillRoute - '/_layout/things/$thingId': typeof LayoutThingsThingIdRoute - '/_layout/things/live': typeof LayoutThingsLiveRoute '/_layout/_public/': typeof LayoutPublicIndexRoute - '/_layout/apps/': typeof LayoutAppsIndexRoute - '/_layout/things/': typeof LayoutThingsIndexRoute + '/_layout/_admin/admin/system': typeof LayoutAdminAdminSystemRoute '/_layout/_authenticated/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute - '/_layout/_authenticated/admin/system': typeof LayoutAuthenticatedAdminSystemRoute '/_layout/_authenticated/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute '/_layout/_authenticated/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute '/_layout/_authenticated/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -288,29 +289,29 @@ export interface FileRoutesById { '/_layout/_authenticated/tenant/$tenantId': typeof LayoutAuthenticatedTenantTenantIdRoute '/_layout/_authenticated/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/_layout/_authenticated/things/new': typeof LayoutAuthenticatedThingsNewRoute - '/_layout/apps/$accountId/$gatewayId': typeof LayoutAppsAccountIdGatewayIdRoute - '/_layout/_authenticated/admin/': typeof LayoutAuthenticatedAdminIndexRoute + '/_layout/_public/things/$thingId': typeof LayoutPublicThingsThingIdRoute + '/_layout/_public/things/live': typeof LayoutPublicThingsLiveRoute + '/_layout/_admin/admin/': typeof LayoutAdminAdminIndexRoute '/_layout/_authenticated/organizations/': typeof LayoutAuthenticatedOrganizationsIndexRoute '/_layout/_authenticated/settings/': typeof LayoutAuthenticatedSettingsIndexRoute - '/_layout/apps/$accountId/': typeof LayoutAppsAccountIdIndexRoute + '/_layout/_public/apps/': typeof LayoutPublicAppsIndexRoute + '/_layout/_public/things/': typeof LayoutPublicThingsIndexRoute + '/_layout/_public/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute + '/_layout/_public/apps/$accountId/': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' | '/admin' - | '/home' + | '/dashboard' | '/settings' | '/$accountId' | '/about' | '/login' | '/skill' - | '/things/$thingId' - | '/things/live' - | '/apps/' - | '/things/' - | '/accept-invitation/$id' | '/admin/system' + | '/accept-invitation/$id' | '/organizations/$slug' | '/organizations/new' | '/settings/auth-methods' @@ -319,25 +320,25 @@ export interface FileRouteTypes { | '/tenant/$tenantId' | '/tenant/new' | '/things/new' - | '/apps/$accountId/$gatewayId' + | '/things/$thingId' + | '/things/live' | '/admin/' | '/organizations/' | '/settings/' + | '/apps/' + | '/things/' + | '/apps/$accountId/$gatewayId' | '/apps/$accountId/' fileRoutesByTo: FileRoutesByTo to: | '/' - | '/home' + | '/dashboard' | '/$accountId' | '/about' | '/login' | '/skill' - | '/things/$thingId' - | '/things/live' - | '/apps' - | '/things' - | '/accept-invitation/$id' | '/admin/system' + | '/accept-invitation/$id' | '/organizations/$slug' | '/organizations/new' | '/settings/auth-methods' @@ -346,30 +347,31 @@ export interface FileRouteTypes { | '/tenant/$tenantId' | '/tenant/new' | '/things/new' - | '/apps/$accountId/$gatewayId' + | '/things/$thingId' + | '/things/live' | '/admin' | '/organizations' | '/settings' + | '/apps' + | '/things' + | '/apps/$accountId/$gatewayId' | '/apps/$accountId' id: | '__root__' | '/_layout' + | '/_layout/_admin' | '/_layout/_authenticated' | '/_layout/_public' - | '/_layout/_authenticated/admin' - | '/_layout/_authenticated/home' + | '/_layout/_admin/admin' + | '/_layout/_authenticated/dashboard' | '/_layout/_authenticated/settings' | '/_layout/_public/$accountId' | '/_layout/_public/about' | '/_layout/_public/login' | '/_layout/_public/skill' - | '/_layout/things/$thingId' - | '/_layout/things/live' | '/_layout/_public/' - | '/_layout/apps/' - | '/_layout/things/' + | '/_layout/_admin/admin/system' | '/_layout/_authenticated/accept-invitation/$id' - | '/_layout/_authenticated/admin/system' | '/_layout/_authenticated/organizations/$slug' | '/_layout/_authenticated/organizations/new' | '/_layout/_authenticated/settings/auth-methods' @@ -378,11 +380,15 @@ export interface FileRouteTypes { | '/_layout/_authenticated/tenant/$tenantId' | '/_layout/_authenticated/tenant/new' | '/_layout/_authenticated/things/new' - | '/_layout/apps/$accountId/$gatewayId' - | '/_layout/_authenticated/admin/' + | '/_layout/_public/things/$thingId' + | '/_layout/_public/things/live' + | '/_layout/_admin/admin/' | '/_layout/_authenticated/organizations/' | '/_layout/_authenticated/settings/' - | '/_layout/apps/$accountId/' + | '/_layout/_public/apps/' + | '/_layout/_public/things/' + | '/_layout/_public/apps/$accountId/$gatewayId' + | '/_layout/_public/apps/$accountId/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -412,18 +418,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedRouteImport parentRoute: typeof LayoutRoute } - '/_layout/things/': { - id: '/_layout/things/' - path: '/things' - fullPath: '/things/' - preLoaderRoute: typeof LayoutThingsIndexRouteImport - parentRoute: typeof LayoutRoute - } - '/_layout/apps/': { - id: '/_layout/apps/' - path: '/apps' - fullPath: '/apps/' - preLoaderRoute: typeof LayoutAppsIndexRouteImport + '/_layout/_admin': { + id: '/_layout/_admin' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutAdminRouteImport parentRoute: typeof LayoutRoute } '/_layout/_public/': { @@ -433,20 +432,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicIndexRouteImport parentRoute: typeof LayoutPublicRoute } - '/_layout/things/live': { - id: '/_layout/things/live' - path: '/things/live' - fullPath: '/things/live' - preLoaderRoute: typeof LayoutThingsLiveRouteImport - parentRoute: typeof LayoutRoute - } - '/_layout/things/$thingId': { - id: '/_layout/things/$thingId' - path: '/things/$thingId' - fullPath: '/things/$thingId' - preLoaderRoute: typeof LayoutThingsThingIdRouteImport - parentRoute: typeof LayoutRoute - } '/_layout/_public/skill': { id: '/_layout/_public/skill' path: '/skill' @@ -482,26 +467,33 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedSettingsRouteImport parentRoute: typeof LayoutAuthenticatedRoute } - '/_layout/_authenticated/home': { - id: '/_layout/_authenticated/home' - path: '/home' - fullPath: '/home' - preLoaderRoute: typeof LayoutAuthenticatedHomeRouteImport + '/_layout/_authenticated/dashboard': { + id: '/_layout/_authenticated/dashboard' + path: '/dashboard' + fullPath: '/dashboard' + preLoaderRoute: typeof LayoutAuthenticatedDashboardRouteImport parentRoute: typeof LayoutAuthenticatedRoute } - '/_layout/_authenticated/admin': { - id: '/_layout/_authenticated/admin' + '/_layout/_admin/admin': { + id: '/_layout/_admin/admin' path: '/admin' fullPath: '/admin' - preLoaderRoute: typeof LayoutAuthenticatedAdminRouteImport - parentRoute: typeof LayoutAuthenticatedRoute + preLoaderRoute: typeof LayoutAdminAdminRouteImport + parentRoute: typeof LayoutAdminRoute } - '/_layout/apps/$accountId/': { - id: '/_layout/apps/$accountId/' - path: '/apps/$accountId' - fullPath: '/apps/$accountId/' - preLoaderRoute: typeof LayoutAppsAccountIdIndexRouteImport - parentRoute: typeof LayoutRoute + '/_layout/_public/things/': { + id: '/_layout/_public/things/' + path: '/things' + fullPath: '/things/' + preLoaderRoute: typeof LayoutPublicThingsIndexRouteImport + parentRoute: typeof LayoutPublicRoute + } + '/_layout/_public/apps/': { + id: '/_layout/_public/apps/' + path: '/apps' + fullPath: '/apps/' + preLoaderRoute: typeof LayoutPublicAppsIndexRouteImport + parentRoute: typeof LayoutPublicRoute } '/_layout/_authenticated/settings/': { id: '/_layout/_authenticated/settings/' @@ -517,19 +509,26 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedOrganizationsIndexRouteImport parentRoute: typeof LayoutAuthenticatedRoute } - '/_layout/_authenticated/admin/': { - id: '/_layout/_authenticated/admin/' + '/_layout/_admin/admin/': { + id: '/_layout/_admin/admin/' path: '/' fullPath: '/admin/' - preLoaderRoute: typeof LayoutAuthenticatedAdminIndexRouteImport - parentRoute: typeof LayoutAuthenticatedAdminRoute + preLoaderRoute: typeof LayoutAdminAdminIndexRouteImport + parentRoute: typeof LayoutAdminAdminRoute } - '/_layout/apps/$accountId/$gatewayId': { - id: '/_layout/apps/$accountId/$gatewayId' - path: '/apps/$accountId/$gatewayId' - fullPath: '/apps/$accountId/$gatewayId' - preLoaderRoute: typeof LayoutAppsAccountIdGatewayIdRouteImport - parentRoute: typeof LayoutRoute + '/_layout/_public/things/live': { + id: '/_layout/_public/things/live' + path: '/things/live' + fullPath: '/things/live' + preLoaderRoute: typeof LayoutPublicThingsLiveRouteImport + parentRoute: typeof LayoutPublicRoute + } + '/_layout/_public/things/$thingId': { + id: '/_layout/_public/things/$thingId' + path: '/things/$thingId' + fullPath: '/things/$thingId' + preLoaderRoute: typeof LayoutPublicThingsThingIdRouteImport + parentRoute: typeof LayoutPublicRoute } '/_layout/_authenticated/things/new': { id: '/_layout/_authenticated/things/new' @@ -587,13 +586,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedOrganizationsSlugRouteImport parentRoute: typeof LayoutAuthenticatedRoute } - '/_layout/_authenticated/admin/system': { - id: '/_layout/_authenticated/admin/system' - path: '/system' - fullPath: '/admin/system' - preLoaderRoute: typeof LayoutAuthenticatedAdminSystemRouteImport - parentRoute: typeof LayoutAuthenticatedAdminRoute - } '/_layout/_authenticated/accept-invitation/$id': { id: '/_layout/_authenticated/accept-invitation/$id' path: '/accept-invitation/$id' @@ -601,24 +593,54 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedAcceptInvitationIdRouteImport parentRoute: typeof LayoutAuthenticatedRoute } + '/_layout/_admin/admin/system': { + id: '/_layout/_admin/admin/system' + path: '/system' + fullPath: '/admin/system' + preLoaderRoute: typeof LayoutAdminAdminSystemRouteImport + parentRoute: typeof LayoutAdminAdminRoute + } + '/_layout/_public/apps/$accountId/': { + id: '/_layout/_public/apps/$accountId/' + path: '/apps/$accountId' + fullPath: '/apps/$accountId/' + preLoaderRoute: typeof LayoutPublicAppsAccountIdIndexRouteImport + parentRoute: typeof LayoutPublicRoute + } + '/_layout/_public/apps/$accountId/$gatewayId': { + id: '/_layout/_public/apps/$accountId/$gatewayId' + path: '/apps/$accountId/$gatewayId' + fullPath: '/apps/$accountId/$gatewayId' + preLoaderRoute: typeof LayoutPublicAppsAccountIdGatewayIdRouteImport + parentRoute: typeof LayoutPublicRoute + } } } -interface LayoutAuthenticatedAdminRouteChildren { - LayoutAuthenticatedAdminSystemRoute: typeof LayoutAuthenticatedAdminSystemRoute - LayoutAuthenticatedAdminIndexRoute: typeof LayoutAuthenticatedAdminIndexRoute +interface LayoutAdminAdminRouteChildren { + LayoutAdminAdminSystemRoute: typeof LayoutAdminAdminSystemRoute + LayoutAdminAdminIndexRoute: typeof LayoutAdminAdminIndexRoute } -const LayoutAuthenticatedAdminRouteChildren: LayoutAuthenticatedAdminRouteChildren = - { - LayoutAuthenticatedAdminSystemRoute: LayoutAuthenticatedAdminSystemRoute, - LayoutAuthenticatedAdminIndexRoute: LayoutAuthenticatedAdminIndexRoute, - } +const LayoutAdminAdminRouteChildren: LayoutAdminAdminRouteChildren = { + LayoutAdminAdminSystemRoute: LayoutAdminAdminSystemRoute, + LayoutAdminAdminIndexRoute: LayoutAdminAdminIndexRoute, +} -const LayoutAuthenticatedAdminRouteWithChildren = - LayoutAuthenticatedAdminRoute._addFileChildren( - LayoutAuthenticatedAdminRouteChildren, - ) +const LayoutAdminAdminRouteWithChildren = + LayoutAdminAdminRoute._addFileChildren(LayoutAdminAdminRouteChildren) + +interface LayoutAdminRouteChildren { + LayoutAdminAdminRoute: typeof LayoutAdminAdminRouteWithChildren +} + +const LayoutAdminRouteChildren: LayoutAdminRouteChildren = { + LayoutAdminAdminRoute: LayoutAdminAdminRouteWithChildren, +} + +const LayoutAdminRouteWithChildren = LayoutAdminRoute._addFileChildren( + LayoutAdminRouteChildren, +) interface LayoutAuthenticatedSettingsRouteChildren { LayoutAuthenticatedSettingsAuthMethodsRoute: typeof LayoutAuthenticatedSettingsAuthMethodsRoute @@ -645,8 +667,7 @@ const LayoutAuthenticatedSettingsRouteWithChildren = ) interface LayoutAuthenticatedRouteChildren { - LayoutAuthenticatedAdminRoute: typeof LayoutAuthenticatedAdminRouteWithChildren - LayoutAuthenticatedHomeRoute: typeof LayoutAuthenticatedHomeRoute + LayoutAuthenticatedDashboardRoute: typeof LayoutAuthenticatedDashboardRoute LayoutAuthenticatedSettingsRoute: typeof LayoutAuthenticatedSettingsRouteWithChildren LayoutAuthenticatedAcceptInvitationIdRoute: typeof LayoutAuthenticatedAcceptInvitationIdRoute LayoutAuthenticatedOrganizationsSlugRoute: typeof LayoutAuthenticatedOrganizationsSlugRoute @@ -658,8 +679,7 @@ interface LayoutAuthenticatedRouteChildren { } const LayoutAuthenticatedRouteChildren: LayoutAuthenticatedRouteChildren = { - LayoutAuthenticatedAdminRoute: LayoutAuthenticatedAdminRouteWithChildren, - LayoutAuthenticatedHomeRoute: LayoutAuthenticatedHomeRoute, + LayoutAuthenticatedDashboardRoute: LayoutAuthenticatedDashboardRoute, LayoutAuthenticatedSettingsRoute: LayoutAuthenticatedSettingsRouteWithChildren, LayoutAuthenticatedAcceptInvitationIdRoute: @@ -685,6 +705,12 @@ interface LayoutPublicRouteChildren { LayoutPublicLoginRoute: typeof LayoutPublicLoginRoute LayoutPublicSkillRoute: typeof LayoutPublicSkillRoute LayoutPublicIndexRoute: typeof LayoutPublicIndexRoute + LayoutPublicThingsThingIdRoute: typeof LayoutPublicThingsThingIdRoute + LayoutPublicThingsLiveRoute: typeof LayoutPublicThingsLiveRoute + LayoutPublicAppsIndexRoute: typeof LayoutPublicAppsIndexRoute + LayoutPublicThingsIndexRoute: typeof LayoutPublicThingsIndexRoute + LayoutPublicAppsAccountIdGatewayIdRoute: typeof LayoutPublicAppsAccountIdGatewayIdRoute + LayoutPublicAppsAccountIdIndexRoute: typeof LayoutPublicAppsAccountIdIndexRoute } const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { @@ -693,6 +719,13 @@ const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { LayoutPublicLoginRoute: LayoutPublicLoginRoute, LayoutPublicSkillRoute: LayoutPublicSkillRoute, LayoutPublicIndexRoute: LayoutPublicIndexRoute, + LayoutPublicThingsThingIdRoute: LayoutPublicThingsThingIdRoute, + LayoutPublicThingsLiveRoute: LayoutPublicThingsLiveRoute, + LayoutPublicAppsIndexRoute: LayoutPublicAppsIndexRoute, + LayoutPublicThingsIndexRoute: LayoutPublicThingsIndexRoute, + LayoutPublicAppsAccountIdGatewayIdRoute: + LayoutPublicAppsAccountIdGatewayIdRoute, + LayoutPublicAppsAccountIdIndexRoute: LayoutPublicAppsAccountIdIndexRoute, } const LayoutPublicRouteWithChildren = LayoutPublicRoute._addFileChildren( @@ -700,25 +733,15 @@ const LayoutPublicRouteWithChildren = LayoutPublicRoute._addFileChildren( ) interface LayoutRouteChildren { + LayoutAdminRoute: typeof LayoutAdminRouteWithChildren LayoutAuthenticatedRoute: typeof LayoutAuthenticatedRouteWithChildren LayoutPublicRoute: typeof LayoutPublicRouteWithChildren - LayoutThingsThingIdRoute: typeof LayoutThingsThingIdRoute - LayoutThingsLiveRoute: typeof LayoutThingsLiveRoute - LayoutAppsIndexRoute: typeof LayoutAppsIndexRoute - LayoutThingsIndexRoute: typeof LayoutThingsIndexRoute - LayoutAppsAccountIdGatewayIdRoute: typeof LayoutAppsAccountIdGatewayIdRoute - LayoutAppsAccountIdIndexRoute: typeof LayoutAppsAccountIdIndexRoute } const LayoutRouteChildren: LayoutRouteChildren = { + LayoutAdminRoute: LayoutAdminRouteWithChildren, LayoutAuthenticatedRoute: LayoutAuthenticatedRouteWithChildren, LayoutPublicRoute: LayoutPublicRouteWithChildren, - LayoutThingsThingIdRoute: LayoutThingsThingIdRoute, - LayoutThingsLiveRoute: LayoutThingsLiveRoute, - LayoutAppsIndexRoute: LayoutAppsIndexRoute, - LayoutThingsIndexRoute: LayoutThingsIndexRoute, - LayoutAppsAccountIdGatewayIdRoute: LayoutAppsAccountIdGatewayIdRoute, - LayoutAppsAccountIdIndexRoute: LayoutAppsAccountIdIndexRoute, } const LayoutRouteWithChildren = diff --git a/ui/src/routes/_layout/_admin.tsx b/ui/src/routes/_layout/_admin.tsx new file mode 100644 index 00000000..9906591e --- /dev/null +++ b/ui/src/routes/_layout/_admin.tsx @@ -0,0 +1,62 @@ +import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; +import type { SessionData } from "@/app"; +import { sessionQueryOptions } from "@/app"; + +interface AuthContext { + isAuthenticated: boolean; + user: SessionData["user"] | null; + session: SessionData["session"] | null; + activeOrganizationId: string | null; + isAnonymous: boolean; + isAdmin: boolean; + isBanned: boolean; +} + +export const Route = createFileRoute("/_layout/_admin")({ + beforeLoad: async ({ context, location }) => { + const { queryClient, authClient } = context; + + const session = await queryClient.ensureQueryData( + sessionQueryOptions(authClient, context.session), + ); + + if (!session?.user) { + throw redirect({ + to: "/login", + search: { + redirect: location.href, + }, + }); + } + + if (session.user.banned) { + throw redirect({ + to: "/login", + hash: "banned", + }); + } + + if (session.user.role !== "admin") { + throw redirect({ to: "/dashboard" }); + } + + const auth: AuthContext = { + isAuthenticated: true, + user: session.user, + session: session.session, + activeOrganizationId: session.session?.activeOrganizationId || null, + isAnonymous: session.user.isAnonymous || false, + isAdmin: session.user.role === "admin", + isBanned: session.user.banned || false, + }; + return { + auth, + session, + }; + }, + component: AdminGate, +}); + +function AdminGate() { + return <Outlet />; +} diff --git a/ui/src/routes/_layout/_authenticated/admin.tsx b/ui/src/routes/_layout/_admin/admin.tsx similarity index 65% rename from ui/src/routes/_layout/_authenticated/admin.tsx rename to ui/src/routes/_layout/_admin/admin.tsx index a044a4f4..e57fd806 100644 --- a/ui/src/routes/_layout/_authenticated/admin.tsx +++ b/ui/src/routes/_layout/_admin/admin.tsx @@ -1,12 +1,10 @@ -import { createFileRoute, Link, redirect } from "@tanstack/react-router"; -import { Shield, Users } from "lucide-react"; +import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router"; +import { Shield } from "lucide-react"; import { getAccount } from "@/app"; -import { Card } from "@/components"; import { EmptyState } from "@/components/empty-state"; import { PageContainer } from "@/components/layout/page-container"; -import { InfoRow } from "@/components/ui/info-row"; -export const Route = createFileRoute("/_layout/_authenticated/admin")({ +export const Route = createFileRoute("/_layout/_admin/admin")({ head: () => ({ meta: [{ title: "Admin | app" }], }), @@ -109,42 +107,7 @@ function AdminPage() { /> </section> - <section className="space-y-3"> - <SectionHeader title="Tenant details" /> - <Card className="p-6 space-y-4"> - <div className="text-muted-foreground text-[11px] font-bold uppercase tracking-wider"> - Configuration - </div> - <div className="flex flex-col gap-2"> - <InfoRow label="name" value={tenant.name} /> - <InfoRow label="subdomain" value={tenant.subdomain} mono /> - <InfoRow label="account" value={tenant.accountId} mono /> - <InfoRow label="org Id" value={tenant.orgId} mono /> - <InfoRow - label="created" - value={tenant.createdAt ? new Date(tenant.createdAt).toLocaleDateString() : "—"} - /> - </div> - </Card> - </section> - - <section className="space-y-3"> - <SectionHeader title="Members & permissions" /> - <Card className="p-4 space-y-3"> - <p className="text-sm text-muted-foreground"> - This tenant is backed by an organization. Manage members, roles, and invitations - there. - </p> - <Link - to="/organizations/$slug" - params={{ slug: tenant.subdomain }} - className="h-9 px-3 inline-flex items-center gap-1.5 text-xs font-medium border-2 border-outset border-border-strong bg-card text-foreground shadow-sm hover:shadow-md active:border-inset active:shadow-none transition-all duration-200 ease-out rounded-[10px]" - > - <Users className="h-3.5 w-3.5" /> - open organization - </Link> - </Card> - </section> + <Outlet /> </div> </PageContainer> ); @@ -172,12 +135,3 @@ function StatCard({ </div> ); } - -function SectionHeader({ title, action }: { title: string; action?: React.ReactNode }) { - return ( - <div className="flex items-end justify-between gap-3"> - <h2 className="text-lg font-semibold text-foreground">{title}</h2> - {action} - </div> - ); -} diff --git a/ui/src/routes/_layout/_admin/admin/index.tsx b/ui/src/routes/_layout/_admin/admin/index.tsx new file mode 100644 index 00000000..fd5a7378 --- /dev/null +++ b/ui/src/routes/_layout/_admin/admin/index.tsx @@ -0,0 +1,152 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import { Building2, Settings, Users } from "lucide-react"; +import { getAccount } from "@/app"; +import { Button, Card } from "@/components"; +import { InfoRow } from "@/components/ui/info-row"; + +export const Route = createFileRoute("/_layout/_admin/admin/")({ + head: () => ({ + meta: [{ title: "Admin Dashboard | app" }], + }), + component: AdminDashboard, +}); + +function AdminDashboard() { + const { auth, tenant } = Route.useRouteContext(); + const account = getAccount(); + const user = auth?.user ?? null; + + return ( + <div className="space-y-8"> + <header className="space-y-3"> + <div className="flex flex-wrap items-end justify-between gap-3"> + <div className="space-y-1"> + <h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground"> + Dashboard + </h1> + <p className="text-sm text-muted-foreground"> + Signed in as <span className="font-mono">{account}</span> + </p> + </div> + </div> + </header> + + <section className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> + <StatCard label="Account" value={account} mono /> + <StatCard label="Name" value={user?.name || user?.email || "—"} /> + <StatCard label="Role" value={user?.role ?? "—"} /> + <StatCard + label="Created" + value={user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : "—"} + /> + </section> + + <section className="space-y-3"> + <h2 className="text-lg font-semibold text-foreground">Manage</h2> + <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> + <Card className="p-6 space-y-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-foreground text-background"> + <Building2 className="h-4 w-4" /> + </div> + <h3 className="text-base font-semibold text-foreground">Organizations</h3> + <p className="text-sm text-muted-foreground"> + Manage organizations, members, roles, and invitations. + </p> + <Button asChild variant="outline" size="sm"> + <Link to="/organizations"> + <Users className="h-3.5 w-3.5" /> + open organizations + </Link> + </Button> + </Card> + + <Card className="p-6 space-y-3"> + <div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-foreground text-background"> + <Settings className="h-4 w-4" /> + </div> + <h3 className="text-base font-semibold text-foreground">Settings</h3> + <p className="text-sm text-muted-foreground"> + Update your profile, auth methods, and security preferences. + </p> + <Button asChild variant="outline" size="sm"> + <Link to="/settings">open settings</Link> + </Button> + </Card> + </div> + </section> + + {tenant && ( + <section className="space-y-3"> + <SectionHeader title="Tenant details" /> + <Card className="p-6 space-y-4"> + <div className="text-muted-foreground text-[11px] font-bold uppercase tracking-wider"> + Configuration + </div> + <div className="flex flex-col gap-2"> + <InfoRow label="name" value={tenant.name} /> + <InfoRow label="subdomain" value={tenant.subdomain} mono /> + <InfoRow label="account" value={tenant.accountId} mono /> + <InfoRow label="org Id" value={tenant.orgId} mono /> + <InfoRow + label="created" + value={tenant.createdAt ? new Date(tenant.createdAt).toLocaleDateString() : "—"} + /> + </div> + </Card> + </section> + )} + + {tenant && ( + <section className="space-y-3"> + <SectionHeader title="Members & permissions" /> + <Card className="p-4 space-y-3"> + <p className="text-sm text-muted-foreground"> + This tenant is backed by an organization. Manage members, roles, and invitations + there. + </p> + <Link + to="/organizations/$slug" + params={{ slug: tenant.subdomain }} + className="h-9 px-3 inline-flex items-center gap-1.5 text-xs font-medium border-2 border-outset border-border-strong bg-card text-foreground shadow-sm hover:shadow-md active:border-inset active:shadow-none transition-all duration-200 ease-out rounded-[10px]" + > + <Users className="h-3.5 w-3.5" /> + open organization + </Link> + </Card> + </section> + )} + </div> + ); +} + +function StatCard({ + label, + value, + mono, +}: { + label: string; + value: React.ReactNode; + mono?: boolean; +}) { + return ( + <div className="border-2 border-outset border-border-strong bg-card p-4 rounded-[12px] shadow-sm space-y-1"> + <div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground"> + {label} + </div> + <div + className={`text-sm text-foreground break-all ${mono ? "font-mono text-xs" : "font-semibold"}`} + > + {value} + </div> + </div> + ); +} + +function SectionHeader({ title, action }: { title: string; action?: React.ReactNode }) { + return ( + <div className="flex items-end justify-between gap-3"> + <h2 className="text-lg font-semibold text-foreground">{title}</h2> + {action} + </div> + ); +} diff --git a/ui/src/routes/_layout/_admin/admin/system.tsx b/ui/src/routes/_layout/_admin/admin/system.tsx new file mode 100644 index 00000000..a64457a3 --- /dev/null +++ b/ui/src/routes/_layout/_admin/admin/system.tsx @@ -0,0 +1,70 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app"; +import { Card } from "@/components"; +import { InfoRow } from "@/components/ui/info-row"; + +export const Route = createFileRoute("/_layout/_admin/admin/system")({ + loader: async ({ context }) => ({ + runtimeConfig: context.runtimeConfig, + }), + head: () => ({ + meta: [{ title: "Admin System | app" }], + }), + component: AdminSystem, +}); + +function AdminSystem() { + const { runtimeConfig } = Route.useLoaderData(); + const account = getAccount(runtimeConfig); + const appName = getAppName(runtimeConfig); + const repository = getRepository(runtimeConfig); + const runtime = getActiveRuntime(runtimeConfig); + + const env = runtimeConfig?.env; + const networkId = runtimeConfig?.networkId; + const hostUrl = runtimeConfig?.hostUrl; + const apiBase = runtimeConfig?.apiBase; + const rpcBase = runtimeConfig?.rpcBase; + const assetsUrl = runtimeConfig?.assetsUrl; + const runtimeBasePath = runtime?.runtimeBasePath; + + return ( + <div className="space-y-6"> + <header className="space-y-3"> + <div className="space-y-1"> + <h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground"> + System + </h1> + <p className="text-sm text-muted-foreground"> + Runtime configuration for this deployment. + </p> + </div> + </header> + + <div className="grid gap-4 sm:grid-cols-2"> + <Card className="p-6 space-y-4"> + <h2 className="text-sm font-semibold text-foreground">Runtime</h2> + <InfoRow label="account" value={runtime?.accountId ?? account} mono /> + <InfoRow label="name" value={appName} /> + <InfoRow label="base path" value={runtimeBasePath ?? "/"} mono /> + <InfoRow label="gateway" value={runtime?.gatewayId} mono /> + </Card> + + <Card className="p-6 space-y-4"> + <h2 className="text-sm font-semibold text-foreground">Deployment</h2> + <InfoRow label="env" value={env ?? "—"} mono /> + <InfoRow label="network" value={networkId ?? "—"} mono /> + <InfoRow label="host" value={hostUrl ?? "—"} mono /> + <InfoRow label="repository" value={repository} mono /> + </Card> + + <Card className="p-6 space-y-4"> + <h2 className="text-sm font-semibold text-foreground">Endpoints</h2> + <InfoRow label="api" value={apiBase} mono /> + <InfoRow label="rpc" value={rpcBase} mono /> + <InfoRow label="assets" value={assetsUrl} mono /> + </Card> + </div> + </div> + ); +} diff --git a/ui/src/routes/_layout/_authenticated.tsx b/ui/src/routes/_layout/_authenticated.tsx index 869f23e3..72eae14a 100644 --- a/ui/src/routes/_layout/_authenticated.tsx +++ b/ui/src/routes/_layout/_authenticated.tsx @@ -94,7 +94,7 @@ function AuthenticatedLayout() { const isAdmin = session?.user?.role === "admin"; const sidebarItems: SidebarItem[] = [ - { icon: Home, label: "home", to: "/home", roleRequired: "anon" }, + { icon: Home, label: "dashboard", to: "/dashboard", roleRequired: "anon" }, { icon: Shield, label: "admin", to: "/admin", roleRequired: "admin" }, { icon: MessageSquare, label: "nostr", to: "/nostr", roleRequired: "admin" }, ]; @@ -221,7 +221,7 @@ function MobileTabBar({ style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)" }} > <div className="flex items-center justify-around px-2 py-1"> - <TabItem to="/home" icon={Home} label="home" active={tabActive("/home")} /> + <TabItem to="/dashboard" icon={Home} label="dashboard" active={tabActive("/dashboard")} /> <TabItem to="/apps" icon={Globe} label="apps" active={tabActive("/apps")} /> <TabItem to="/explore" icon={Compass} label="explore" active={tabActive("/explore")} /> <TabItem to="/recent" icon={ClipboardList} label="recent" active={tabActive("/recent")} /> diff --git a/ui/src/routes/_layout/_authenticated/admin/index.tsx b/ui/src/routes/_layout/_authenticated/admin/index.tsx deleted file mode 100644 index 65c3c2d3..00000000 --- a/ui/src/routes/_layout/_authenticated/admin/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import { Building2, Settings, Users } from "lucide-react"; -import { getAccount } from "@/app"; -import { Button, Card } from "@/components"; -import { PageContainer } from "@/components/layout/page-container"; - -export const Route = createFileRoute("/_layout/_authenticated/admin/")({ - head: () => ({ - meta: [{ title: "Admin Dashboard | app" }], - }), - component: AdminDashboard, -}); - -function AdminDashboard() { - const { auth } = Route.useRouteContext(); - const account = getAccount(); - const user = auth?.user ?? null; - - return ( - <PageContainer variant="wide"> - <div className="space-y-8"> - <header className="space-y-3"> - <div className="flex flex-wrap items-end justify-between gap-3"> - <div className="space-y-1"> - <h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground"> - Dashboard - </h1> - <p className="text-sm text-muted-foreground"> - Signed in as <span className="font-mono">{account}</span> - </p> - </div> - </div> - </header> - - <section className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> - <StatCard label="Account" value={account} mono /> - <StatCard label="Name" value={user?.name || user?.email || "—"} /> - <StatCard label="Role" value={user?.role ?? "—"} /> - <StatCard - label="Created" - value={user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : "—"} - /> - </section> - - <section className="space-y-3"> - <h2 className="text-lg font-semibold text-foreground">Manage</h2> - <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> - <Card className="p-6 space-y-3"> - <div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-foreground text-background"> - <Building2 className="h-4 w-4" /> - </div> - <h3 className="text-base font-semibold text-foreground">Organizations</h3> - <p className="text-sm text-muted-foreground"> - Manage organizations, members, roles, and invitations. - </p> - <Button asChild variant="outline" size="sm"> - <Link to="/organizations"> - <Users className="h-3.5 w-3.5" /> - open organizations - </Link> - </Button> - </Card> - - <Card className="p-6 space-y-3"> - <div className="flex h-10 w-10 items-center justify-center rounded-[10px] bg-foreground text-background"> - <Settings className="h-4 w-4" /> - </div> - <h3 className="text-base font-semibold text-foreground">Settings</h3> - <p className="text-sm text-muted-foreground"> - Update your profile, auth methods, and security preferences. - </p> - <Button asChild variant="outline" size="sm"> - <Link to="/settings">open settings</Link> - </Button> - </Card> - </div> - </section> - </div> - </PageContainer> - ); -} - -function StatCard({ - label, - value, - mono, -}: { - label: string; - value: React.ReactNode; - mono?: boolean; -}) { - return ( - <div className="border-2 border-outset border-border-strong bg-card p-4 rounded-[12px] shadow-sm space-y-1"> - <div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground"> - {label} - </div> - <div - className={`text-sm text-foreground break-all ${mono ? "font-mono text-xs" : "font-semibold"}`} - > - {value} - </div> - </div> - ); -} diff --git a/ui/src/routes/_layout/_authenticated/admin/system.tsx b/ui/src/routes/_layout/_authenticated/admin/system.tsx deleted file mode 100644 index a98296af..00000000 --- a/ui/src/routes/_layout/_authenticated/admin/system.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app"; -import { Card } from "@/components"; -import { PageContainer } from "@/components/layout/page-container"; -import { InfoRow } from "@/components/ui/info-row"; - -export const Route = createFileRoute("/_layout/_authenticated/admin/system")({ - loader: async ({ context }) => ({ - runtimeConfig: context.runtimeConfig, - }), - head: () => ({ - meta: [{ title: "Admin System | app" }], - }), - component: AdminSystem, -}); - -function AdminSystem() { - const { runtimeConfig } = Route.useLoaderData(); - const account = getAccount(runtimeConfig); - const appName = getAppName(runtimeConfig); - const repository = getRepository(runtimeConfig); - const runtime = getActiveRuntime(runtimeConfig); - - const env = runtimeConfig?.env; - const networkId = runtimeConfig?.networkId; - const hostUrl = runtimeConfig?.hostUrl; - const apiBase = runtimeConfig?.apiBase; - const rpcBase = runtimeConfig?.rpcBase; - const assetsUrl = runtimeConfig?.assetsUrl; - const runtimeBasePath = runtime?.runtimeBasePath; - - return ( - <PageContainer variant="default"> - <div className="space-y-6"> - <header className="space-y-3"> - <div className="space-y-1"> - <h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground"> - System - </h1> - <p className="text-sm text-muted-foreground"> - Runtime configuration for this deployment. - </p> - </div> - </header> - - <div className="grid gap-4 sm:grid-cols-2"> - <Card className="p-6 space-y-4"> - <h2 className="text-sm font-semibold text-foreground">Runtime</h2> - <InfoRow label="account" value={runtime?.accountId ?? account} mono /> - <InfoRow label="name" value={appName} /> - <InfoRow label="base path" value={runtimeBasePath ?? "/"} mono /> - <InfoRow label="gateway" value={runtime?.gatewayId} mono /> - </Card> - - <Card className="p-6 space-y-4"> - <h2 className="text-sm font-semibold text-foreground">Deployment</h2> - <InfoRow label="env" value={env ?? "—"} mono /> - <InfoRow label="network" value={networkId ?? "—"} mono /> - <InfoRow label="host" value={hostUrl ?? "—"} mono /> - <InfoRow label="repository" value={repository} mono /> - </Card> - - <Card className="p-6 space-y-4"> - <h2 className="text-sm font-semibold text-foreground">Endpoints</h2> - <InfoRow label="api" value={apiBase} mono /> - <InfoRow label="rpc" value={rpcBase} mono /> - <InfoRow label="assets" value={assetsUrl} mono /> - </Card> - </div> - </div> - </PageContainer> - ); -} diff --git a/ui/src/routes/_layout/_authenticated/home.tsx b/ui/src/routes/_layout/_authenticated/dashboard.tsx similarity index 98% rename from ui/src/routes/_layout/_authenticated/home.tsx rename to ui/src/routes/_layout/_authenticated/dashboard.tsx index 1fccff0e..86f23a6c 100644 --- a/ui/src/routes/_layout/_authenticated/home.tsx +++ b/ui/src/routes/_layout/_authenticated/dashboard.tsx @@ -13,7 +13,7 @@ import { Card } from "@/components"; import { PageContainer } from "@/components/layout/page-container"; import { InfoRow } from "@/components/ui/info-row"; -export const Route = createFileRoute("/_layout/_authenticated/home")({ +export const Route = createFileRoute("/_layout/_authenticated/dashboard")({ beforeLoad: async ({ context }) => { const { apiClient, runtimeConfig } = context; const accountId = getAccount(runtimeConfig); diff --git a/ui/src/routes/_layout/apps/$accountId/$gatewayId.tsx b/ui/src/routes/_layout/_public/apps/$accountId/$gatewayId.tsx similarity index 99% rename from ui/src/routes/_layout/apps/$accountId/$gatewayId.tsx rename to ui/src/routes/_layout/_public/apps/$accountId/$gatewayId.tsx index 279e4782..52984590 100644 --- a/ui/src/routes/_layout/apps/$accountId/$gatewayId.tsx +++ b/ui/src/routes/_layout/_public/apps/$accountId/$gatewayId.tsx @@ -19,7 +19,7 @@ const getStartCommand = (accountId: string, gatewayId: string) => const getExtendsCommand = (accountId: string, gatewayId: string) => `bunx everything-dev@latest init --extends bos://${accountId}/${gatewayId}`; -export const Route = createFileRoute("/_layout/apps/$accountId/$gatewayId")({ +export const Route = createFileRoute("/_layout/_public/apps/$accountId/$gatewayId")({ loader: async ({ params, context }) => { const { queryClient, apiClient } = context; await queryClient.prefetchQuery({ diff --git a/ui/src/routes/_layout/apps/$accountId/index.tsx b/ui/src/routes/_layout/_public/apps/$accountId/index.tsx similarity index 99% rename from ui/src/routes/_layout/apps/$accountId/index.tsx rename to ui/src/routes/_layout/_public/apps/$accountId/index.tsx index 904bdaf7..0ec1eb61 100644 --- a/ui/src/routes/_layout/apps/$accountId/index.tsx +++ b/ui/src/routes/_layout/_public/apps/$accountId/index.tsx @@ -9,7 +9,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"; const BASE_RUNTIME = "bos://dev.everything.near/everything.dev"; -export const Route = createFileRoute("/_layout/apps/$accountId/")({ +export const Route = createFileRoute("/_layout/_public/apps/$accountId/")({ loader: async ({ params, context }) => { const { queryClient, apiClient } = context; await queryClient.prefetchQuery({ diff --git a/ui/src/routes/_layout/apps/index.tsx b/ui/src/routes/_layout/_public/apps/index.tsx similarity index 99% rename from ui/src/routes/_layout/apps/index.tsx rename to ui/src/routes/_layout/_public/apps/index.tsx index 8ea8263f..4ce3c601 100644 --- a/ui/src/routes/_layout/apps/index.tsx +++ b/ui/src/routes/_layout/_public/apps/index.tsx @@ -21,7 +21,7 @@ type SearchParams = { preview?: string; }; -export const Route = createFileRoute("/_layout/apps/")({ +export const Route = createFileRoute("/_layout/_public/apps/")({ validateSearch: (search: Record<string, unknown>): SearchParams => ({ preview: typeof search.preview === "string" && search.preview.length > 0 ? search.preview : undefined, diff --git a/ui/src/routes/_layout/_public/login.tsx b/ui/src/routes/_layout/_public/login.tsx index fdc771d9..abc68a82 100644 --- a/ui/src/routes/_layout/_public/login.tsx +++ b/ui/src/routes/_layout/_public/login.tsx @@ -24,7 +24,7 @@ export const Route = createFileRoute("/_layout/_public/login")({ queryClient.getQueryData(sessionQueryOptions(authClient, initialSession).queryKey); if (session?.user) { - const redirectTo = search.redirect?.startsWith("/") ? search.redirect : "/home"; + const redirectTo = search.redirect?.startsWith("/") ? search.redirect : "/dashboard"; throw redirect({ to: redirectTo, search: {} }); } }, @@ -57,7 +57,7 @@ function LoginPage() { }, [auth.near]); const handleSuccess = async (message: string) => { - const redirectTo = redirect?.startsWith("/") ? redirect : "/home"; + const redirectTo = redirect?.startsWith("/") ? redirect : "/dashboard"; toast.success(message); queryClient.invalidateQueries({ queryKey: ["session"] }); navigate({ to: redirectTo, replace: true, search: {} }); @@ -110,7 +110,7 @@ function LoginPage() { }; if (session?.user) { - const redirectTo = redirect?.startsWith("/") ? redirect : "/home"; + const redirectTo = redirect?.startsWith("/") ? redirect : "/dashboard"; return <Navigate to={redirectTo} replace search={{}} />; } diff --git a/ui/src/routes/_layout/things/$thingId.tsx b/ui/src/routes/_layout/_public/things/$thingId.tsx similarity index 98% rename from ui/src/routes/_layout/things/$thingId.tsx rename to ui/src/routes/_layout/_public/things/$thingId.tsx index 0fdb7405..ac3184f4 100644 --- a/ui/src/routes/_layout/things/$thingId.tsx +++ b/ui/src/routes/_layout/_public/things/$thingId.tsx @@ -6,7 +6,7 @@ import { sessionQueryOptions, useApiClient, useAuthClient } from "@/app"; import { Badge, Button } from "@/components"; import { Skeleton } from "@/components/ui/skeleton"; -export const Route = createFileRoute("/_layout/things/$thingId")({ +export const Route = createFileRoute("/_layout/_public/things/$thingId")({ head: ({ params }) => ({ meta: [ { title: `${params.thingId} | Things | everything.dev` }, diff --git a/ui/src/routes/_layout/things/index.tsx b/ui/src/routes/_layout/_public/things/index.tsx similarity index 97% rename from ui/src/routes/_layout/things/index.tsx rename to ui/src/routes/_layout/_public/things/index.tsx index 6cbee7e0..fba26c37 100644 --- a/ui/src/routes/_layout/things/index.tsx +++ b/ui/src/routes/_layout/_public/things/index.tsx @@ -1,7 +1,7 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useState } from "react"; -export const Route = createFileRoute("/_layout/things/")({ +export const Route = createFileRoute("/_layout/_public/things/")({ head: () => ({ meta: [ { title: "Things | everything.dev" }, diff --git a/ui/src/routes/_layout/things/live.tsx b/ui/src/routes/_layout/_public/things/live.tsx similarity index 98% rename from ui/src/routes/_layout/things/live.tsx rename to ui/src/routes/_layout/_public/things/live.tsx index 7be8f0c6..674d7813 100644 --- a/ui/src/routes/_layout/things/live.tsx +++ b/ui/src/routes/_layout/_public/things/live.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useApiClient } from "@/app"; import { Badge } from "@/components"; -export const Route = createFileRoute("/_layout/things/live")({ +export const Route = createFileRoute("/_layout/_public/things/live")({ head: () => ({ meta: [ { title: "Live Stream | Things | everything.dev" }, From 9c90224a15ff2a60e38d7eade3f0f6e20f95ab15 Mon Sep 17 00:00:00 2001 From: Elliot Braem <elliot@ejlbraem.com> Date: Thu, 13 Aug 2026 17:44:55 -0500 Subject: [PATCH 06/24] test(regression): update /home references to /dashboard after route rename --- tests/regression/browser/helpers/seeded.ts | 2 +- tests/regression/browser/specs/auth-client.spec.ts | 4 ++-- tests/regression/browser/specs/auth-redirect.spec.ts | 4 ++-- tests/regression/browser/specs/logout.spec.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/regression/browser/helpers/seeded.ts b/tests/regression/browser/helpers/seeded.ts index 560e0aad..c580e4f9 100644 --- a/tests/regression/browser/helpers/seeded.ts +++ b/tests/regression/browser/helpers/seeded.ts @@ -42,7 +42,7 @@ export function loadSeedData(): SeedData { } export async function verifyAuthenticated(page: Page) { - await page.goto("/home", { waitUntil: "domcontentloaded" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(500); await page.waitForLoadState("networkidle"); await expect(page.locator("button[title='account menu']")).toBeVisible({ timeout: 10000 }); diff --git a/tests/regression/browser/specs/auth-client.spec.ts b/tests/regression/browser/specs/auth-client.spec.ts index bda4857d..970989f7 100644 --- a/tests/regression/browser/specs/auth-client.spec.ts +++ b/tests/regression/browser/specs/auth-client.spec.ts @@ -58,9 +58,9 @@ test.describe("authClient", () => { await anonymousBtn.click(); await signInDone; - await page.waitForURL(/\/home$/, { timeout: 15000 }); + await page.waitForURL(/\/dashboard$/, { timeout: 15000 }); await page.reload(); - await page.waitForURL(/\/home$/, { timeout: 15000 }); + await page.waitForURL(/\/dashboard$/, { timeout: 15000 }); await page.waitForLoadState("networkidle"); await waitForApp(page); diff --git a/tests/regression/browser/specs/auth-redirect.spec.ts b/tests/regression/browser/specs/auth-redirect.spec.ts index f0b3aede..afbaf19a 100644 --- a/tests/regression/browser/specs/auth-redirect.spec.ts +++ b/tests/regression/browser/specs/auth-redirect.spec.ts @@ -26,8 +26,8 @@ test.describe("Auth redirect", () => { expectNoHydrationFailure(pageErrors); }); - test("unauthenticated /home redirects to /login", async ({ page }) => { - await page.goto("/home", { waitUntil: "domcontentloaded" }); + test("unauthenticated /dashboard redirects to /login", async ({ page }) => { + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await waitForApp(page); await page.waitForURL(/\/login/, { timeout: 15000 }); diff --git a/tests/regression/browser/specs/logout.spec.ts b/tests/regression/browser/specs/logout.spec.ts index cef410e9..97642bcd 100644 --- a/tests/regression/browser/specs/logout.spec.ts +++ b/tests/regression/browser/specs/logout.spec.ts @@ -23,9 +23,9 @@ test.describe("logout", () => { await anonymousBtn.click(); await signInDone; - await page.waitForURL(/\/home$/, { timeout: 15000 }); + await page.waitForURL(/\/dashboard$/, { timeout: 15000 }); await page.reload(); - await page.waitForURL(/\/home$/, { timeout: 15000 }); + await page.waitForURL(/\/dashboard$/, { timeout: 15000 }); await page.waitForLoadState("networkidle"); await waitForApp(page); await page.locator("button[title='account menu']").click(); From 7f1b9dc11e98cdd98a404914dcde0c2bbb0bffb2 Mon Sep 17 00:00:00 2001 From: Elliot Braem <elliot@ejlbraem.com> Date: Thu, 13 Aug 2026 17:56:16 -0500 Subject: [PATCH 07/24] refactor(ui): add _anon mount for login, rename organizations to orgs, remove nostr - move login from public layout into new _anon pathless layout that redirects authed users to /dashboard and provides theme toggle header - rename organization route group from /organizations to /orgs; move invitation acceptance to /orgs/invites/$id - remove stale nostr entry from authenticated sidebar - changeset: ui-anon-orgs --- .changeset/ui-anon-orgs.md | 9 + ui/src/components/org-switcher.tsx | 2 +- ui/src/components/user-nav.tsx | 2 +- ui/src/routeTree.gen.ts | 224 ++++++++++-------- ui/src/routes/_layout/_admin/admin.tsx | 4 +- ui/src/routes/_layout/_admin/admin/index.tsx | 4 +- ui/src/routes/_layout/_anon.tsx | 57 +++++ .../_layout/{_public => _anon}/login.tsx | 4 +- ui/src/routes/_layout/_authenticated.tsx | 3 +- .../{organizations => orgs}/$slug.tsx | 12 +- .../{organizations => orgs}/index.tsx | 10 +- .../invites.$id.tsx} | 10 +- .../{organizations => orgs}/new.tsx | 10 +- .../_authenticated/tenant/$tenantId.tsx | 2 +- 14 files changed, 220 insertions(+), 133 deletions(-) create mode 100644 .changeset/ui-anon-orgs.md create mode 100644 ui/src/routes/_layout/_anon.tsx rename ui/src/routes/_layout/{_public => _anon}/login.tsx (98%) rename ui/src/routes/_layout/_authenticated/{organizations => orgs}/$slug.tsx (99%) rename ui/src/routes/_layout/_authenticated/{organizations => orgs}/index.tsx (98%) rename ui/src/routes/_layout/_authenticated/{accept-invitation.$id.tsx => orgs/invites.$id.tsx} (95%) rename ui/src/routes/_layout/_authenticated/{organizations => orgs}/new.tsx (96%) diff --git a/.changeset/ui-anon-orgs.md b/.changeset/ui-anon-orgs.md new file mode 100644 index 00000000..d1e2a805 --- /dev/null +++ b/.changeset/ui-anon-orgs.md @@ -0,0 +1,9 @@ +--- +"ui": minor +--- + +Add a dedicated anonymous mount and reorganize organization routes. + +- Add a `/_layout/_anon` pathless layout for pre-auth pages. Move login from the public layout into it; the layout redirects authenticated users to `/dashboard` and provides the theme toggle header. +- Rename the organization route group from `/organizations` to `/orgs` (`/orgs`, `/orgs/new`, `/orgs/$slug`) and move invitation acceptance to `/orgs/invites/$id`. +- Remove the stale nostr entry from the authenticated sidebar. \ No newline at end of file diff --git a/ui/src/components/org-switcher.tsx b/ui/src/components/org-switcher.tsx index 8d4eada0..7760d57f 100644 --- a/ui/src/components/org-switcher.tsx +++ b/ui/src/components/org-switcher.tsx @@ -64,7 +64,7 @@ export function OrgSwitcher({ organizations, activeOrgId, onSwitch }: OrgSwitche )} <DropdownMenuSeparator /> <DropdownMenuItem asChild> - <Link to="/organizations/new" className="flex items-center gap-2 cursor-pointer"> + <Link to="/orgs/new" className="flex items-center gap-2 cursor-pointer"> <Plus className="h-3.5 w-3.5" /> new organization </Link> diff --git a/ui/src/components/user-nav.tsx b/ui/src/components/user-nav.tsx index ba6a6107..084aca2f 100644 --- a/ui/src/components/user-nav.tsx +++ b/ui/src/components/user-nav.tsx @@ -151,7 +151,7 @@ export function UserNav() { </DropdownMenuItem> {activeOrg && ( <DropdownMenuItem asChild> - <Link to="/organizations/$slug" params={{ slug: activeOrg.slug }}> + <Link to="/orgs/$slug" params={{ slug: activeOrg.slug }}> <Building2 /> {activeOrg.name} </Link> diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 359bd53d..4ad07bcf 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -12,19 +12,20 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as LayoutRouteImport } from './routes/_layout' import { Route as LayoutPublicRouteImport } from './routes/_layout/_public' import { Route as LayoutAuthenticatedRouteImport } from './routes/_layout/_authenticated' +import { Route as LayoutAnonRouteImport } from './routes/_layout/_anon' import { Route as LayoutAdminRouteImport } from './routes/_layout/_admin' import { Route as LayoutPublicIndexRouteImport } from './routes/_layout/_public/index' import { Route as LayoutPublicSkillRouteImport } from './routes/_layout/_public/skill' -import { Route as LayoutPublicLoginRouteImport } from './routes/_layout/_public/login' import { Route as LayoutPublicAboutRouteImport } from './routes/_layout/_public/about' import { Route as LayoutPublicAccountIdRouteImport } from './routes/_layout/_public/$accountId' import { Route as LayoutAuthenticatedSettingsRouteImport } from './routes/_layout/_authenticated/settings' import { Route as LayoutAuthenticatedDashboardRouteImport } from './routes/_layout/_authenticated/dashboard' +import { Route as LayoutAnonLoginRouteImport } from './routes/_layout/_anon/login' import { Route as LayoutAdminAdminRouteImport } from './routes/_layout/_admin/admin' import { Route as LayoutPublicThingsIndexRouteImport } from './routes/_layout/_public/things/index' import { Route as LayoutPublicAppsIndexRouteImport } from './routes/_layout/_public/apps/index' import { Route as LayoutAuthenticatedSettingsIndexRouteImport } from './routes/_layout/_authenticated/settings/index' -import { Route as LayoutAuthenticatedOrganizationsIndexRouteImport } from './routes/_layout/_authenticated/organizations/index' +import { Route as LayoutAuthenticatedOrgsIndexRouteImport } from './routes/_layout/_authenticated/orgs/index' import { Route as LayoutAdminAdminIndexRouteImport } from './routes/_layout/_admin/admin/index' import { Route as LayoutPublicThingsLiveRouteImport } from './routes/_layout/_public/things/live' import { Route as LayoutPublicThingsThingIdRouteImport } from './routes/_layout/_public/things/$thingId' @@ -34,12 +35,12 @@ import { Route as LayoutAuthenticatedTenantTenantIdRouteImport } from './routes/ import { Route as LayoutAuthenticatedSettingsSecurityRouteImport } from './routes/_layout/_authenticated/settings/security' import { Route as LayoutAuthenticatedSettingsProfileRouteImport } from './routes/_layout/_authenticated/settings/profile' import { Route as LayoutAuthenticatedSettingsAuthMethodsRouteImport } from './routes/_layout/_authenticated/settings/auth-methods' -import { Route as LayoutAuthenticatedOrganizationsNewRouteImport } from './routes/_layout/_authenticated/organizations/new' -import { Route as LayoutAuthenticatedOrganizationsSlugRouteImport } from './routes/_layout/_authenticated/organizations/$slug' -import { Route as LayoutAuthenticatedAcceptInvitationIdRouteImport } from './routes/_layout/_authenticated/accept-invitation.$id' +import { Route as LayoutAuthenticatedOrgsNewRouteImport } from './routes/_layout/_authenticated/orgs/new' +import { Route as LayoutAuthenticatedOrgsSlugRouteImport } from './routes/_layout/_authenticated/orgs/$slug' import { Route as LayoutAdminAdminSystemRouteImport } from './routes/_layout/_admin/admin/system' import { Route as LayoutPublicAppsAccountIdIndexRouteImport } from './routes/_layout/_public/apps/$accountId/index' import { Route as LayoutPublicAppsAccountIdGatewayIdRouteImport } from './routes/_layout/_public/apps/$accountId/$gatewayId' +import { Route as LayoutAuthenticatedOrgsInvitesIdRouteImport } from './routes/_layout/_authenticated/orgs/invites.$id' const LayoutRoute = LayoutRouteImport.update({ id: '/_layout', @@ -53,6 +54,10 @@ const LayoutAuthenticatedRoute = LayoutAuthenticatedRouteImport.update({ id: '/_authenticated', getParentRoute: () => LayoutRoute, } as any) +const LayoutAnonRoute = LayoutAnonRouteImport.update({ + id: '/_anon', + getParentRoute: () => LayoutRoute, +} as any) const LayoutAdminRoute = LayoutAdminRouteImport.update({ id: '/_admin', getParentRoute: () => LayoutRoute, @@ -67,11 +72,6 @@ const LayoutPublicSkillRoute = LayoutPublicSkillRouteImport.update({ path: '/skill', getParentRoute: () => LayoutPublicRoute, } as any) -const LayoutPublicLoginRoute = LayoutPublicLoginRouteImport.update({ - id: '/login', - path: '/login', - getParentRoute: () => LayoutPublicRoute, -} as any) const LayoutPublicAboutRoute = LayoutPublicAboutRouteImport.update({ id: '/about', path: '/about', @@ -94,6 +94,11 @@ const LayoutAuthenticatedDashboardRoute = path: '/dashboard', getParentRoute: () => LayoutAuthenticatedRoute, } as any) +const LayoutAnonLoginRoute = LayoutAnonLoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => LayoutAnonRoute, +} as any) const LayoutAdminAdminRoute = LayoutAdminAdminRouteImport.update({ id: '/admin', path: '/admin', @@ -115,10 +120,10 @@ const LayoutAuthenticatedSettingsIndexRoute = path: '/', getParentRoute: () => LayoutAuthenticatedSettingsRoute, } as any) -const LayoutAuthenticatedOrganizationsIndexRoute = - LayoutAuthenticatedOrganizationsIndexRouteImport.update({ - id: '/organizations/', - path: '/organizations/', +const LayoutAuthenticatedOrgsIndexRoute = + LayoutAuthenticatedOrgsIndexRouteImport.update({ + id: '/orgs/', + path: '/orgs/', getParentRoute: () => LayoutAuthenticatedRoute, } as any) const LayoutAdminAdminIndexRoute = LayoutAdminAdminIndexRouteImport.update({ @@ -173,22 +178,16 @@ const LayoutAuthenticatedSettingsAuthMethodsRoute = path: '/auth-methods', getParentRoute: () => LayoutAuthenticatedSettingsRoute, } as any) -const LayoutAuthenticatedOrganizationsNewRoute = - LayoutAuthenticatedOrganizationsNewRouteImport.update({ - id: '/organizations/new', - path: '/organizations/new', +const LayoutAuthenticatedOrgsNewRoute = + LayoutAuthenticatedOrgsNewRouteImport.update({ + id: '/orgs/new', + path: '/orgs/new', getParentRoute: () => LayoutAuthenticatedRoute, } as any) -const LayoutAuthenticatedOrganizationsSlugRoute = - LayoutAuthenticatedOrganizationsSlugRouteImport.update({ - id: '/organizations/$slug', - path: '/organizations/$slug', - getParentRoute: () => LayoutAuthenticatedRoute, - } as any) -const LayoutAuthenticatedAcceptInvitationIdRoute = - LayoutAuthenticatedAcceptInvitationIdRouteImport.update({ - id: '/accept-invitation/$id', - path: '/accept-invitation/$id', +const LayoutAuthenticatedOrgsSlugRoute = + LayoutAuthenticatedOrgsSlugRouteImport.update({ + id: '/orgs/$slug', + path: '/orgs/$slug', getParentRoute: () => LayoutAuthenticatedRoute, } as any) const LayoutAdminAdminSystemRoute = LayoutAdminAdminSystemRouteImport.update({ @@ -208,20 +207,25 @@ const LayoutPublicAppsAccountIdGatewayIdRoute = path: '/apps/$accountId/$gatewayId', getParentRoute: () => LayoutPublicRoute, } as any) +const LayoutAuthenticatedOrgsInvitesIdRoute = + LayoutAuthenticatedOrgsInvitesIdRouteImport.update({ + id: '/orgs/invites/$id', + path: '/orgs/invites/$id', + getParentRoute: () => LayoutAuthenticatedRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof LayoutPublicIndexRoute '/admin': typeof LayoutAdminAdminRouteWithChildren + '/login': typeof LayoutAnonLoginRoute '/dashboard': typeof LayoutAuthenticatedDashboardRoute '/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren '/$accountId': typeof LayoutPublicAccountIdRoute '/about': typeof LayoutPublicAboutRoute - '/login': typeof LayoutPublicLoginRoute '/skill': typeof LayoutPublicSkillRoute '/admin/system': typeof LayoutAdminAdminSystemRoute - '/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute - '/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute - '/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute + '/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute + '/orgs/new': typeof LayoutAuthenticatedOrgsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute '/settings/profile': typeof LayoutAuthenticatedSettingsProfileRoute '/settings/security': typeof LayoutAuthenticatedSettingsSecurityRoute @@ -231,24 +235,24 @@ export interface FileRoutesByFullPath { '/things/$thingId': typeof LayoutPublicThingsThingIdRoute '/things/live': typeof LayoutPublicThingsLiveRoute '/admin/': typeof LayoutAdminAdminIndexRoute - '/organizations/': typeof LayoutAuthenticatedOrganizationsIndexRoute + '/orgs/': typeof LayoutAuthenticatedOrgsIndexRoute '/settings/': typeof LayoutAuthenticatedSettingsIndexRoute '/apps/': typeof LayoutPublicAppsIndexRoute '/things/': typeof LayoutPublicThingsIndexRoute + '/orgs/invites/$id': typeof LayoutAuthenticatedOrgsInvitesIdRoute '/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute '/apps/$accountId/': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRoutesByTo { '/': typeof LayoutPublicIndexRoute + '/login': typeof LayoutAnonLoginRoute '/dashboard': typeof LayoutAuthenticatedDashboardRoute '/$accountId': typeof LayoutPublicAccountIdRoute '/about': typeof LayoutPublicAboutRoute - '/login': typeof LayoutPublicLoginRoute '/skill': typeof LayoutPublicSkillRoute '/admin/system': typeof LayoutAdminAdminSystemRoute - '/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute - '/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute - '/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute + '/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute + '/orgs/new': typeof LayoutAuthenticatedOrgsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute '/settings/profile': typeof LayoutAuthenticatedSettingsProfileRoute '/settings/security': typeof LayoutAuthenticatedSettingsSecurityRoute @@ -258,10 +262,11 @@ export interface FileRoutesByTo { '/things/$thingId': typeof LayoutPublicThingsThingIdRoute '/things/live': typeof LayoutPublicThingsLiveRoute '/admin': typeof LayoutAdminAdminIndexRoute - '/organizations': typeof LayoutAuthenticatedOrganizationsIndexRoute + '/orgs': typeof LayoutAuthenticatedOrgsIndexRoute '/settings': typeof LayoutAuthenticatedSettingsIndexRoute '/apps': typeof LayoutPublicAppsIndexRoute '/things': typeof LayoutPublicThingsIndexRoute + '/orgs/invites/$id': typeof LayoutAuthenticatedOrgsInvitesIdRoute '/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute '/apps/$accountId': typeof LayoutPublicAppsAccountIdIndexRoute } @@ -269,20 +274,20 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/_layout': typeof LayoutRouteWithChildren '/_layout/_admin': typeof LayoutAdminRouteWithChildren + '/_layout/_anon': typeof LayoutAnonRouteWithChildren '/_layout/_authenticated': typeof LayoutAuthenticatedRouteWithChildren '/_layout/_public': typeof LayoutPublicRouteWithChildren '/_layout/_admin/admin': typeof LayoutAdminAdminRouteWithChildren + '/_layout/_anon/login': typeof LayoutAnonLoginRoute '/_layout/_authenticated/dashboard': typeof LayoutAuthenticatedDashboardRoute '/_layout/_authenticated/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren '/_layout/_public/$accountId': typeof LayoutPublicAccountIdRoute '/_layout/_public/about': typeof LayoutPublicAboutRoute - '/_layout/_public/login': typeof LayoutPublicLoginRoute '/_layout/_public/skill': typeof LayoutPublicSkillRoute '/_layout/_public/': typeof LayoutPublicIndexRoute '/_layout/_admin/admin/system': typeof LayoutAdminAdminSystemRoute - '/_layout/_authenticated/accept-invitation/$id': typeof LayoutAuthenticatedAcceptInvitationIdRoute - '/_layout/_authenticated/organizations/$slug': typeof LayoutAuthenticatedOrganizationsSlugRoute - '/_layout/_authenticated/organizations/new': typeof LayoutAuthenticatedOrganizationsNewRoute + '/_layout/_authenticated/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute + '/_layout/_authenticated/orgs/new': typeof LayoutAuthenticatedOrgsNewRoute '/_layout/_authenticated/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute '/_layout/_authenticated/settings/profile': typeof LayoutAuthenticatedSettingsProfileRoute '/_layout/_authenticated/settings/security': typeof LayoutAuthenticatedSettingsSecurityRoute @@ -292,10 +297,11 @@ export interface FileRoutesById { '/_layout/_public/things/$thingId': typeof LayoutPublicThingsThingIdRoute '/_layout/_public/things/live': typeof LayoutPublicThingsLiveRoute '/_layout/_admin/admin/': typeof LayoutAdminAdminIndexRoute - '/_layout/_authenticated/organizations/': typeof LayoutAuthenticatedOrganizationsIndexRoute + '/_layout/_authenticated/orgs/': typeof LayoutAuthenticatedOrgsIndexRoute '/_layout/_authenticated/settings/': typeof LayoutAuthenticatedSettingsIndexRoute '/_layout/_public/apps/': typeof LayoutPublicAppsIndexRoute '/_layout/_public/things/': typeof LayoutPublicThingsIndexRoute + '/_layout/_authenticated/orgs/invites/$id': typeof LayoutAuthenticatedOrgsInvitesIdRoute '/_layout/_public/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute '/_layout/_public/apps/$accountId/': typeof LayoutPublicAppsAccountIdIndexRoute } @@ -304,16 +310,15 @@ export interface FileRouteTypes { fullPaths: | '/' | '/admin' + | '/login' | '/dashboard' | '/settings' | '/$accountId' | '/about' - | '/login' | '/skill' | '/admin/system' - | '/accept-invitation/$id' - | '/organizations/$slug' - | '/organizations/new' + | '/orgs/$slug' + | '/orgs/new' | '/settings/auth-methods' | '/settings/profile' | '/settings/security' @@ -323,24 +328,24 @@ export interface FileRouteTypes { | '/things/$thingId' | '/things/live' | '/admin/' - | '/organizations/' + | '/orgs/' | '/settings/' | '/apps/' | '/things/' + | '/orgs/invites/$id' | '/apps/$accountId/$gatewayId' | '/apps/$accountId/' fileRoutesByTo: FileRoutesByTo to: | '/' + | '/login' | '/dashboard' | '/$accountId' | '/about' - | '/login' | '/skill' | '/admin/system' - | '/accept-invitation/$id' - | '/organizations/$slug' - | '/organizations/new' + | '/orgs/$slug' + | '/orgs/new' | '/settings/auth-methods' | '/settings/profile' | '/settings/security' @@ -350,30 +355,31 @@ export interface FileRouteTypes { | '/things/$thingId' | '/things/live' | '/admin' - | '/organizations' + | '/orgs' | '/settings' | '/apps' | '/things' + | '/orgs/invites/$id' | '/apps/$accountId/$gatewayId' | '/apps/$accountId' id: | '__root__' | '/_layout' | '/_layout/_admin' + | '/_layout/_anon' | '/_layout/_authenticated' | '/_layout/_public' | '/_layout/_admin/admin' + | '/_layout/_anon/login' | '/_layout/_authenticated/dashboard' | '/_layout/_authenticated/settings' | '/_layout/_public/$accountId' | '/_layout/_public/about' - | '/_layout/_public/login' | '/_layout/_public/skill' | '/_layout/_public/' | '/_layout/_admin/admin/system' - | '/_layout/_authenticated/accept-invitation/$id' - | '/_layout/_authenticated/organizations/$slug' - | '/_layout/_authenticated/organizations/new' + | '/_layout/_authenticated/orgs/$slug' + | '/_layout/_authenticated/orgs/new' | '/_layout/_authenticated/settings/auth-methods' | '/_layout/_authenticated/settings/profile' | '/_layout/_authenticated/settings/security' @@ -383,10 +389,11 @@ export interface FileRouteTypes { | '/_layout/_public/things/$thingId' | '/_layout/_public/things/live' | '/_layout/_admin/admin/' - | '/_layout/_authenticated/organizations/' + | '/_layout/_authenticated/orgs/' | '/_layout/_authenticated/settings/' | '/_layout/_public/apps/' | '/_layout/_public/things/' + | '/_layout/_authenticated/orgs/invites/$id' | '/_layout/_public/apps/$accountId/$gatewayId' | '/_layout/_public/apps/$accountId/' fileRoutesById: FileRoutesById @@ -418,6 +425,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedRouteImport parentRoute: typeof LayoutRoute } + '/_layout/_anon': { + id: '/_layout/_anon' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutAnonRouteImport + parentRoute: typeof LayoutRoute + } '/_layout/_admin': { id: '/_layout/_admin' path: '' @@ -439,13 +453,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicSkillRouteImport parentRoute: typeof LayoutPublicRoute } - '/_layout/_public/login': { - id: '/_layout/_public/login' - path: '/login' - fullPath: '/login' - preLoaderRoute: typeof LayoutPublicLoginRouteImport - parentRoute: typeof LayoutPublicRoute - } '/_layout/_public/about': { id: '/_layout/_public/about' path: '/about' @@ -474,6 +481,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedDashboardRouteImport parentRoute: typeof LayoutAuthenticatedRoute } + '/_layout/_anon/login': { + id: '/_layout/_anon/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LayoutAnonLoginRouteImport + parentRoute: typeof LayoutAnonRoute + } '/_layout/_admin/admin': { id: '/_layout/_admin/admin' path: '/admin' @@ -502,11 +516,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedSettingsIndexRouteImport parentRoute: typeof LayoutAuthenticatedSettingsRoute } - '/_layout/_authenticated/organizations/': { - id: '/_layout/_authenticated/organizations/' - path: '/organizations' - fullPath: '/organizations/' - preLoaderRoute: typeof LayoutAuthenticatedOrganizationsIndexRouteImport + '/_layout/_authenticated/orgs/': { + id: '/_layout/_authenticated/orgs/' + path: '/orgs' + fullPath: '/orgs/' + preLoaderRoute: typeof LayoutAuthenticatedOrgsIndexRouteImport parentRoute: typeof LayoutAuthenticatedRoute } '/_layout/_admin/admin/': { @@ -572,25 +586,18 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedSettingsAuthMethodsRouteImport parentRoute: typeof LayoutAuthenticatedSettingsRoute } - '/_layout/_authenticated/organizations/new': { - id: '/_layout/_authenticated/organizations/new' - path: '/organizations/new' - fullPath: '/organizations/new' - preLoaderRoute: typeof LayoutAuthenticatedOrganizationsNewRouteImport + '/_layout/_authenticated/orgs/new': { + id: '/_layout/_authenticated/orgs/new' + path: '/orgs/new' + fullPath: '/orgs/new' + preLoaderRoute: typeof LayoutAuthenticatedOrgsNewRouteImport parentRoute: typeof LayoutAuthenticatedRoute } - '/_layout/_authenticated/organizations/$slug': { - id: '/_layout/_authenticated/organizations/$slug' - path: '/organizations/$slug' - fullPath: '/organizations/$slug' - preLoaderRoute: typeof LayoutAuthenticatedOrganizationsSlugRouteImport - parentRoute: typeof LayoutAuthenticatedRoute - } - '/_layout/_authenticated/accept-invitation/$id': { - id: '/_layout/_authenticated/accept-invitation/$id' - path: '/accept-invitation/$id' - fullPath: '/accept-invitation/$id' - preLoaderRoute: typeof LayoutAuthenticatedAcceptInvitationIdRouteImport + '/_layout/_authenticated/orgs/$slug': { + id: '/_layout/_authenticated/orgs/$slug' + path: '/orgs/$slug' + fullPath: '/orgs/$slug' + preLoaderRoute: typeof LayoutAuthenticatedOrgsSlugRouteImport parentRoute: typeof LayoutAuthenticatedRoute } '/_layout/_admin/admin/system': { @@ -614,6 +621,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicAppsAccountIdGatewayIdRouteImport parentRoute: typeof LayoutPublicRoute } + '/_layout/_authenticated/orgs/invites/$id': { + id: '/_layout/_authenticated/orgs/invites/$id' + path: '/orgs/invites/$id' + fullPath: '/orgs/invites/$id' + preLoaderRoute: typeof LayoutAuthenticatedOrgsInvitesIdRouteImport + parentRoute: typeof LayoutAuthenticatedRoute + } } } @@ -642,6 +656,18 @@ const LayoutAdminRouteWithChildren = LayoutAdminRoute._addFileChildren( LayoutAdminRouteChildren, ) +interface LayoutAnonRouteChildren { + LayoutAnonLoginRoute: typeof LayoutAnonLoginRoute +} + +const LayoutAnonRouteChildren: LayoutAnonRouteChildren = { + LayoutAnonLoginRoute: LayoutAnonLoginRoute, +} + +const LayoutAnonRouteWithChildren = LayoutAnonRoute._addFileChildren( + LayoutAnonRouteChildren, +) + interface LayoutAuthenticatedSettingsRouteChildren { LayoutAuthenticatedSettingsAuthMethodsRoute: typeof LayoutAuthenticatedSettingsAuthMethodsRoute LayoutAuthenticatedSettingsProfileRoute: typeof LayoutAuthenticatedSettingsProfileRoute @@ -669,31 +695,27 @@ const LayoutAuthenticatedSettingsRouteWithChildren = interface LayoutAuthenticatedRouteChildren { LayoutAuthenticatedDashboardRoute: typeof LayoutAuthenticatedDashboardRoute LayoutAuthenticatedSettingsRoute: typeof LayoutAuthenticatedSettingsRouteWithChildren - LayoutAuthenticatedAcceptInvitationIdRoute: typeof LayoutAuthenticatedAcceptInvitationIdRoute - LayoutAuthenticatedOrganizationsSlugRoute: typeof LayoutAuthenticatedOrganizationsSlugRoute - LayoutAuthenticatedOrganizationsNewRoute: typeof LayoutAuthenticatedOrganizationsNewRoute + LayoutAuthenticatedOrgsSlugRoute: typeof LayoutAuthenticatedOrgsSlugRoute + LayoutAuthenticatedOrgsNewRoute: typeof LayoutAuthenticatedOrgsNewRoute LayoutAuthenticatedTenantTenantIdRoute: typeof LayoutAuthenticatedTenantTenantIdRoute LayoutAuthenticatedTenantNewRoute: typeof LayoutAuthenticatedTenantNewRoute LayoutAuthenticatedThingsNewRoute: typeof LayoutAuthenticatedThingsNewRoute - LayoutAuthenticatedOrganizationsIndexRoute: typeof LayoutAuthenticatedOrganizationsIndexRoute + LayoutAuthenticatedOrgsIndexRoute: typeof LayoutAuthenticatedOrgsIndexRoute + LayoutAuthenticatedOrgsInvitesIdRoute: typeof LayoutAuthenticatedOrgsInvitesIdRoute } const LayoutAuthenticatedRouteChildren: LayoutAuthenticatedRouteChildren = { LayoutAuthenticatedDashboardRoute: LayoutAuthenticatedDashboardRoute, LayoutAuthenticatedSettingsRoute: LayoutAuthenticatedSettingsRouteWithChildren, - LayoutAuthenticatedAcceptInvitationIdRoute: - LayoutAuthenticatedAcceptInvitationIdRoute, - LayoutAuthenticatedOrganizationsSlugRoute: - LayoutAuthenticatedOrganizationsSlugRoute, - LayoutAuthenticatedOrganizationsNewRoute: - LayoutAuthenticatedOrganizationsNewRoute, + LayoutAuthenticatedOrgsSlugRoute: LayoutAuthenticatedOrgsSlugRoute, + LayoutAuthenticatedOrgsNewRoute: LayoutAuthenticatedOrgsNewRoute, LayoutAuthenticatedTenantTenantIdRoute: LayoutAuthenticatedTenantTenantIdRoute, LayoutAuthenticatedTenantNewRoute: LayoutAuthenticatedTenantNewRoute, LayoutAuthenticatedThingsNewRoute: LayoutAuthenticatedThingsNewRoute, - LayoutAuthenticatedOrganizationsIndexRoute: - LayoutAuthenticatedOrganizationsIndexRoute, + LayoutAuthenticatedOrgsIndexRoute: LayoutAuthenticatedOrgsIndexRoute, + LayoutAuthenticatedOrgsInvitesIdRoute: LayoutAuthenticatedOrgsInvitesIdRoute, } const LayoutAuthenticatedRouteWithChildren = @@ -702,7 +724,6 @@ const LayoutAuthenticatedRouteWithChildren = interface LayoutPublicRouteChildren { LayoutPublicAccountIdRoute: typeof LayoutPublicAccountIdRoute LayoutPublicAboutRoute: typeof LayoutPublicAboutRoute - LayoutPublicLoginRoute: typeof LayoutPublicLoginRoute LayoutPublicSkillRoute: typeof LayoutPublicSkillRoute LayoutPublicIndexRoute: typeof LayoutPublicIndexRoute LayoutPublicThingsThingIdRoute: typeof LayoutPublicThingsThingIdRoute @@ -716,7 +737,6 @@ interface LayoutPublicRouteChildren { const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { LayoutPublicAccountIdRoute: LayoutPublicAccountIdRoute, LayoutPublicAboutRoute: LayoutPublicAboutRoute, - LayoutPublicLoginRoute: LayoutPublicLoginRoute, LayoutPublicSkillRoute: LayoutPublicSkillRoute, LayoutPublicIndexRoute: LayoutPublicIndexRoute, LayoutPublicThingsThingIdRoute: LayoutPublicThingsThingIdRoute, @@ -734,12 +754,14 @@ const LayoutPublicRouteWithChildren = LayoutPublicRoute._addFileChildren( interface LayoutRouteChildren { LayoutAdminRoute: typeof LayoutAdminRouteWithChildren + LayoutAnonRoute: typeof LayoutAnonRouteWithChildren LayoutAuthenticatedRoute: typeof LayoutAuthenticatedRouteWithChildren LayoutPublicRoute: typeof LayoutPublicRouteWithChildren } const LayoutRouteChildren: LayoutRouteChildren = { LayoutAdminRoute: LayoutAdminRouteWithChildren, + LayoutAnonRoute: LayoutAnonRouteWithChildren, LayoutAuthenticatedRoute: LayoutAuthenticatedRouteWithChildren, LayoutPublicRoute: LayoutPublicRouteWithChildren, } diff --git a/ui/src/routes/_layout/_admin/admin.tsx b/ui/src/routes/_layout/_admin/admin.tsx index e57fd806..9fee1727 100644 --- a/ui/src/routes/_layout/_admin/admin.tsx +++ b/ui/src/routes/_layout/_admin/admin.tsx @@ -55,7 +55,7 @@ function AdminPage() { home </Link> <Link - to="/organizations" + to="/orgs" className="h-10 px-4 inline-flex items-center gap-1.5 text-sm font-medium border-2 border-outset border-border-strong bg-card text-foreground shadow-sm hover:shadow-md active:border-inset active:shadow-none transition-all duration-200 ease-out rounded-[12px]" > organizations @@ -93,7 +93,7 @@ function AdminPage() { label="Organization" value={ <Link - to="/organizations/$slug" + to="/orgs/$slug" params={{ slug: tenant.subdomain }} className="text-foreground hover:underline font-mono" > diff --git a/ui/src/routes/_layout/_admin/admin/index.tsx b/ui/src/routes/_layout/_admin/admin/index.tsx index fd5a7378..137d4e33 100644 --- a/ui/src/routes/_layout/_admin/admin/index.tsx +++ b/ui/src/routes/_layout/_admin/admin/index.tsx @@ -53,7 +53,7 @@ function AdminDashboard() { Manage organizations, members, roles, and invitations. </p> <Button asChild variant="outline" size="sm"> - <Link to="/organizations"> + <Link to="/orgs"> <Users className="h-3.5 w-3.5" /> open organizations </Link> @@ -105,7 +105,7 @@ function AdminDashboard() { there. </p> <Link - to="/organizations/$slug" + to="/orgs/$slug" params={{ slug: tenant.subdomain }} className="h-9 px-3 inline-flex items-center gap-1.5 text-xs font-medium border-2 border-outset border-border-strong bg-card text-foreground shadow-sm hover:shadow-md active:border-inset active:shadow-none transition-all duration-200 ease-out rounded-[10px]" > diff --git a/ui/src/routes/_layout/_anon.tsx b/ui/src/routes/_layout/_anon.tsx new file mode 100644 index 00000000..da38e147 --- /dev/null +++ b/ui/src/routes/_layout/_anon.tsx @@ -0,0 +1,57 @@ +import { createFileRoute, Link, Outlet, redirect, useRouterState } from "@tanstack/react-router"; +import { getAppName, sessionQueryOptions } from "@/app"; +import { ThemeToggle } from "@/components/theme-toggle"; + +export const Route = createFileRoute("/_layout/_anon")({ + beforeLoad: async ({ context }) => { + const { queryClient, authClient } = context; + const initialSession = context.session; + const session = initialSession ?? queryClient.getQueryData( + sessionQueryOptions(authClient, initialSession).queryKey, + ); + if (session?.user) { + throw redirect({ to: "/dashboard", search: {} }); + } + }, + component: AnonLayout, +}); + +function AnonLayout() { + const { runtimeConfig } = Route.useRouteContext(); + const appName = getAppName(runtimeConfig); + const pathname = useRouterState({ select: (s) => s.location.pathname }); + + return ( + <div className="flex-1 flex flex-col min-h-0"> + <header className="shrink-0 bg-card/50 border-b border-border transition-all duration-200 overflow-hidden h-12"> + <div className="flex items-center justify-between px-4 sm:px-6 h-12"> + <Link + to="/" + aria-label={`${appName} home`} + className="flex items-center justify-center w-10 h-10 transition-opacity duration-200 hover:opacity-70" + > + <svg + viewBox="0 0 24 24" + fill="currentColor" + className="w-5 h-5 text-foreground" + aria-label={`${appName} logo`} + > + <title>{appName} + + + + +
+ +
+
+ + +
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/routes/_layout/_public/login.tsx b/ui/src/routes/_layout/_anon/login.tsx similarity index 98% rename from ui/src/routes/_layout/_public/login.tsx rename to ui/src/routes/_layout/_anon/login.tsx index abc68a82..fb37f0ee 100644 --- a/ui/src/routes/_layout/_public/login.tsx +++ b/ui/src/routes/_layout/_anon/login.tsx @@ -11,7 +11,7 @@ type SearchParams = { redirect?: string; }; -export const Route = createFileRoute("/_layout/_public/login")({ +export const Route = createFileRoute("/_layout/_anon/login")({ ssr: false, validateSearch: (search: Record): SearchParams => ({ redirect: typeof search.redirect === "string" ? search.redirect : undefined, @@ -200,7 +200,7 @@ function LoginPage() {
diff --git a/ui/src/routes/_layout/_authenticated.tsx b/ui/src/routes/_layout/_authenticated.tsx index 72eae14a..75ca082d 100644 --- a/ui/src/routes/_layout/_authenticated.tsx +++ b/ui/src/routes/_layout/_authenticated.tsx @@ -1,5 +1,5 @@ import { createFileRoute, Link, Outlet, redirect, useRouterState } from "@tanstack/react-router"; -import { ClipboardList, Compass, Globe, Home, Menu, MessageSquare, Shield } from "lucide-react"; +import { ClipboardList, Compass, Globe, Home, Menu, Shield } from "lucide-react"; import { useState } from "react"; import type { SessionData } from "@/app"; import { getAccount, getActiveRuntime, getAppName, sessionQueryOptions } from "@/app"; @@ -96,7 +96,6 @@ function AuthenticatedLayout() { const sidebarItems: SidebarItem[] = [ { icon: Home, label: "dashboard", to: "/dashboard", roleRequired: "anon" }, { icon: Shield, label: "admin", to: "/admin", roleRequired: "admin" }, - { icon: MessageSquare, label: "nostr", to: "/nostr", roleRequired: "admin" }, ]; const visibleItems = filterSidebarByRole(sidebarItems, getUserRole(true, isAdmin)); diff --git a/ui/src/routes/_layout/_authenticated/organizations/$slug.tsx b/ui/src/routes/_layout/_authenticated/orgs/$slug.tsx similarity index 99% rename from ui/src/routes/_layout/_authenticated/organizations/$slug.tsx rename to ui/src/routes/_layout/_authenticated/orgs/$slug.tsx index e95cc943..ae78e315 100644 --- a/ui/src/routes/_layout/_authenticated/organizations/$slug.tsx +++ b/ui/src/routes/_layout/_authenticated/orgs/$slug.tsx @@ -59,7 +59,7 @@ const orgMembersQueryKey = (orgId: string) => ["org-members", orgId] as const; const orgInvitationsQueryKey = (orgId: string) => ["org-invitations", orgId] as const; const orgApiKeysQueryKey = (orgId: string) => ["org-api-keys", orgId] as const; -export const Route = createFileRoute("/_layout/_authenticated/organizations/$slug")({ +export const Route = createFileRoute("/_layout/_authenticated/orgs/$slug")({ head: () => ({ title: "Organization | auth.everything.dev", meta: [{ name: "description", content: "Manage organization details and members." }], @@ -328,7 +328,7 @@ function OrganizationDetail() { onSuccess: async () => { toast.success("You have left the organization"); await queryClient.invalidateQueries({ queryKey: ["organizations"] }); - await router.navigate({ to: "/organizations" }); + await router.navigate({ to: "/orgs" }); }, onError: (error: Error) => toast.error(error.message || "Failed to leave organization"), }); @@ -341,7 +341,7 @@ function OrganizationDetail() { onSuccess: async () => { toast.success("Organization deleted"); await queryClient.invalidateQueries({ queryKey: ["organizations"] }); - await router.navigate({ to: "/organizations" }); + await router.navigate({ to: "/orgs" }); }, onError: (error: Error) => toast.error(error.message || "Failed to delete organization"), }); @@ -369,7 +369,7 @@ function OrganizationDetail() { description="This organization does not exist or you do not have access." action={ } /> @@ -383,7 +383,7 @@ function OrganizationDetail() {
- + Organizations / @@ -657,7 +657,7 @@ function OrganizationDetail() {
- ); + ) } function Chip({ children, accent }: { children: React.ReactNode; accent?: boolean }) { diff --git a/ui/src/routes/_layout/_authenticated/organizations/index.tsx b/ui/src/routes/_layout/_authenticated/orgs/index.tsx similarity index 98% rename from ui/src/routes/_layout/_authenticated/organizations/index.tsx rename to ui/src/routes/_layout/_authenticated/orgs/index.tsx index 96e4c576..ae09675a 100644 --- a/ui/src/routes/_layout/_authenticated/organizations/index.tsx +++ b/ui/src/routes/_layout/_authenticated/orgs/index.tsx @@ -18,7 +18,7 @@ type UserInvitationsResponse = Awaited< >; type UserInvitationItem = NonNullable[number]; -export const Route = createFileRoute("/_layout/_authenticated/organizations/")({ +export const Route = createFileRoute("/_layout/_authenticated/orgs/")({ head: () => ({ title: "Organizations | auth.everything.dev", meta: [{ name: "description", content: "Manage your organizations and teams." }], @@ -114,7 +114,7 @@ function OrganizationsList() { await queryClient.refetchQueries({ queryKey: ["organizations"] }); if (invitation.organizationSlug) { await router.navigate({ - to: "/organizations/$slug", + to: "/orgs/$slug", params: { slug: invitation.organizationSlug }, }); } @@ -164,7 +164,7 @@ function OrganizationsList() { @@ -253,7 +253,7 @@ function OrganizationsList() {

No organizations yet.

create your first org @@ -303,7 +303,7 @@ function OrganizationsList() {
diff --git a/ui/src/routes/_layout/_authenticated/accept-invitation.$id.tsx b/ui/src/routes/_layout/_authenticated/orgs/invites.$id.tsx similarity index 95% rename from ui/src/routes/_layout/_authenticated/accept-invitation.$id.tsx rename to ui/src/routes/_layout/_authenticated/orgs/invites.$id.tsx index e7941f9a..b1c997e1 100644 --- a/ui/src/routes/_layout/_authenticated/accept-invitation.$id.tsx +++ b/ui/src/routes/_layout/_authenticated/orgs/invites.$id.tsx @@ -5,7 +5,7 @@ import { toast } from "sonner"; import { getAppName, useAuthClient } from "@/app"; import { Badge, Button, Card, CardContent } from "@/components"; -export const Route = createFileRoute("/_layout/_authenticated/accept-invitation/$id")({ +export const Route = createFileRoute("/_layout/_authenticated/orgs/invites/$id")({ head: () => ({ meta: [{ title: `Accept Invitation | ${getAppName()}` }], }), @@ -63,7 +63,7 @@ function AcceptInvitation() { ]); await queryClient.refetchQueries({ queryKey: ["organizations"] }); await router.navigate({ - to: "/organizations/$slug", + to: "/orgs/$slug", params: { slug: invitation?.organizationSlug ?? "" }, }); }, @@ -78,7 +78,7 @@ function AcceptInvitation() { onSuccess: async () => { toast.success("Invitation declined"); await queryClient.invalidateQueries({ queryKey: ["user-invitations"] }); - await router.navigate({ to: "/organizations" }); + await router.navigate({ to: "/orgs" }); }, onError: (error: Error) => toast.error(error.message || "Failed to decline invitation"), }); @@ -100,7 +100,7 @@ function AcceptInvitation() { This invitation does not exist, has expired, or is not addressed to your account.

@@ -166,7 +166,7 @@ function AcceptInvitation() {
diff --git a/ui/src/routes/_layout/_authenticated/organizations/new.tsx b/ui/src/routes/_layout/_authenticated/orgs/new.tsx similarity index 96% rename from ui/src/routes/_layout/_authenticated/organizations/new.tsx rename to ui/src/routes/_layout/_authenticated/orgs/new.tsx index 664b81e7..b08c6b31 100644 --- a/ui/src/routes/_layout/_authenticated/organizations/new.tsx +++ b/ui/src/routes/_layout/_authenticated/orgs/new.tsx @@ -7,7 +7,7 @@ import { useAuthClient } from "@/app"; import { Button, Card, CardContent, Input } from "@/components"; import { PageContainer } from "@/components/layout/page-container"; -export const Route = createFileRoute("/_layout/_authenticated/organizations/new")({ +export const Route = createFileRoute("/_layout/_authenticated/orgs/new")({ head: () => ({ title: "New Organization | auth.everything.dev", meta: [{ name: "description", content: "Create a new organization." }], @@ -37,7 +37,7 @@ function NewOrganization() { await queryClient.refetchQueries({ queryKey: ["organizations"] }); if (data?.slug) { await router.navigate({ - to: "/organizations/$slug", + to: "/orgs/$slug", params: { slug: data.slug }, }); } @@ -76,7 +76,7 @@ function NewOrganization() {
@@ -122,7 +122,7 @@ function NewOrganization() {
- ); + ) } function Field({ diff --git a/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx b/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx index 4a49f5c9..e9d6e78b 100644 --- a/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx +++ b/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx @@ -403,7 +403,7 @@ function TenantDetail() { there.

+ + + + Menu + +
+
+ {visibleItems.map((item) => { + const Icon = item.icon; + const active = isActive(item); + return ( + setDrawerOpen(false)} + className={cn( + "flex items-center gap-3 px-3 py-2 rounded-[10px] text-sm font-medium transition-colors", + active + ? "bg-foreground/10 text-foreground" + : "text-muted-foreground hover:text-foreground hover:bg-muted", + )} + > + + {item.label} + + ); + })} +
+
+
+ +
+
+ +
+
+ + + + ); +} + +function TabItem({ + to, + icon: Icon, + label, + active, +}: { + to: string; + icon: React.ComponentType<{ className?: string }>; + label: string; + active: boolean; +}) { + return ( + + + {label} + + ); +} diff --git a/ui/src/components/layout/simple-header.tsx b/ui/src/components/layout/simple-header.tsx new file mode 100644 index 00000000..033ed496 --- /dev/null +++ b/ui/src/components/layout/simple-header.tsx @@ -0,0 +1,40 @@ +import { Link } from "@tanstack/react-router"; +import type { ClientRuntimeConfig } from "@/app"; +import { getAppName } from "@/app"; +import { ThemeToggle } from "@/components/theme-toggle"; + +interface SimpleHeaderProps { + runtimeConfig?: Partial; + rightSlot?: React.ReactNode; +} + +export function SimpleHeader({ runtimeConfig, rightSlot }: SimpleHeaderProps) { + const appName = getAppName(runtimeConfig); + + return ( +
+
+ + + {appName} + + + + +
+ + {rightSlot} +
+
+
+ ); +} diff --git a/ui/src/components/ui/network-toggle.tsx b/ui/src/components/ui/network-toggle.tsx index bec227b9..4ceabfc4 100644 --- a/ui/src/components/ui/network-toggle.tsx +++ b/ui/src/components/ui/network-toggle.tsx @@ -1,33 +1,34 @@ -import { useQuery } from "@tanstack/react-query"; +import { Globe } from "lucide-react"; import { useAuthClient } from "@/app"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { sessionQueryOptions } from "@/lib/auth"; export function NetworkToggle() { const auth = useAuthClient(); - const { data: session } = useQuery(sessionQueryOptions(auth)); const supportedNetworks = auth.near.getSupportedNetworks(); const currentNetwork = auth.useActiveNetwork(); - if (session?.user) return null; if (supportedNetworks.length <= 1) return null; return ( -
- { - auth.near.setNetwork(network as "mainnet" | "testnet"); - }} - > - - {supportedNetworks.map((network) => ( - - {network === "mainnet" ? "Mainnet" : "Testnet"} - - ))} - - +
+ {supportedNetworks.map((network) => ( + + ))}
); } diff --git a/ui/src/components/user-nav.tsx b/ui/src/components/user-nav.tsx index 084aca2f..13f7ef2e 100644 --- a/ui/src/components/user-nav.tsx +++ b/ui/src/components/user-nav.tsx @@ -82,6 +82,7 @@ export function UserNav() { const handleOrgSwitch = async () => { await queryClient.invalidateQueries({ queryKey: ["session"] }); await queryClient.invalidateQueries({ queryKey: ["organizations"] }); + await router.invalidate(); }; const avatarSrc = resolveNearImageUrl(nearProfile?.image) ?? user.image ?? undefined; diff --git a/ui/src/lib/use-relayer-fund.ts b/ui/src/lib/use-relayer-fund.ts new file mode 100644 index 00000000..fa4c98bd --- /dev/null +++ b/ui/src/lib/use-relayer-fund.ts @@ -0,0 +1,111 @@ +import { type UseQueryResult, useQuery } from "@tanstack/react-query"; +import { useCallback, useMemo, useState } from "react"; +import { toast } from "sonner"; +import type { AuthClient } from "./auth"; +import { useAuthClient } from "./auth"; + +export interface RelayerInfoData { + accountId?: string; + mode?: "ephemeral" | "explicit"; + enabled?: boolean; + balance?: string; + available?: string; + network?: "mainnet" | "testnet"; +} + +export interface RelayerInfoResult { + data: RelayerInfoData | null; + refetch: () => Promise; +} + +export const relayerInfoQueryKey = ["relayer-info"] as const; + +export function useRelayerInfoQuery( + auth: AuthClient = useAuthClient(), +): UseQueryResult { + return useQuery({ + queryKey: relayerInfoQueryKey, + queryFn: async () => { + const { data } = await auth.near.getRelayerInfo(); + return (data ?? null) as RelayerInfoData | null; + }, + refetchInterval: 30_000, + }); +} + +export function useRelayerFund( + info: RelayerInfoData | null | undefined, + options?: { onSuccess?: () => void }, +) { + const auth = useAuthClient(); + const [amount, setAmount] = useState("5"); + const [sending, setSending] = useState(false); + + const parsedAmount = useMemo(() => { + const value = Number(amount); + if (!amount || Number.isNaN(value) || value <= 0) return null; + return value; + }, [amount]); + + const sendFund = useCallback(async () => { + const target = info?.accountId; + if (!target) { + toast.error("Relayer not configured on the server."); + return; + } + if (parsedAmount === null) { + toast.error("Enter a valid amount in NEAR."); + return; + } + const connected = await auth.near.ensureConnected(); + if (!connected) { + toast.error("Connect a NEAR wallet first"); + return; + } + const signer = auth.near.getAccountId(); + if (!signer) { + toast.error("Connect a NEAR wallet first"); + return; + } + setSending(true); + try { + const result = await auth.near + .getNearClient() + .transaction(signer) + .transfer(target, `${parsedAmount} NEAR`) + .send({ waitUntil: "FINAL" }); + toast.success("Relayer funded", { + description: result.transaction?.hash + ? `tx: ${result.transaction.hash}` + : `Sent ${parsedAmount} NEAR → ${target}`, + }); + options?.onSuccess?.(); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Funding failed"); + } finally { + setSending(false); + } + }, [auth, info?.accountId, parsedAmount, options]); + + return { + amount, + setAmount, + sending, + parsedAmount, + sendFund, + }; +} + +export function formatYocto(value: string | bigint | number | null | undefined): string | null { + if (value === null || value === undefined) return null; + try { + const v = typeof value === "string" ? BigInt(value) : BigInt(value); + if (v === 0n) return "0 NEAR"; + const whole = v / 10n ** 24n; + const frac = v % 10n ** 24n; + const fracStr = frac.toString().padStart(24, "0").slice(0, 4); + return fracStr === "0000" ? `${whole} NEAR` : `${whole}.${fracStr} NEAR`; + } catch { + return null; + } +} diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 89244e6f..337a84f4 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as LayoutAuthenticatedRouteImport } from './routes/_layout/_authe import { Route as LayoutAnonRouteImport } from './routes/_layout/_anon' import { Route as LayoutAdminRouteImport } from './routes/_layout/_admin' import { Route as LayoutPublicIndexRouteImport } from './routes/_layout/_public/index' +import { Route as LayoutPublicStakeRouteImport } from './routes/_layout/_public/stake' import { Route as LayoutPublicSkillRouteImport } from './routes/_layout/_public/skill' import { Route as LayoutPublicAboutRouteImport } from './routes/_layout/_public/about' import { Route as LayoutPublicAccountIdRouteImport } from './routes/_layout/_public/$accountId' @@ -32,17 +33,20 @@ import { Route as LayoutPublicThingsLiveRouteImport } from './routes/_layout/_pu import { Route as LayoutPublicThingsThingIdRouteImport } from './routes/_layout/_public/things/$thingId' import { Route as LayoutPublicAccountIdAppsRouteImport } from './routes/_layout/_public/$accountId/apps' import { Route as LayoutAuthenticatedThingsNewRouteImport } from './routes/_layout/_authenticated/things/new' -import { Route as LayoutAuthenticatedTenantNewRouteImport } from './routes/_layout/_authenticated/tenant/new' import { Route as LayoutAuthenticatedTenantTenantIdRouteImport } from './routes/_layout/_authenticated/tenant/$tenantId' import { Route as LayoutAuthenticatedSettingsSecurityRouteImport } from './routes/_layout/_authenticated/settings/security' import { Route as LayoutAuthenticatedSettingsProfileRouteImport } from './routes/_layout/_authenticated/settings/profile' import { Route as LayoutAuthenticatedSettingsAuthMethodsRouteImport } from './routes/_layout/_authenticated/settings/auth-methods' import { Route as LayoutAuthenticatedOrgsNewRouteImport } from './routes/_layout/_authenticated/orgs/new' import { Route as LayoutAuthenticatedOrgsSlugRouteImport } from './routes/_layout/_authenticated/orgs/$slug' +import { Route as LayoutAdminAdminTenantsRouteImport } from './routes/_layout/_admin/admin/tenants' import { Route as LayoutAdminAdminSystemRouteImport } from './routes/_layout/_admin/admin/system' +import { Route as LayoutAdminAdminRelayerRouteImport } from './routes/_layout/_admin/admin/relayer' import { Route as LayoutPublicAppsAccountIdIndexRouteImport } from './routes/_layout/_public/apps/$accountId/index' +import { Route as LayoutAdminAdminTenantsIndexRouteImport } from './routes/_layout/_admin/admin/tenants/index' import { Route as LayoutPublicAppsAccountIdGatewayIdRouteImport } from './routes/_layout/_public/apps/$accountId/$gatewayId' import { Route as LayoutAuthenticatedOrgsInvitesIdRouteImport } from './routes/_layout/_authenticated/orgs/invites.$id' +import { Route as LayoutAdminAdminTenantsNewRouteImport } from './routes/_layout/_admin/admin/tenants/new' const LayoutRoute = LayoutRouteImport.update({ id: '/_layout', @@ -69,6 +73,11 @@ const LayoutPublicIndexRoute = LayoutPublicIndexRouteImport.update({ path: '/', getParentRoute: () => LayoutPublicRoute, } as any) +const LayoutPublicStakeRoute = LayoutPublicStakeRouteImport.update({ + id: '/stake', + path: '/stake', + getParentRoute: () => LayoutPublicRoute, +} as any) const LayoutPublicSkillRoute = LayoutPublicSkillRouteImport.update({ id: '/skill', path: '/skill', @@ -162,12 +171,6 @@ const LayoutAuthenticatedThingsNewRoute = path: '/things/new', getParentRoute: () => LayoutAuthenticatedRoute, } as any) -const LayoutAuthenticatedTenantNewRoute = - LayoutAuthenticatedTenantNewRouteImport.update({ - id: '/tenant/new', - path: '/tenant/new', - getParentRoute: () => LayoutAuthenticatedRoute, - } as any) const LayoutAuthenticatedTenantTenantIdRoute = LayoutAuthenticatedTenantTenantIdRouteImport.update({ id: '/tenant/$tenantId', @@ -204,17 +207,33 @@ const LayoutAuthenticatedOrgsSlugRoute = path: '/orgs/$slug', getParentRoute: () => LayoutAuthenticatedRoute, } as any) +const LayoutAdminAdminTenantsRoute = LayoutAdminAdminTenantsRouteImport.update({ + id: '/tenants', + path: '/tenants', + getParentRoute: () => LayoutAdminAdminRoute, +} as any) const LayoutAdminAdminSystemRoute = LayoutAdminAdminSystemRouteImport.update({ id: '/system', path: '/system', getParentRoute: () => LayoutAdminAdminRoute, } as any) +const LayoutAdminAdminRelayerRoute = LayoutAdminAdminRelayerRouteImport.update({ + id: '/relayer', + path: '/relayer', + getParentRoute: () => LayoutAdminAdminRoute, +} as any) const LayoutPublicAppsAccountIdIndexRoute = LayoutPublicAppsAccountIdIndexRouteImport.update({ id: '/apps/$accountId/', path: '/apps/$accountId/', getParentRoute: () => LayoutPublicRoute, } as any) +const LayoutAdminAdminTenantsIndexRoute = + LayoutAdminAdminTenantsIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => LayoutAdminAdminTenantsRoute, + } as any) const LayoutPublicAppsAccountIdGatewayIdRoute = LayoutPublicAppsAccountIdGatewayIdRouteImport.update({ id: '/apps/$accountId/$gatewayId', @@ -227,6 +246,12 @@ const LayoutAuthenticatedOrgsInvitesIdRoute = path: '/orgs/invites/$id', getParentRoute: () => LayoutAuthenticatedRoute, } as any) +const LayoutAdminAdminTenantsNewRoute = + LayoutAdminAdminTenantsNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => LayoutAdminAdminTenantsRoute, + } as any) export interface FileRoutesByFullPath { '/': typeof LayoutPublicIndexRoute @@ -237,14 +262,16 @@ export interface FileRoutesByFullPath { '/$accountId': typeof LayoutPublicAccountIdRouteWithChildren '/about': typeof LayoutPublicAboutRoute '/skill': typeof LayoutPublicSkillRoute + '/stake': typeof LayoutPublicStakeRoute + '/admin/relayer': typeof LayoutAdminAdminRelayerRoute '/admin/system': typeof LayoutAdminAdminSystemRoute + '/admin/tenants': typeof LayoutAdminAdminTenantsRouteWithChildren '/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute '/orgs/new': typeof LayoutAuthenticatedOrgsNewRoute '/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute '/settings/profile': typeof LayoutAuthenticatedSettingsProfileRoute '/settings/security': typeof LayoutAuthenticatedSettingsSecurityRoute '/tenant/$tenantId': typeof LayoutAuthenticatedTenantTenantIdRoute - '/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/things/new': typeof LayoutAuthenticatedThingsNewRoute '/$accountId/apps': typeof LayoutPublicAccountIdAppsRoute '/things/$thingId': typeof LayoutPublicThingsThingIdRoute @@ -255,8 +282,10 @@ export interface FileRoutesByFullPath { '/$accountId/': typeof LayoutPublicAccountIdIndexRoute '/apps/': typeof LayoutPublicAppsIndexRoute '/things/': typeof LayoutPublicThingsIndexRoute + '/admin/tenants/new': typeof LayoutAdminAdminTenantsNewRoute '/orgs/invites/$id': typeof LayoutAuthenticatedOrgsInvitesIdRoute '/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute + '/admin/tenants/': typeof LayoutAdminAdminTenantsIndexRoute '/apps/$accountId/': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRoutesByTo { @@ -265,6 +294,8 @@ export interface FileRoutesByTo { '/dashboard': typeof LayoutAuthenticatedDashboardRoute '/about': typeof LayoutPublicAboutRoute '/skill': typeof LayoutPublicSkillRoute + '/stake': typeof LayoutPublicStakeRoute + '/admin/relayer': typeof LayoutAdminAdminRelayerRoute '/admin/system': typeof LayoutAdminAdminSystemRoute '/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute '/orgs/new': typeof LayoutAuthenticatedOrgsNewRoute @@ -272,7 +303,6 @@ export interface FileRoutesByTo { '/settings/profile': typeof LayoutAuthenticatedSettingsProfileRoute '/settings/security': typeof LayoutAuthenticatedSettingsSecurityRoute '/tenant/$tenantId': typeof LayoutAuthenticatedTenantTenantIdRoute - '/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/things/new': typeof LayoutAuthenticatedThingsNewRoute '/$accountId/apps': typeof LayoutPublicAccountIdAppsRoute '/things/$thingId': typeof LayoutPublicThingsThingIdRoute @@ -283,8 +313,10 @@ export interface FileRoutesByTo { '/$accountId': typeof LayoutPublicAccountIdIndexRoute '/apps': typeof LayoutPublicAppsIndexRoute '/things': typeof LayoutPublicThingsIndexRoute + '/admin/tenants/new': typeof LayoutAdminAdminTenantsNewRoute '/orgs/invites/$id': typeof LayoutAuthenticatedOrgsInvitesIdRoute '/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute + '/admin/tenants': typeof LayoutAdminAdminTenantsIndexRoute '/apps/$accountId': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRoutesById { @@ -301,15 +333,17 @@ export interface FileRoutesById { '/_layout/_public/$accountId': typeof LayoutPublicAccountIdRouteWithChildren '/_layout/_public/about': typeof LayoutPublicAboutRoute '/_layout/_public/skill': typeof LayoutPublicSkillRoute + '/_layout/_public/stake': typeof LayoutPublicStakeRoute '/_layout/_public/': typeof LayoutPublicIndexRoute + '/_layout/_admin/admin/relayer': typeof LayoutAdminAdminRelayerRoute '/_layout/_admin/admin/system': typeof LayoutAdminAdminSystemRoute + '/_layout/_admin/admin/tenants': typeof LayoutAdminAdminTenantsRouteWithChildren '/_layout/_authenticated/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute '/_layout/_authenticated/orgs/new': typeof LayoutAuthenticatedOrgsNewRoute '/_layout/_authenticated/settings/auth-methods': typeof LayoutAuthenticatedSettingsAuthMethodsRoute '/_layout/_authenticated/settings/profile': typeof LayoutAuthenticatedSettingsProfileRoute '/_layout/_authenticated/settings/security': typeof LayoutAuthenticatedSettingsSecurityRoute '/_layout/_authenticated/tenant/$tenantId': typeof LayoutAuthenticatedTenantTenantIdRoute - '/_layout/_authenticated/tenant/new': typeof LayoutAuthenticatedTenantNewRoute '/_layout/_authenticated/things/new': typeof LayoutAuthenticatedThingsNewRoute '/_layout/_public/$accountId/apps': typeof LayoutPublicAccountIdAppsRoute '/_layout/_public/things/$thingId': typeof LayoutPublicThingsThingIdRoute @@ -320,8 +354,10 @@ export interface FileRoutesById { '/_layout/_public/$accountId/': typeof LayoutPublicAccountIdIndexRoute '/_layout/_public/apps/': typeof LayoutPublicAppsIndexRoute '/_layout/_public/things/': typeof LayoutPublicThingsIndexRoute + '/_layout/_admin/admin/tenants/new': typeof LayoutAdminAdminTenantsNewRoute '/_layout/_authenticated/orgs/invites/$id': typeof LayoutAuthenticatedOrgsInvitesIdRoute '/_layout/_public/apps/$accountId/$gatewayId': typeof LayoutPublicAppsAccountIdGatewayIdRoute + '/_layout/_admin/admin/tenants/': typeof LayoutAdminAdminTenantsIndexRoute '/_layout/_public/apps/$accountId/': typeof LayoutPublicAppsAccountIdIndexRoute } export interface FileRouteTypes { @@ -335,14 +371,16 @@ export interface FileRouteTypes { | '/$accountId' | '/about' | '/skill' + | '/stake' + | '/admin/relayer' | '/admin/system' + | '/admin/tenants' | '/orgs/$slug' | '/orgs/new' | '/settings/auth-methods' | '/settings/profile' | '/settings/security' | '/tenant/$tenantId' - | '/tenant/new' | '/things/new' | '/$accountId/apps' | '/things/$thingId' @@ -353,8 +391,10 @@ export interface FileRouteTypes { | '/$accountId/' | '/apps/' | '/things/' + | '/admin/tenants/new' | '/orgs/invites/$id' | '/apps/$accountId/$gatewayId' + | '/admin/tenants/' | '/apps/$accountId/' fileRoutesByTo: FileRoutesByTo to: @@ -363,6 +403,8 @@ export interface FileRouteTypes { | '/dashboard' | '/about' | '/skill' + | '/stake' + | '/admin/relayer' | '/admin/system' | '/orgs/$slug' | '/orgs/new' @@ -370,7 +412,6 @@ export interface FileRouteTypes { | '/settings/profile' | '/settings/security' | '/tenant/$tenantId' - | '/tenant/new' | '/things/new' | '/$accountId/apps' | '/things/$thingId' @@ -381,8 +422,10 @@ export interface FileRouteTypes { | '/$accountId' | '/apps' | '/things' + | '/admin/tenants/new' | '/orgs/invites/$id' | '/apps/$accountId/$gatewayId' + | '/admin/tenants' | '/apps/$accountId' id: | '__root__' @@ -398,15 +441,17 @@ export interface FileRouteTypes { | '/_layout/_public/$accountId' | '/_layout/_public/about' | '/_layout/_public/skill' + | '/_layout/_public/stake' | '/_layout/_public/' + | '/_layout/_admin/admin/relayer' | '/_layout/_admin/admin/system' + | '/_layout/_admin/admin/tenants' | '/_layout/_authenticated/orgs/$slug' | '/_layout/_authenticated/orgs/new' | '/_layout/_authenticated/settings/auth-methods' | '/_layout/_authenticated/settings/profile' | '/_layout/_authenticated/settings/security' | '/_layout/_authenticated/tenant/$tenantId' - | '/_layout/_authenticated/tenant/new' | '/_layout/_authenticated/things/new' | '/_layout/_public/$accountId/apps' | '/_layout/_public/things/$thingId' @@ -417,8 +462,10 @@ export interface FileRouteTypes { | '/_layout/_public/$accountId/' | '/_layout/_public/apps/' | '/_layout/_public/things/' + | '/_layout/_admin/admin/tenants/new' | '/_layout/_authenticated/orgs/invites/$id' | '/_layout/_public/apps/$accountId/$gatewayId' + | '/_layout/_admin/admin/tenants/' | '/_layout/_public/apps/$accountId/' fileRoutesById: FileRoutesById } @@ -470,6 +517,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicIndexRouteImport parentRoute: typeof LayoutPublicRoute } + '/_layout/_public/stake': { + id: '/_layout/_public/stake' + path: '/stake' + fullPath: '/stake' + preLoaderRoute: typeof LayoutPublicStakeRouteImport + parentRoute: typeof LayoutPublicRoute + } '/_layout/_public/skill': { id: '/_layout/_public/skill' path: '/skill' @@ -589,13 +643,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedThingsNewRouteImport parentRoute: typeof LayoutAuthenticatedRoute } - '/_layout/_authenticated/tenant/new': { - id: '/_layout/_authenticated/tenant/new' - path: '/tenant/new' - fullPath: '/tenant/new' - preLoaderRoute: typeof LayoutAuthenticatedTenantNewRouteImport - parentRoute: typeof LayoutAuthenticatedRoute - } '/_layout/_authenticated/tenant/$tenantId': { id: '/_layout/_authenticated/tenant/$tenantId' path: '/tenant/$tenantId' @@ -638,6 +685,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedOrgsSlugRouteImport parentRoute: typeof LayoutAuthenticatedRoute } + '/_layout/_admin/admin/tenants': { + id: '/_layout/_admin/admin/tenants' + path: '/tenants' + fullPath: '/admin/tenants' + preLoaderRoute: typeof LayoutAdminAdminTenantsRouteImport + parentRoute: typeof LayoutAdminAdminRoute + } '/_layout/_admin/admin/system': { id: '/_layout/_admin/admin/system' path: '/system' @@ -645,6 +699,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAdminAdminSystemRouteImport parentRoute: typeof LayoutAdminAdminRoute } + '/_layout/_admin/admin/relayer': { + id: '/_layout/_admin/admin/relayer' + path: '/relayer' + fullPath: '/admin/relayer' + preLoaderRoute: typeof LayoutAdminAdminRelayerRouteImport + parentRoute: typeof LayoutAdminAdminRoute + } '/_layout/_public/apps/$accountId/': { id: '/_layout/_public/apps/$accountId/' path: '/apps/$accountId' @@ -652,6 +713,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicAppsAccountIdIndexRouteImport parentRoute: typeof LayoutPublicRoute } + '/_layout/_admin/admin/tenants/': { + id: '/_layout/_admin/admin/tenants/' + path: '/' + fullPath: '/admin/tenants/' + preLoaderRoute: typeof LayoutAdminAdminTenantsIndexRouteImport + parentRoute: typeof LayoutAdminAdminTenantsRoute + } '/_layout/_public/apps/$accountId/$gatewayId': { id: '/_layout/_public/apps/$accountId/$gatewayId' path: '/apps/$accountId/$gatewayId' @@ -666,16 +734,43 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutAuthenticatedOrgsInvitesIdRouteImport parentRoute: typeof LayoutAuthenticatedRoute } + '/_layout/_admin/admin/tenants/new': { + id: '/_layout/_admin/admin/tenants/new' + path: '/new' + fullPath: '/admin/tenants/new' + preLoaderRoute: typeof LayoutAdminAdminTenantsNewRouteImport + parentRoute: typeof LayoutAdminAdminTenantsRoute + } } } +interface LayoutAdminAdminTenantsRouteChildren { + LayoutAdminAdminTenantsNewRoute: typeof LayoutAdminAdminTenantsNewRoute + LayoutAdminAdminTenantsIndexRoute: typeof LayoutAdminAdminTenantsIndexRoute +} + +const LayoutAdminAdminTenantsRouteChildren: LayoutAdminAdminTenantsRouteChildren = + { + LayoutAdminAdminTenantsNewRoute: LayoutAdminAdminTenantsNewRoute, + LayoutAdminAdminTenantsIndexRoute: LayoutAdminAdminTenantsIndexRoute, + } + +const LayoutAdminAdminTenantsRouteWithChildren = + LayoutAdminAdminTenantsRoute._addFileChildren( + LayoutAdminAdminTenantsRouteChildren, + ) + interface LayoutAdminAdminRouteChildren { + LayoutAdminAdminRelayerRoute: typeof LayoutAdminAdminRelayerRoute LayoutAdminAdminSystemRoute: typeof LayoutAdminAdminSystemRoute + LayoutAdminAdminTenantsRoute: typeof LayoutAdminAdminTenantsRouteWithChildren LayoutAdminAdminIndexRoute: typeof LayoutAdminAdminIndexRoute } const LayoutAdminAdminRouteChildren: LayoutAdminAdminRouteChildren = { + LayoutAdminAdminRelayerRoute: LayoutAdminAdminRelayerRoute, LayoutAdminAdminSystemRoute: LayoutAdminAdminSystemRoute, + LayoutAdminAdminTenantsRoute: LayoutAdminAdminTenantsRouteWithChildren, LayoutAdminAdminIndexRoute: LayoutAdminAdminIndexRoute, } @@ -736,7 +831,6 @@ interface LayoutAuthenticatedRouteChildren { LayoutAuthenticatedOrgsSlugRoute: typeof LayoutAuthenticatedOrgsSlugRoute LayoutAuthenticatedOrgsNewRoute: typeof LayoutAuthenticatedOrgsNewRoute LayoutAuthenticatedTenantTenantIdRoute: typeof LayoutAuthenticatedTenantTenantIdRoute - LayoutAuthenticatedTenantNewRoute: typeof LayoutAuthenticatedTenantNewRoute LayoutAuthenticatedThingsNewRoute: typeof LayoutAuthenticatedThingsNewRoute LayoutAuthenticatedOrgsIndexRoute: typeof LayoutAuthenticatedOrgsIndexRoute LayoutAuthenticatedOrgsInvitesIdRoute: typeof LayoutAuthenticatedOrgsInvitesIdRoute @@ -750,7 +844,6 @@ const LayoutAuthenticatedRouteChildren: LayoutAuthenticatedRouteChildren = { LayoutAuthenticatedOrgsNewRoute: LayoutAuthenticatedOrgsNewRoute, LayoutAuthenticatedTenantTenantIdRoute: LayoutAuthenticatedTenantTenantIdRoute, - LayoutAuthenticatedTenantNewRoute: LayoutAuthenticatedTenantNewRoute, LayoutAuthenticatedThingsNewRoute: LayoutAuthenticatedThingsNewRoute, LayoutAuthenticatedOrgsIndexRoute: LayoutAuthenticatedOrgsIndexRoute, LayoutAuthenticatedOrgsInvitesIdRoute: LayoutAuthenticatedOrgsInvitesIdRoute, @@ -778,6 +871,7 @@ interface LayoutPublicRouteChildren { LayoutPublicAccountIdRoute: typeof LayoutPublicAccountIdRouteWithChildren LayoutPublicAboutRoute: typeof LayoutPublicAboutRoute LayoutPublicSkillRoute: typeof LayoutPublicSkillRoute + LayoutPublicStakeRoute: typeof LayoutPublicStakeRoute LayoutPublicIndexRoute: typeof LayoutPublicIndexRoute LayoutPublicThingsThingIdRoute: typeof LayoutPublicThingsThingIdRoute LayoutPublicThingsLiveRoute: typeof LayoutPublicThingsLiveRoute @@ -791,6 +885,7 @@ const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { LayoutPublicAccountIdRoute: LayoutPublicAccountIdRouteWithChildren, LayoutPublicAboutRoute: LayoutPublicAboutRoute, LayoutPublicSkillRoute: LayoutPublicSkillRoute, + LayoutPublicStakeRoute: LayoutPublicStakeRoute, LayoutPublicIndexRoute: LayoutPublicIndexRoute, LayoutPublicThingsThingIdRoute: LayoutPublicThingsThingIdRoute, LayoutPublicThingsLiveRoute: LayoutPublicThingsLiveRoute, diff --git a/ui/src/routes/_layout.tsx b/ui/src/routes/_layout.tsx index 3f68e524..de1371ac 100644 --- a/ui/src/routes/_layout.tsx +++ b/ui/src/routes/_layout.tsx @@ -9,9 +9,16 @@ export const Route = createFileRoute("/_layout")({ function Layout() { const isNavigating = useRouterState({ select: (s) => s.status === "pending" }); + const [showBar, setShowBar] = useState(false); - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); + useEffect(() => { + if (!isNavigating) { + setShowBar(false); + return; + } + const t = setTimeout(() => setShowBar(true), 150); + return () => clearTimeout(t); + }, [isNavigating]); const [betaBannerDismissed, setBetaBannerDismissed] = useState(() => { if (typeof window === "undefined") return false; @@ -48,7 +55,7 @@ function Layout() {
)} - {mounted && isNavigating && ( + {showBar && (
diff --git a/ui/src/routes/_layout/_admin.tsx b/ui/src/routes/_layout/_admin.tsx index 9906591e..6adcd61a 100644 --- a/ui/src/routes/_layout/_admin.tsx +++ b/ui/src/routes/_layout/_admin.tsx @@ -1,6 +1,14 @@ -import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; +import { useQueryClient } from "@tanstack/react-query"; +import { createFileRoute, Link, redirect } from "@tanstack/react-router"; +import { ChevronDown, ChevronUp, Copy, ExternalLink, Fuel, Wallet } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; import type { SessionData } from "@/app"; -import { sessionQueryOptions } from "@/app"; +import { sessionQueryOptions, useAuthClient } from "@/app"; +import { Badge, Button, Field, FieldLabel, Input } from "@/components"; +import { AuthShell } from "@/components/layout/auth-shell"; +import { formatYocto, useRelayerFund, useRelayerInfoQuery } from "@/lib/use-relayer-fund"; +import { cn } from "@/lib/utils"; interface AuthContext { isAuthenticated: boolean; @@ -12,6 +20,8 @@ interface AuthContext { isBanned: boolean; } +const FUND_PRESETS = ["1", "5", "10"] as const; + export const Route = createFileRoute("/_layout/_admin")({ beforeLoad: async ({ context, location }) => { const { queryClient, authClient } = context; @@ -58,5 +68,200 @@ export const Route = createFileRoute("/_layout/_admin")({ }); function AdminGate() { - return ; + const { runtimeConfig, session } = Route.useRouteContext(); + return ( + <> + + + + ); +} + +function RelayerPanel() { + const auth = useAuthClient(); + const queryClient = useQueryClient(); + const nearAccountId = auth.near.getAccountId(); + const infoQuery = useRelayerInfoQuery(); + const info = infoQuery.data; + const [expanded, setExpanded] = useState(false); + + const fund = useRelayerFund(info, { + onSuccess: () => { + infoQuery.refetch(); + queryClient.invalidateQueries({ queryKey: ["relay-history"] }); + }, + }); + + const handleConnect = async () => { + const connected = await auth.near.ensureConnected(); + if (connected) { + toast.success("Wallet connected"); + infoQuery.refetch(); + } else { + toast.error("Wallet connection declined"); + } + }; + + const handleCopy = async (value: string) => { + try { + await navigator.clipboard.writeText(value); + toast.success("Copied to clipboard"); + } catch { + toast.error("Copy failed"); + } + }; + + const statusLabel = !info + ? "not configured" + : info.enabled + ? "active" + : info.accountId + ? "needs funding" + : "initialising"; + + const statusVariant = + !info || info.enabled ? "default" : info.accountId ? "destructive" : "secondary"; + + const balance = formatYocto(info?.balance); + const canExpand = !!info?.accountId; + const shortAccount = info?.accountId + ? `${info.accountId.slice(0, 6)}…${info.accountId.slice(-4)}` + : null; + + return ( +
+
+
+
+ +
+ + relayer + + {statusLabel} + {info?.mode && ( + + {info.mode} + + )} +
+ {info?.accountId && ( +
+ + {shortAccount} + + +
+ )} + {balance && ( +
+ {balance} +
+ )} +
+ + {canExpand && ( + + )} +
+
+ + {expanded && canExpand && ( +
+ {!nearAccountId ? ( +
+

+ Connect a NEAR wallet to send NEAR to the relayer account. +

+ +
+ ) : ( +
+
+

+ fund relayer +

+

+ sending from{" "} + {nearAccountId} +

+
+ + amount (NEAR) + fund.setAmount(e.target.value)} + disabled={fund.sending} + className="max-w-xs" + /> + +
+
+ {FUND_PRESETS.map((preset) => ( + + ))} +
+ +
+
+ )} +
+ )} +
+
+
+ ); } diff --git a/ui/src/routes/_layout/_admin/admin.tsx b/ui/src/routes/_layout/_admin/admin.tsx index 9fee1727..65b7e6a4 100644 --- a/ui/src/routes/_layout/_admin/admin.tsx +++ b/ui/src/routes/_layout/_admin/admin.tsx @@ -1,8 +1,11 @@ -import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router"; -import { Shield } from "lucide-react"; -import { getAccount } from "@/app"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Link, Outlet, useRouterState } from "@tanstack/react-router"; +import { Building2, Fuel, LayoutDashboard, Settings, Shield } from "lucide-react"; +import { getAccount, useAuthClient } from "@/app"; +import { Badge } from "@/components"; import { EmptyState } from "@/components/empty-state"; import { PageContainer } from "@/components/layout/page-container"; +import { cn } from "@/lib/utils"; export const Route = createFileRoute("/_layout/_admin/admin")({ head: () => ({ @@ -17,9 +20,6 @@ export const Route = createFileRoute("/_layout/_admin/admin")({ } catch { tenant = null; } - if (!tenant) { - throw redirect({ to: "/" }); - } return { tenant }; }, component: AdminPage, @@ -27,15 +27,26 @@ export const Route = createFileRoute("/_layout/_admin/admin")({ function AdminPage() { const { tenant, session } = Route.useRouteContext(); + const auth = useAuthClient(); const activeOrgId = session?.session?.activeOrganizationId ?? null; const isMember = !!tenant && !!activeOrgId && activeOrgId === tenant.orgId; const isAdmin = session?.user?.role === "admin"; const authorized = isMember || isAdmin; - if (!tenant) return null; + const { data: relayerInfo } = useQuery({ + queryKey: ["relayer-info"], + queryFn: async () => { + const { data } = await auth.near.getRelayerInfo(); + return data ?? null; + }, + refetchInterval: 60_000, + }); + + const relayerNeedsFunding = + relayerInfo && relayerInfo.enabled === false && !!relayerInfo.accountId; - if (!authorized) { + if (tenant && !authorized) { return (
-
-
- - Admin -
-
-
-

- {tenant.name} -

-

- {tenant.subdomain} · {tenant.accountId} -

+ {tenant && ( +
+
+ + Admin
-
-
+
+
+

+ {tenant.name} +

+

+ {tenant.subdomain} · {tenant.accountId} +

+
+
+ + )} + + {tenant && ( +
+ + + + {tenant.subdomain} + + } + /> + +
+ )} + + {relayerNeedsFunding && ( + +
+
+ +
+

Relayer needs funding

+

+ The ephemeral relayer{" "} + {relayerInfo?.accountId} has + zero balance — gasless relay is disabled. Fund it with NEAR to enable tenant + + app meta publishes. +

+
+
+ action needed +
+ + )} -
- - - - {tenant.subdomain} - - } - /> - -
+
@@ -113,6 +153,40 @@ function AdminPage() { ); } +const NAV_ITEMS = [ + { label: "dashboard", to: "/admin", icon: LayoutDashboard }, + { label: "tenants", to: "/admin/tenants", icon: Building2 }, + { label: "relayer", to: "/admin/relayer", icon: Fuel }, + { label: "system", to: "/admin/system", icon: Settings }, +] as const; + +function AdminNav() { + const pathname = useRouterState({ select: (s) => s.location.pathname }); + const isActive = (to: string) => + to === "/admin" ? pathname === "/admin" || pathname === "/admin/" : pathname.startsWith(to); + + return ( + + ); +} + function StatCard({ label, value, diff --git a/ui/src/routes/_layout/_admin/admin/index.tsx b/ui/src/routes/_layout/_admin/admin/index.tsx index 137d4e33..c2b43a39 100644 --- a/ui/src/routes/_layout/_admin/admin/index.tsx +++ b/ui/src/routes/_layout/_admin/admin/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute, Link } from "@tanstack/react-router"; -import { Building2, Settings, Users } from "lucide-react"; +import { Building2, LayoutDashboard, Settings, Users } from "lucide-react"; import { getAccount } from "@/app"; import { Button, Card } from "@/components"; import { InfoRow } from "@/components/ui/info-row"; @@ -44,6 +44,22 @@ function AdminDashboard() {

Manage

+ +
+ +
+

Tenants

+

+ Create and manage tenant deployments for your organization. +

+ +
+
diff --git a/ui/src/routes/_layout/_admin/admin/relayer.tsx b/ui/src/routes/_layout/_admin/admin/relayer.tsx new file mode 100644 index 00000000..17923bdc --- /dev/null +++ b/ui/src/routes/_layout/_admin/admin/relayer.tsx @@ -0,0 +1,240 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { Coins, Fuel, Wallet } from "lucide-react"; +import { toast } from "sonner"; +import { useAuthClient } from "@/app"; +import { Badge, Button, Card, CardContent, Field, FieldLabel, Input } from "@/components"; +import { InfoRow } from "@/components/ui/info-row"; +import { + formatYocto, + relayerInfoQueryKey, + useRelayerFund, + useRelayerInfoQuery, +} from "@/lib/use-relayer-fund"; + +export const Route = createFileRoute("/_layout/_admin/admin/relayer")({ + head: () => ({ + meta: [{ title: "Relayer | app" }], + }), + component: AdminRelayerPage, +}); + +const FUND_PRESETS = ["1", "5", "10"] as const; + +function AdminRelayerPage() { + const auth = useAuthClient(); + const queryClient = useQueryClient(); + const nearAccountId = auth.near.getAccountId(); + + const relayerInfoQuery = useRelayerInfoQuery(); + const info = relayerInfoQuery.data; + + const fund = useRelayerFund(info, { + onSuccess: () => { + relayerInfoQuery.refetch(); + queryClient.invalidateQueries({ queryKey: ["relay-history"] }); + }, + }); + + const relayHistoryQuery = useQuery({ + queryKey: ["relay-history"], + queryFn: async () => { + const { data } = await auth.near.relayHistory(); + return data ?? null; + }, + refetchInterval: 30_000, + }); + + const refresh = () => { + queryClient.invalidateQueries({ queryKey: relayerInfoQueryKey }); + queryClient.invalidateQueries({ queryKey: ["relay-history"] }); + }; + + const handleConnect = async () => { + const connected = await auth.near.ensureConnected(); + if (connected) { + toast.success("Wallet connected"); + refresh(); + } else { + toast.error("Wallet connection declined"); + } + }; + + const history = relayHistoryQuery.data; + + const statusLabel = !info + ? "not configured" + : info.enabled + ? "active" + : info.accountId + ? "needs funding" + : "initialising"; + + const statusVariant = + !info || info.enabled ? "default" : info.accountId ? "destructive" : "secondary"; + + return ( +
+
+
+

Relayer

+

+ Gasless NEP-366 delegate action relay for tenant config + app metadata writes. +

+
+
+ +
+ +
+

Status

+
+ {statusLabel} + {info?.mode && ( + + {info.mode} + + )} +
+
+ + {relayerInfoQuery.isLoading ? ( +

Loading relayer info…

+ ) : !info ? ( +

+ No relayer configured. Update bos.config.json with{" "} + app.auth.variables.siwn.relayer and run{" "} + bos publish to enable. +

+ ) : !info.enabled ? ( +

+ {info.accountId ? ( + <> + Relayer keypair generated but the account has zero balance. Fund{" "} + {info.accountId} with NEAR to + activate gasless relay. + + ) : ( + "Restart the auth service to complete ephemeral keypair generation." + )} +

+ ) : ( +
+ + + + +
+ )} + + +
+ + +
+ +

Top up

+
+ + {!nearAccountId ? ( +
+

+ Connect a NEAR wallet to fund the relayer. +

+ +
+ ) : ( +
+

+ Sending from {nearAccountId} +

+ + amount (NEAR) + fund.setAmount(e.target.value)} + disabled={fund.sending} + /> + +
+ {FUND_PRESETS.map((preset) => ( + + ))} +
+ +
+ )} +
+
+ + + +

Recent relays

+ {relayHistoryQuery.isLoading ? ( +

Loading…

+ ) : !history?.transactions?.length ? ( +

+ No relayed transactions yet. Tenant republish + app metadata writes will appear here. +

+ ) : ( +
    + {history.transactions.slice(0, 8).map((tx) => ( +
  • + {tx.txHash.slice(0, 12)}… + {tx.senderId} + + {tx.status} + +
  • + ))} +
+ )} +
+
+
+ ); +} diff --git a/ui/src/routes/_layout/_admin/admin/tenants.tsx b/ui/src/routes/_layout/_admin/admin/tenants.tsx new file mode 100644 index 00000000..a4069aac --- /dev/null +++ b/ui/src/routes/_layout/_admin/admin/tenants.tsx @@ -0,0 +1,5 @@ +import { createFileRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createFileRoute("/_layout/_admin/admin/tenants")({ + component: () => , +}); diff --git a/ui/src/routes/_layout/_admin/admin/tenants/index.tsx b/ui/src/routes/_layout/_admin/admin/tenants/index.tsx new file mode 100644 index 00000000..68ce8b55 --- /dev/null +++ b/ui/src/routes/_layout/_admin/admin/tenants/index.tsx @@ -0,0 +1,163 @@ +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute, Link } from "@tanstack/react-router"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Building2, Plus } from "lucide-react"; +import { useMemo } from "react"; +import { getActiveRuntime, useApiClient } from "@/app"; +import { Badge, Button, Card, Skeleton } from "@/components"; +import { EmptyState } from "@/components/empty-state"; +import { DataTable } from "@/components/ui/data-table"; + +type ApiClient = ReturnType; +type Tenant = Awaited>[number]; + +export const Route = createFileRoute("/_layout/_admin/admin/tenants/")({ + head: () => ({ + meta: [{ title: "Tenants | app" }], + }), + component: AdminTenants, +}); + +const STATUS_VARIANT: Record = { + active: "default", + pending: "secondary", + suspended: "destructive", + pending_deletion: "secondary", +}; + +function AdminTenants() { + const apiClient = useApiClient(); + const gatewayId = getActiveRuntime()?.gatewayId ?? "everything.dev"; + + const { + data: tenants = [], + isLoading, + error, + refetch, + } = useQuery({ + queryKey: ["tenants"], + queryFn: async () => apiClient.listTenants(), + staleTime: 30 * 1000, + }); + + const columns = useMemo[]>( + () => [ + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => ( + + {row.original.name} + + ), + }, + { + accessorKey: "subdomain", + header: "Subdomain", + cell: ({ row }) => ( + + {row.original.subdomain}.{gatewayId} + + ), + }, + { + accessorKey: "accountId", + header: "Account", + cell: ({ row }) => ( + {row.original.accountId} + ), + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => ( + {row.original.status} + ), + }, + { + accessorKey: "createdAt", + header: "Created", + cell: ({ row }) => + row.original.createdAt ? new Date(row.original.createdAt).toLocaleDateString() : "—", + }, + { + id: "actions", + header: "", + cell: ({ row }) => ( + + ), + }, + ], + [gatewayId], + ); + + return ( +
+
+
+
+
+ + Tenants +
+

+ Tenants +

+

+ Manage tenant deployments for your organization. +

+
+ +
+
+ + {isLoading ? ( + + {[1, 2, 3].map((n) => ( + + ))} + + ) : error ? ( + refetch()}> + retry + + } + /> + ) : tenants.length === 0 ? ( + + + + create tenant + + + } + /> + ) : ( + + )} +
+ ); +} diff --git a/ui/src/routes/_layout/_authenticated/tenant/new.tsx b/ui/src/routes/_layout/_admin/admin/tenants/new.tsx similarity index 99% rename from ui/src/routes/_layout/_authenticated/tenant/new.tsx rename to ui/src/routes/_layout/_admin/admin/tenants/new.tsx index 1ec2c150..b3867da3 100644 --- a/ui/src/routes/_layout/_authenticated/tenant/new.tsx +++ b/ui/src/routes/_layout/_admin/admin/tenants/new.tsx @@ -9,7 +9,7 @@ import { Button, Card, CardContent, Field, FieldLabel, Input } from "@/component import { PageContainer } from "@/components/layout/page-container"; import { StepList, useStepper } from "@/components/ui/stepper"; -export const Route = createFileRoute("/_layout/_authenticated/tenant/new")({ +export const Route = createFileRoute("/_layout/_admin/admin/tenants/new")({ head: () => ({ title: "New Tenant | app", meta: [{ name: "description", content: "Create a new tenant." }], @@ -240,7 +240,7 @@ function NewTenantPage() { return auth.near .getNearClient() - .transaction(accountId) + .transaction(nearAccountId) .functionCall(prepared.data.contractId, prepared.data.methodName, prepared.data.args, { gas: METADATA_GAS, attachedDeposit: 0n, @@ -286,7 +286,7 @@ function NewTenantPage() { return auth.near .getNearClient() - .transaction(accountId) + .transaction(nearAccountId) .functionCall(prepared.data.contractId, prepared.data.methodName, prepared.data.args, { gas: CONFIG_GAS, attachedDeposit: 0n, diff --git a/ui/src/routes/_layout/_anon.tsx b/ui/src/routes/_layout/_anon.tsx index 2a14987a..59dc0fe4 100644 --- a/ui/src/routes/_layout/_anon.tsx +++ b/ui/src/routes/_layout/_anon.tsx @@ -1,6 +1,6 @@ -import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router"; -import { getAppName, sessionQueryOptions } from "@/app"; -import { ThemeToggle } from "@/components/theme-toggle"; +import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"; +import { sessionQueryOptions } from "@/app"; +import { SimpleHeader } from "@/components/layout/simple-header"; export const Route = createFileRoute("/_layout/_anon")({ beforeLoad: async ({ context }) => { @@ -18,33 +18,10 @@ export const Route = createFileRoute("/_layout/_anon")({ function AnonLayout() { const { runtimeConfig } = Route.useRouteContext(); - const appName = getAppName(runtimeConfig); return (
-
-
- - - {appName} - - - - -
- -
-
-
+
diff --git a/ui/src/routes/_layout/_authenticated.tsx b/ui/src/routes/_layout/_authenticated.tsx index 83bfd82d..40edbe1a 100644 --- a/ui/src/routes/_layout/_authenticated.tsx +++ b/ui/src/routes/_layout/_authenticated.tsx @@ -1,14 +1,7 @@ -import { createFileRoute, Link, Outlet, redirect, useRouterState } from "@tanstack/react-router"; -import { ClipboardList, Compass, Globe, Home, Menu, Shield } from "lucide-react"; -import { useState } from "react"; +import { createFileRoute, redirect } from "@tanstack/react-router"; import type { SessionData } from "@/app"; -import { getAccount, getActiveRuntime, getAppName, sessionQueryOptions } from "@/app"; -import { NearBranding } from "@/components/near-branding"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { UserNav } from "@/components/user-nav"; -import { cn } from "@/lib/utils"; +import { sessionQueryOptions } from "@/app"; +import { AuthShell } from "@/components/layout/auth-shell"; interface AuthContext { isAuthenticated: boolean; @@ -20,30 +13,6 @@ interface AuthContext { isBanned: boolean; } -type SidebarRole = "anon" | "member" | "admin"; - -interface SidebarItem { - icon: React.ComponentType<{ className?: string }>; - label: string; - to: string; - roleRequired: SidebarRole; -} - -function filterSidebarByRole(items: SidebarItem[], userRole: SidebarRole): SidebarItem[] { - return items.filter((item) => { - if (item.roleRequired === "anon") return true; - if (item.roleRequired === "member" && userRole !== "anon") return true; - if (item.roleRequired === "admin" && userRole === "admin") return true; - return false; - }); -} - -function getUserRole(isAuthenticated: boolean, isAdmin: boolean): SidebarRole { - if (isAdmin) return "admin"; - if (isAuthenticated) return "member"; - return "anon"; -} - export const Route = createFileRoute("/_layout/_authenticated")({ beforeLoad: async ({ context, location }) => { const { queryClient, authClient } = context; @@ -86,218 +55,7 @@ export const Route = createFileRoute("/_layout/_authenticated")({ }); function AuthenticatedLayout() { - const pathname = useRouterState({ select: (s) => s.location.pathname }); const { runtimeConfig, session } = Route.useRouteContext(); - const appName = getAppName(runtimeConfig); - const runtime = getActiveRuntime(runtimeConfig); - const account = getAccount(runtimeConfig); const isAdmin = session?.user?.role === "admin"; - - const sidebarItems: SidebarItem[] = [ - { icon: Home, label: "dashboard", to: "/dashboard", roleRequired: "anon" }, - { icon: Shield, label: "admin", to: "/admin", roleRequired: "admin" }, - ]; - const visibleItems = filterSidebarByRole(sidebarItems, getUserRole(true, isAdmin)); - - const isActive = (item: SidebarItem) => { - return pathname === item.to || (item.to !== "/" && pathname.startsWith(`${item.to}/`)); - }; - - return ( -
- - -
-
-
-
- - - {appName} - - - - -
- {runtime?.accountId ?? account} - / - - {pathname === "/" ? "home" : pathname.slice(1).split("/").join(" / ")} - -
-
- -
- -
-
-
- -
-
- -
-
-
- - -
- ); -} - -function MobileTabBar({ - visibleItems, - isActive, -}: { - visibleItems: SidebarItem[]; - isActive: (item: SidebarItem) => boolean; -}) { - const [drawerOpen, setDrawerOpen] = useState(false); - const pathname = useRouterState({ select: (s) => s.location.pathname }); - const tabActive = (to: string) => (to === "/" ? pathname === "/" : pathname.startsWith(to)); - - return ( - - ); -} - -function TabItem({ - to, - icon: Icon, - label, - active, -}: { - to: string; - icon: React.ComponentType<{ className?: string }>; - label: string; - active: boolean; -}) { - return ( - - - {label} - - ); + return ; } diff --git a/ui/src/routes/_layout/_authenticated/orgs/$slug.tsx b/ui/src/routes/_layout/_authenticated/orgs/$slug.tsx index c26288a3..16591f33 100644 --- a/ui/src/routes/_layout/_authenticated/orgs/$slug.tsx +++ b/ui/src/routes/_layout/_authenticated/orgs/$slug.tsx @@ -86,14 +86,7 @@ function OrganizationDetail() { const { slug: orgSlug } = Route.useParams(); const auth = useAuthClient(); - const { data: session } = useQuery({ - queryKey: ["session"], - queryFn: async () => { - const { data } = await auth.getSession(); - return data ?? null; - }, - staleTime: 60 * 1000, - }); + const { data: session } = useQuery(sessionQueryOptions(auth)); const { data: organizations = [], isLoading: isLoadingOrgs } = useQuery({ queryKey: ["organizations"], diff --git a/ui/src/routes/_layout/_authenticated/orgs/index.tsx b/ui/src/routes/_layout/_authenticated/orgs/index.tsx index ae09675a..a759f5ce 100644 --- a/ui/src/routes/_layout/_authenticated/orgs/index.tsx +++ b/ui/src/routes/_layout/_authenticated/orgs/index.tsx @@ -38,9 +38,13 @@ export const Route = createFileRoute("/_layout/_authenticated/orgs/")({ await context.queryClient.ensureQueryData({ queryKey: ["user-invitations"], queryFn: async (): Promise => { - const { data, error } = await context.authClient.organization.listUserInvitations(); - if (error) throw new Error(error.message); - return (data ?? []) as UserInvitationItem[]; + try { + const { data, error } = await context.authClient.organization.listUserInvitations(); + if (error) throw new Error(error.message); + return (data ?? []) as UserInvitationItem[]; + } catch { + return []; + } }, staleTime: 30 * 1000, }); @@ -53,14 +57,7 @@ function OrganizationsList() { const apiClient = useApiClient(); const router = useRouter(); const queryClient = useQueryClient(); - const { data: session } = useQuery({ - queryKey: ["session"], - queryFn: async () => { - const { data } = await auth.getSession(); - return data ?? null; - }, - staleTime: 60 * 1000, - }); + const { data: session } = useQuery(sessionQueryOptions(auth)); const { data: organizations, isLoading } = useQuery({ queryKey: ["organizations"], queryFn: async () => { @@ -73,9 +70,13 @@ function OrganizationsList() { const { data: userInvitations = [] } = useQuery({ queryKey: ["user-invitations"], queryFn: async (): Promise => { - const { data, error } = await auth.organization.listUserInvitations(); - if (error) throw new Error(error.message); - return (data ?? []) as UserInvitationItem[]; + try { + const { data, error } = await auth.organization.listUserInvitations(); + if (error) throw new Error(error.message); + return (data ?? []) as UserInvitationItem[]; + } catch { + return []; + } }, staleTime: 30 * 1000, }); diff --git a/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx b/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx index e9d6e78b..996b5699 100644 --- a/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx +++ b/ui/src/routes/_layout/_authenticated/tenant/$tenantId.tsx @@ -37,18 +37,42 @@ async function publishTenantConfig( config: tenantConfig, }); - const signed = await auth.near.buildSignedDelegateAction( - prepared.data.contractId, - (builder: TransactionBuilder) => - builder.functionCall(prepared.data.contractId, prepared.data.methodName, prepared.data.args, { - gas: CONFIG_GAS, - attachedDeposit: 0n, - }), - ); + const relayerInfo = await auth.near.getRelayerInfo(); + const hasRelayer = relayerInfo.data?.enabled === true; + + if (hasRelayer) { + const signed = await auth.near.buildSignedDelegateAction( + prepared.data.contractId, + (builder: TransactionBuilder) => + builder.functionCall( + prepared.data.contractId, + prepared.data.methodName, + prepared.data.args, + { + gas: CONFIG_GAS, + attachedDeposit: 0n, + }, + ), + ); + + const relayed = await auth.near.relayTransaction({ payload: signed }); + if (relayed.error) throw new Error(relayed.error.message); + return relayed; + } + + const signerAccountId = auth.near.getAccountId(); + if (!signerAccountId) { + throw new Error("Connect a NEAR wallet first"); + } - const relayed = await auth.near.relayTransaction({ payload: signed }); - if (relayed.error) throw new Error(relayed.error.message); - return relayed; + return auth.near + .getNearClient() + .transaction(signerAccountId) + .functionCall(prepared.data.contractId, prepared.data.methodName, prepared.data.args, { + gas: CONFIG_GAS, + attachedDeposit: 0n, + }) + .send({ waitUntil: "EXECUTED" }); } export const Route = createFileRoute("/_layout/_authenticated/tenant/$tenantId")({ diff --git a/ui/src/routes/_layout/_public.tsx b/ui/src/routes/_layout/_public.tsx index 4001e2dd..bd004997 100644 --- a/ui/src/routes/_layout/_public.tsx +++ b/ui/src/routes/_layout/_public.tsx @@ -1,7 +1,6 @@ -import { createFileRoute, Link, Outlet } from "@tanstack/react-router"; -import { getAppName } from "@/app"; +import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { SimpleHeader } from "@/components/layout/simple-header"; import { NearBranding } from "@/components/near-branding"; -import { ThemeToggle } from "@/components/theme-toggle"; import { UserNav } from "@/components/user-nav"; export const Route = createFileRoute("/_layout/_public")({ @@ -10,34 +9,10 @@ export const Route = createFileRoute("/_layout/_public")({ function PublicLayout() { const { runtimeConfig } = Route.useRouteContext(); - const appName = getAppName(runtimeConfig); return (
-
-
- - - {appName} - - - - -
- - -
-
-
+ } />
diff --git a/ui/src/routes/_layout/_public/index.tsx b/ui/src/routes/_layout/_public/index.tsx index 8cbb4f06..3fed3c94 100644 --- a/ui/src/routes/_layout/_public/index.tsx +++ b/ui/src/routes/_layout/_public/index.tsx @@ -1,7 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; import { createFileRoute, Link } from "@tanstack/react-router"; -import { ArrowRight, Building2, FileCode2, Lock, Sparkles } from "lucide-react"; -import { getAccount, getActiveRuntime, getAppName, getRepository } from "@/app"; -import { Button, Card, PageContainer } from "@/components"; +import { ArrowRight, Landmark, Server, Sparkles } from "lucide-react"; +import { getAccount, getActiveRuntime, getAppName, useApiClient } from "@/app"; +import { Button, Card } from "@/components"; +import { PageContainer } from "@/components/layout/page-container"; export const Route = createFileRoute("/_layout/_public/")({ loader: async ({ context }) => ({ @@ -9,10 +11,10 @@ export const Route = createFileRoute("/_layout/_public/")({ }), head: () => ({ meta: [ - { title: "Welcome | app" }, + { title: "City Nodes | app" }, { name: "description", - content: "A modern starter app built with TanStack Router and Better Auth on NEAR.", + content: "Stake NEAR to city validator pools and help run decentralized city nodes.", }, ], }), @@ -21,13 +23,19 @@ export const Route = createFileRoute("/_layout/_public/")({ function LandingPage() { const { runtimeConfig } = Route.useLoaderData(); + const apiClient = useApiClient(); const appName = getAppName(runtimeConfig); const account = getAccount(runtimeConfig); const runtime = getActiveRuntime(runtimeConfig); - const repository = getRepository(runtimeConfig); const accountId = runtime?.accountId ?? account; + const { data: cityNodes = [] } = useQuery({ + queryKey: ["citynodes"], + queryFn: () => apiClient.listCityNodes(), + staleTime: 30 * 1000, + }); + return (
@@ -42,8 +50,8 @@ function LandingPage() { {appName}

- A production-ready starter built on TanStack Router, Better Auth, and Effect — with - organization management, a guarded dashboard, and a fully typed API. + Every city runs its own validator pool on NEAR. Pick a city, sign in with your wallet, + and stake NEAR to help keep it online.

@@ -54,57 +62,59 @@ function LandingPage() { -
-
- -
- -
-

Secure authentication

-

- NEAR wallet sign-in, email, and passkeys via Better Auth — with an authenticated - layout guard and session-aware routing. -

-
- - -
- +
+
+
+

Live cities

+

+ Stake to a city's validator pool from its subdomain. +

-

Organizations

-

- Create and manage organizations with members, roles, invitations, and API keys — all - backed by typed oRPC endpoints. -

- + + {cityNodes.length} {cityNodes.length === 1 ? "city" : "cities"} + +
- -
- + {cityNodes.length === 0 ? ( + +

+ No city nodes yet. Create a tenant and publish the first one. +

+
+ ) : ( +
+ {cityNodes.map((cityNode) => ( + +
+
+ +
+
+

+ {cityNode.name} +

+

+ {cityNode.hostname}.{runtime?.gatewayId ?? "citynode.app"} +

+
+
+
+ + {cityNode.validatorPool} +
+ +
+ ))}
-

Typed end to end

-

- One contract drives the API and the client — schemas, validation, and types stay in - sync across the whole stack. -

- + )}
- - {repository && ( -
-

Fork the template and make it yours.

- -
- )}
); diff --git a/ui/src/routes/_layout/_public/stake.tsx b/ui/src/routes/_layout/_public/stake.tsx new file mode 100644 index 00000000..d3e9b819 --- /dev/null +++ b/ui/src/routes/_layout/_public/stake.tsx @@ -0,0 +1,687 @@ +import { PingpayOnramp, PingpayOnrampError } from "@pingpay/onramp-sdk"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { Landmark, Pencil, Plus, Server, Trash2, Wallet } from "lucide-react"; +import { formatAmount } from "near-kit"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; +import { + getAccount, + getActiveRuntime, + sessionQueryOptions, + useApiClient, + useAuthClient, +} from "@/app"; +import pingpayLogoDark from "@/assets/brands/pingpay/pingpay-logo-dark.png"; +import pingpayLogoLight from "@/assets/brands/pingpay/pingpay-logo-light.png"; +import { Badge, Button, Card, Field, FieldLabel, Input } from "@/components"; +import { PageContainer } from "@/components/layout/page-container"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; + +const STAKE_GAS = "300000000000000"; + +type StakeSearch = { city?: string }; + +export const Route = createFileRoute("/_layout/_public/stake")({ + validateSearch: (search: Record): StakeSearch => ({ + city: typeof search.city === "string" ? search.city : undefined, + }), + head: () => ({ + meta: [ + { title: "Stake | app" }, + { name: "description", content: "Stake NEAR to a city validator pool." }, + ], + }), + component: StakePage, +}); + +function StakePage() { + const apiClient = useApiClient(); + const auth = useAuthClient(); + const queryClient = useQueryClient(); + const { runtimeConfig } = Route.useRouteContext(); + const { city: cityFromSearch } = Route.useSearch(); + const account = getAccount(runtimeConfig); + const gatewayId = getActiveRuntime()?.gatewayId ?? "citynode.app"; + + const { data: session } = useQuery(sessionQueryOptions(auth, undefined)); + + const [nearAccountId, setNearAccountId] = useState(() => auth.near.getAccountId()); + const [connectingWallet, setConnectingWallet] = useState(false); + + const handleConnectWallet = async () => { + setConnectingWallet(true); + try { + const connected = await auth.near.ensureConnected(); + if (connected) { + setNearAccountId(auth.near.getAccountId()); + } else { + toast.error("Failed to connect wallet"); + } + } catch { + toast.error("Failed to connect wallet"); + } finally { + setConnectingWallet(false); + } + }; + + const { data: activeMember } = useQuery({ + queryKey: ["active-member"], + queryFn: async () => { + const { data } = await auth.organization.getActiveMember(); + return data ?? null; + }, + enabled: !!session?.session?.activeOrganizationId, + staleTime: 30 * 1000, + }); + + const [amount, setAmount] = useState("1"); + const [selectedCity, setSelectedCity] = useState(() => cityFromSearch ?? ""); + const [editingPool, setEditingPool] = useState(false); + const [poolInput, setPoolInput] = useState(""); + const [deleteOpen, setDeleteOpen] = useState(false); + const [creating, setCreating] = useState(false); + const [newTenantId, setNewTenantId] = useState(""); + const [newPool, setNewPool] = useState(""); + + const { data: resolvedCityNode } = useQuery({ + queryKey: ["citynode", "resolve", account], + queryFn: () => apiClient.resolveCityNode({ accountId: account }), + staleTime: 30 * 1000, + }); + + const { data: allCityNodes = [] } = useQuery({ + queryKey: ["citynodes"], + queryFn: () => apiClient.listCityNodes(), + staleTime: 30 * 1000, + }); + + const isTenantSubdomain = !!resolvedCityNode; + const selectedCityNode = + resolvedCityNode ?? allCityNodes.find((c) => c.hostname === selectedCity) ?? null; + + const { data: members = [] } = useQuery({ + queryKey: ["org-members", selectedCityNode?.orgId], + queryFn: async () => { + if (!selectedCityNode?.orgId) return []; + const { data, error } = await auth.organization.listMembers({ + query: { organizationId: selectedCityNode.orgId }, + }); + if (error) throw new Error(error.message); + return (data?.members ?? []) as Array<{ userId: string; role: string }>; + }, + enabled: !!selectedCityNode?.orgId, + }); + + const { data: totalStaked, isLoading: totalStakedLoading } = useQuery({ + queryKey: ["pool-total-staked", selectedCityNode?.validatorPool], + queryFn: () => + auth.near + .getNearClient() + .view(selectedCityNode?.validatorPool as string, "get_total_staked_balance"), + enabled: !!selectedCityNode?.validatorPool, + staleTime: 30 * 1000, + retry: 1, + }); + + const { data: numDelegators, isLoading: numDelegatorsLoading } = useQuery({ + queryKey: ["pool-num-accounts", selectedCityNode?.validatorPool], + queryFn: () => + auth.near + .getNearClient() + .view(selectedCityNode?.validatorPool as string, "get_number_of_accounts"), + enabled: !!selectedCityNode?.validatorPool, + staleTime: 30 * 1000, + retry: 1, + }); + + const myRole = members.find((m) => m.userId === session?.user?.id)?.role; + const activeOrgId = session?.session?.activeOrganizationId; + const activeOrgRole = activeMember?.role; + const isActiveOrgAdmin = activeOrgRole === "admin" || activeOrgRole === "owner"; + const isOrgAdmin = + !!selectedCityNode && + selectedCityNode.orgId === activeOrgId && + (myRole === "admin" || myRole === "owner"); + + const { data: orgTenants = [] } = useQuery({ + queryKey: ["org-tenants", activeOrgId], + queryFn: () => apiClient.listTenants(), + enabled: !!activeOrgId && !isTenantSubdomain, + staleTime: 30 * 1000, + }); + + const tenantAlreadyLinked = new Set(allCityNodes.map((c) => c.tenantId)); + const availableTenants = orgTenants.filter((t) => !tenantAlreadyLinked.has(t.id)); + + const createMutation = useMutation({ + mutationFn: async () => { + if (!newTenantId || !newPool.trim()) + throw new Error("Select a tenant and enter a validator pool."); + return apiClient.createCityNode({ + tenantId: newTenantId, + validatorPool: newPool.trim(), + }); + }, + onSuccess: async () => { + toast.success("City node created"); + setCreating(false); + setNewTenantId(""); + setNewPool(""); + await queryClient.invalidateQueries({ queryKey: ["citynodes"] }); + }, + onError: (err: Error) => toast.error(err.message || "Failed to create city node"), + }); + + const parsedYocto = useMemo(() => { + const value = Number(amount); + if (!amount || Number.isNaN(value) || value <= 0) return null; + return BigInt(parseFloat(amount) * 1e24); + }, [amount]); + + const stakeMutation = useMutation({ + mutationFn: async () => { + if (!nearAccountId) throw new Error("Connect a NEAR wallet to stake."); + if (!selectedCityNode) throw new Error("Select a city to stake to."); + if (!parsedYocto) throw new Error("Enter a valid amount to stake."); + const near = auth.near.getNearClient(); + const result = await near + .transaction(nearAccountId) + .functionCall( + selectedCityNode.validatorPool, + "deposit_and_stake", + {}, + { gas: STAKE_GAS, attachedDeposit: parsedYocto }, + ) + .send({ waitUntil: "FINAL" }); + return result; + }, + onSuccess: (result) => { + toast.success("Staked", { + description: result.transaction?.hash ? `tx: ${result.transaction.hash}` : undefined, + }); + }, + onError: (err: Error) => toast.error(err.message || "Failed to stake"), + }); + + const onrampRef = useRef(null); + + const onrampMutation = useMutation({ + mutationFn: async () => { + const onramp = new PingpayOnramp({ + destinationAddress: nearAccountId ?? undefined, + onPopupClose: () => onrampMutation.reset(), + }); + onrampRef.current = onramp; + return onramp.initiateOnramp({ chain: "NEAR", asset: "NEAR" }); + }, + onSuccess: (result) => { + toast.success("Purchase complete", { + description: `Deposited to ${result.depositAddress}`, + }); + }, + onError: (err: Error) => { + if (err instanceof PingpayOnrampError) { + toast.error(err.message || "Onramp failed"); + } else { + toast.error("Unexpected error during purchase"); + } + }, + }); + + useEffect(() => { + return () => onrampRef.current?.close(); + }, []); + + const updatePoolMutation = useMutation({ + mutationFn: async () => { + if (!selectedCityNode) throw new Error("No city node selected"); + return apiClient.updateCityNode({ + cityNodeId: selectedCityNode.id, + validatorPool: poolInput.trim(), + }); + }, + onSuccess: async () => { + toast.success("Validator pool updated"); + setEditingPool(false); + await queryClient.invalidateQueries({ queryKey: ["citynode", "resolve", account] }); + await queryClient.invalidateQueries({ queryKey: ["citynodes"] }); + }, + onError: (err: Error) => toast.error(err.message || "Failed to update validator pool"), + }); + + const deleteMutation = useMutation({ + mutationFn: async () => { + if (!selectedCityNode) throw new Error("No city node selected"); + return apiClient.deleteCityNode({ cityNodeId: selectedCityNode.id }); + }, + onSuccess: async () => { + toast.success("City node deleted"); + setDeleteOpen(false); + await queryClient.invalidateQueries({ queryKey: ["citynode", "resolve", account] }); + await queryClient.invalidateQueries({ queryKey: ["citynodes"] }); + }, + onError: (err: Error) => toast.error(err.message || "Failed to delete city node"), + }); + + return ( + +
+
+
+ + Stake +
+

+ Stake NEAR to a city +

+

+ Deposits are staked directly to the city's validator pool via{" "} + deposit_and_stake. +

+
+ + {isTenantSubdomain && resolvedCityNode ? ( +
+ { + setPoolInput(resolvedCityNode.validatorPool); + setEditingPool(true); + }} + onDelete={() => setDeleteOpen(true)} + /> + {editingPool && ( + + + Validator pool + setPoolInput(e.target.value)} + placeholder="city-node-3.pool.near" + className="h-9 text-sm font-mono" + /> + +
+ + +
+
+ )} +
+ ) : ( +
+ + City + + + {selectedCityNode && ( + { + setPoolInput(selectedCityNode.validatorPool); + setEditingPool(true); + }} + onDelete={() => setDeleteOpen(true)} + /> + )} +
+ )} + + {!isTenantSubdomain && isActiveOrgAdmin && ( +
+ + {creating && ( + +

+ Link a validator pool to one of your organization's tenants. +

+ + Tenant + + + + Validator pool + setNewPool(e.target.value)} + placeholder="city-node-3.pool.near" + className="h-9 text-sm font-mono" + /> + + + {availableTenants.length === 0 && ( +

+ No tenants available. Create a tenant first, then link it to a pool. +

+ )} +
+ )} +
+ )} + + +
+ Stake +
+ {!nearAccountId ? ( +
+

+ No NEAR wallet linked. Connect one to stake. +

+ +
+ ) : ( + <> +
+ + {nearAccountId} +
+ + Amount (NEAR) + setAmount(e.target.value)} + className="h-9 text-sm" + /> + + +

+ Staking sends NEAR to the pool contract — your stake stays under your account. +

+ + )} +
+ + onrampMutation.mutate()} + /> + + {deleteOpen && selectedCityNode && ( + +

+ Delete city node {selectedCityNode.hostname}? This + unlinks it from its validator pool. +

+
+ + +
+
+ )} +
+
+ ); +} + +const TENANT_STATUS_VARIANT: Record = { + active: "default", + pending: "secondary", + suspended: "destructive", + pending_deletion: "destructive", +}; + +function CityNodeCard({ + cityNode, + gatewayId, + isAdmin, + memberCount, + totalStaked, + totalStakedLoading, + numDelegators, + numDelegatorsLoading, + onEdit, + onDelete, +}: { + cityNode: { + id: string; + hostname: string; + name: string; + accountId: string; + validatorPool: string; + tenantStatus: string; + }; + gatewayId: string; + isAdmin: boolean; + memberCount: number; + totalStaked: string | undefined; + totalStakedLoading: boolean; + numDelegators: number | undefined; + numDelegatorsLoading: boolean; + onEdit: () => void; + onDelete: () => void; +}) { + return ( + +
+
+
+ +
+
+
+

+ {cityNode.name} +

+ + {cityNode.tenantStatus.replace(/_/g, " ")} + +
+

+ {cityNode.hostname}.{gatewayId} · {cityNode.accountId} +

+
+
+ {isAdmin && ( +
+ + +
+ )} +
+
+ + {cityNode.validatorPool} +
+
+ + + +
+
+ ); +} + +function StatBlock({ label, value }: { label: string; value: string | number }) { + return ( +
+

+ {label} +

+

{value}

+
+ ); +} + +function PingOnrampBanner({ + disabled, + pending, + onBuy, +}: { + disabled: boolean; + pending: boolean; + onBuy: () => void; +}) { + const button = ( + + ); + + return ( +
+
+
+
+ +
+
+

+ Need NEAR to stake? +

+

+ Buy instantly with card, Apple Pay, or bank transfer. +

+
+
+ {disabled ? ( + + {button} + + Connect a NEAR wallet to buy NEAR + + + ) : ( + button + )} +
+
+ ); +} From 0e0ea1b23e93b635e09e33a7c5e6e310ca8f7005 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Sat, 15 Aug 2026 21:49:18 -0500 Subject: [PATCH 14/24] relayer --- bos.config.json | 14 +- ui/src/components/index.ts | 1 + ui/src/components/layout/auth-shell.tsx | 15 +- ui/src/components/layout/simple-header.tsx | 6 +- ui/src/components/user-nav.tsx | 11 +- ui/src/routeTree.gen.ts | 43 ++-- ui/src/routes/_layout/_admin.tsx | 210 +----------------- .../{_public => _authenticated}/stake.tsx | 4 +- 8 files changed, 52 insertions(+), 252 deletions(-) rename ui/src/routes/_layout/{_public => _authenticated}/stake.tsx (99%) diff --git a/bos.config.json b/bos.config.json index ff90f8e7..07a09d83 100644 --- a/bos.config.json +++ b/bos.config.json @@ -12,7 +12,7 @@ "app": { "host": { "development": "local:host", - "production": "https://elliot-braem-7375-host-everything-dev-nearbuilder-5ef47f16d-ze.zephyrcloud.app", + "production": "https://elliot-braem-7380-host-everything-dev-nearbuilder-58d75d144-ze.zephyrcloud.app", "secrets": [ "CORS_ORIGIN", "CSP_STRICT" @@ -21,14 +21,14 @@ }, "ui": { "development": "local:ui", - "production": "https://elliot-braem-7371-ui-everything-dev-nearbuilders-0423ed3a2-ze.zephyrcloud.app", - "ssr": "https://elliot-braem-7374-ui-everything-dev-nearbuilders-6460adb8d-ze.zephyrcloud.app", - "integrity": "sha384-QdZZu1hwpHi1qImC2jVp9QmIYYKpDBZBXh1FPTDDxtegZT3yUXs4xfd+j01xQyo9", - "ssrIntegrity": "sha384-olwa4R7LwVK2cMJg2IdR7a5yDPaypwjHlVwjpxNNtCHF7y8LMv0VlrOHYCVo7kqx" + "production": "https://elliot-braem-7376-ui-everything-dev-nearbuilders-640fbb40f-ze.zephyrcloud.app", + "ssr": "https://elliot-braem-7379-ui-everything-dev-nearbuilders-9e2e0e650-ze.zephyrcloud.app", + "integrity": "sha384-UQX/yKubluHrdSEKhNtBs39QAbtXHs/iJ0aqkyPIW/ey/k26xRVvCEKsL2iK+cmb", + "ssrIntegrity": "sha384-V8ObgdJVdJ/jwU5xZbwX0R0+APdf2pli4B+ApUAfkB9vtzpxGVrFNDC3qS0FsL+i" }, "api": { "development": "local:api", - "production": "https://elliot-braem-7373-api-everything-dev-nearbuilders-3c3b45512-ze.zephyrcloud.app", + "production": "https://elliot-braem-7377-api-everything-dev-nearbuilders-d0f1039d8-ze.zephyrcloud.app", "secrets": [ "API_DATABASE_URL" ], @@ -75,7 +75,7 @@ "plugins": { "apps": { "development": "local:plugins/apps", - "production": "https://elliot-braem-7372-everything-dev-apps-plugin-ever-39050cf2b-ze.zephyrcloud.app", + "production": "https://elliot-braem-7378-everything-dev-apps-plugin-ever-bd36fe158-ze.zephyrcloud.app", "variables": { "registryNamespace": "v1.citynode.near" }, diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index f7bcfd5c..716fa632 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -9,6 +9,7 @@ export { ConfirmDialog } from "./confirm-dialog"; export { EmptyState } from "./empty-state"; export { PageContainer } from "./layout/page-container"; export { OrgSwitcher } from "./org-switcher"; +export { ThemeToggle } from "./theme-toggle"; export { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; export { Badge } from "./ui/badge"; export { Button } from "./ui/button"; diff --git a/ui/src/components/layout/auth-shell.tsx b/ui/src/components/layout/auth-shell.tsx index 96c38886..85b3b704 100644 --- a/ui/src/components/layout/auth-shell.tsx +++ b/ui/src/components/layout/auth-shell.tsx @@ -1,10 +1,9 @@ import { Link, Outlet, useRouterState } from "@tanstack/react-router"; -import { ClipboardList, Compass, Globe, Home, Landmark, Menu, Shield } from "lucide-react"; +import { Building2, Home, Landmark, Menu, Shield } from "lucide-react"; import { useState } from "react"; import type { ClientRuntimeConfig, SessionData } from "@/app"; import { getAccount, getActiveRuntime, getAppName } from "@/app"; import { NearBranding } from "@/components/near-branding"; -import { ThemeToggle } from "@/components/theme-toggle"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { UserNav } from "@/components/user-nav"; @@ -105,7 +104,7 @@ export function AuthShell({ session, runtimeConfig, isAdmin = false }: AuthShell
- +
@@ -175,9 +174,8 @@ function MobileTabBar({ >
- - - + +
-
+
-
- -
diff --git a/ui/src/components/layout/simple-header.tsx b/ui/src/components/layout/simple-header.tsx index 033ed496..a9506edc 100644 --- a/ui/src/components/layout/simple-header.tsx +++ b/ui/src/components/layout/simple-header.tsx @@ -1,7 +1,6 @@ import { Link } from "@tanstack/react-router"; import type { ClientRuntimeConfig } from "@/app"; import { getAppName } from "@/app"; -import { ThemeToggle } from "@/components/theme-toggle"; interface SimpleHeaderProps { runtimeConfig?: Partial; @@ -30,10 +29,7 @@ export function SimpleHeader({ runtimeConfig, rightSlot }: SimpleHeaderProps) { -
- - {rightSlot} -
+
{rightSlot}
); diff --git a/ui/src/components/user-nav.tsx b/ui/src/components/user-nav.tsx index 13f7ef2e..d4a61673 100644 --- a/ui/src/components/user-nav.tsx +++ b/ui/src/components/user-nav.tsx @@ -4,7 +4,7 @@ import { Building2, Home, LogOut, Settings, User } from "lucide-react"; import { useMemo } from "react"; import type { Organization } from "@/app"; import { sessionQueryOptions, useAuthClient } from "@/app"; -import { Avatar, AvatarFallback, AvatarImage, OrgSwitcher } from "@/components"; +import { Avatar, AvatarFallback, AvatarImage, OrgSwitcher, ThemeToggle } from "@/components"; import { DropdownMenu, DropdownMenuContent, @@ -165,6 +165,15 @@ export function UserNav() { + +
+ + + theme + +
+
+ { diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 337a84f4..c3b89a83 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -15,10 +15,10 @@ import { Route as LayoutAuthenticatedRouteImport } from './routes/_layout/_authe import { Route as LayoutAnonRouteImport } from './routes/_layout/_anon' import { Route as LayoutAdminRouteImport } from './routes/_layout/_admin' import { Route as LayoutPublicIndexRouteImport } from './routes/_layout/_public/index' -import { Route as LayoutPublicStakeRouteImport } from './routes/_layout/_public/stake' import { Route as LayoutPublicSkillRouteImport } from './routes/_layout/_public/skill' import { Route as LayoutPublicAboutRouteImport } from './routes/_layout/_public/about' import { Route as LayoutPublicAccountIdRouteImport } from './routes/_layout/_public/$accountId' +import { Route as LayoutAuthenticatedStakeRouteImport } from './routes/_layout/_authenticated/stake' import { Route as LayoutAuthenticatedSettingsRouteImport } from './routes/_layout/_authenticated/settings' import { Route as LayoutAuthenticatedDashboardRouteImport } from './routes/_layout/_authenticated/dashboard' import { Route as LayoutAnonLoginRouteImport } from './routes/_layout/_anon/login' @@ -73,11 +73,6 @@ const LayoutPublicIndexRoute = LayoutPublicIndexRouteImport.update({ path: '/', getParentRoute: () => LayoutPublicRoute, } as any) -const LayoutPublicStakeRoute = LayoutPublicStakeRouteImport.update({ - id: '/stake', - path: '/stake', - getParentRoute: () => LayoutPublicRoute, -} as any) const LayoutPublicSkillRoute = LayoutPublicSkillRouteImport.update({ id: '/skill', path: '/skill', @@ -93,6 +88,12 @@ const LayoutPublicAccountIdRoute = LayoutPublicAccountIdRouteImport.update({ path: '/$accountId', getParentRoute: () => LayoutPublicRoute, } as any) +const LayoutAuthenticatedStakeRoute = + LayoutAuthenticatedStakeRouteImport.update({ + id: '/stake', + path: '/stake', + getParentRoute: () => LayoutAuthenticatedRoute, + } as any) const LayoutAuthenticatedSettingsRoute = LayoutAuthenticatedSettingsRouteImport.update({ id: '/settings', @@ -259,10 +260,10 @@ export interface FileRoutesByFullPath { '/login': typeof LayoutAnonLoginRoute '/dashboard': typeof LayoutAuthenticatedDashboardRoute '/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren + '/stake': typeof LayoutAuthenticatedStakeRoute '/$accountId': typeof LayoutPublicAccountIdRouteWithChildren '/about': typeof LayoutPublicAboutRoute '/skill': typeof LayoutPublicSkillRoute - '/stake': typeof LayoutPublicStakeRoute '/admin/relayer': typeof LayoutAdminAdminRelayerRoute '/admin/system': typeof LayoutAdminAdminSystemRoute '/admin/tenants': typeof LayoutAdminAdminTenantsRouteWithChildren @@ -292,9 +293,9 @@ export interface FileRoutesByTo { '/': typeof LayoutPublicIndexRoute '/login': typeof LayoutAnonLoginRoute '/dashboard': typeof LayoutAuthenticatedDashboardRoute + '/stake': typeof LayoutAuthenticatedStakeRoute '/about': typeof LayoutPublicAboutRoute '/skill': typeof LayoutPublicSkillRoute - '/stake': typeof LayoutPublicStakeRoute '/admin/relayer': typeof LayoutAdminAdminRelayerRoute '/admin/system': typeof LayoutAdminAdminSystemRoute '/orgs/$slug': typeof LayoutAuthenticatedOrgsSlugRoute @@ -330,10 +331,10 @@ export interface FileRoutesById { '/_layout/_anon/login': typeof LayoutAnonLoginRoute '/_layout/_authenticated/dashboard': typeof LayoutAuthenticatedDashboardRoute '/_layout/_authenticated/settings': typeof LayoutAuthenticatedSettingsRouteWithChildren + '/_layout/_authenticated/stake': typeof LayoutAuthenticatedStakeRoute '/_layout/_public/$accountId': typeof LayoutPublicAccountIdRouteWithChildren '/_layout/_public/about': typeof LayoutPublicAboutRoute '/_layout/_public/skill': typeof LayoutPublicSkillRoute - '/_layout/_public/stake': typeof LayoutPublicStakeRoute '/_layout/_public/': typeof LayoutPublicIndexRoute '/_layout/_admin/admin/relayer': typeof LayoutAdminAdminRelayerRoute '/_layout/_admin/admin/system': typeof LayoutAdminAdminSystemRoute @@ -368,10 +369,10 @@ export interface FileRouteTypes { | '/login' | '/dashboard' | '/settings' + | '/stake' | '/$accountId' | '/about' | '/skill' - | '/stake' | '/admin/relayer' | '/admin/system' | '/admin/tenants' @@ -401,9 +402,9 @@ export interface FileRouteTypes { | '/' | '/login' | '/dashboard' + | '/stake' | '/about' | '/skill' - | '/stake' | '/admin/relayer' | '/admin/system' | '/orgs/$slug' @@ -438,10 +439,10 @@ export interface FileRouteTypes { | '/_layout/_anon/login' | '/_layout/_authenticated/dashboard' | '/_layout/_authenticated/settings' + | '/_layout/_authenticated/stake' | '/_layout/_public/$accountId' | '/_layout/_public/about' | '/_layout/_public/skill' - | '/_layout/_public/stake' | '/_layout/_public/' | '/_layout/_admin/admin/relayer' | '/_layout/_admin/admin/system' @@ -517,13 +518,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicIndexRouteImport parentRoute: typeof LayoutPublicRoute } - '/_layout/_public/stake': { - id: '/_layout/_public/stake' - path: '/stake' - fullPath: '/stake' - preLoaderRoute: typeof LayoutPublicStakeRouteImport - parentRoute: typeof LayoutPublicRoute - } '/_layout/_public/skill': { id: '/_layout/_public/skill' path: '/skill' @@ -545,6 +539,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LayoutPublicAccountIdRouteImport parentRoute: typeof LayoutPublicRoute } + '/_layout/_authenticated/stake': { + id: '/_layout/_authenticated/stake' + path: '/stake' + fullPath: '/stake' + preLoaderRoute: typeof LayoutAuthenticatedStakeRouteImport + parentRoute: typeof LayoutAuthenticatedRoute + } '/_layout/_authenticated/settings': { id: '/_layout/_authenticated/settings' path: '/settings' @@ -828,6 +829,7 @@ const LayoutAuthenticatedSettingsRouteWithChildren = interface LayoutAuthenticatedRouteChildren { LayoutAuthenticatedDashboardRoute: typeof LayoutAuthenticatedDashboardRoute LayoutAuthenticatedSettingsRoute: typeof LayoutAuthenticatedSettingsRouteWithChildren + LayoutAuthenticatedStakeRoute: typeof LayoutAuthenticatedStakeRoute LayoutAuthenticatedOrgsSlugRoute: typeof LayoutAuthenticatedOrgsSlugRoute LayoutAuthenticatedOrgsNewRoute: typeof LayoutAuthenticatedOrgsNewRoute LayoutAuthenticatedTenantTenantIdRoute: typeof LayoutAuthenticatedTenantTenantIdRoute @@ -840,6 +842,7 @@ const LayoutAuthenticatedRouteChildren: LayoutAuthenticatedRouteChildren = { LayoutAuthenticatedDashboardRoute: LayoutAuthenticatedDashboardRoute, LayoutAuthenticatedSettingsRoute: LayoutAuthenticatedSettingsRouteWithChildren, + LayoutAuthenticatedStakeRoute: LayoutAuthenticatedStakeRoute, LayoutAuthenticatedOrgsSlugRoute: LayoutAuthenticatedOrgsSlugRoute, LayoutAuthenticatedOrgsNewRoute: LayoutAuthenticatedOrgsNewRoute, LayoutAuthenticatedTenantTenantIdRoute: @@ -871,7 +874,6 @@ interface LayoutPublicRouteChildren { LayoutPublicAccountIdRoute: typeof LayoutPublicAccountIdRouteWithChildren LayoutPublicAboutRoute: typeof LayoutPublicAboutRoute LayoutPublicSkillRoute: typeof LayoutPublicSkillRoute - LayoutPublicStakeRoute: typeof LayoutPublicStakeRoute LayoutPublicIndexRoute: typeof LayoutPublicIndexRoute LayoutPublicThingsThingIdRoute: typeof LayoutPublicThingsThingIdRoute LayoutPublicThingsLiveRoute: typeof LayoutPublicThingsLiveRoute @@ -885,7 +887,6 @@ const LayoutPublicRouteChildren: LayoutPublicRouteChildren = { LayoutPublicAccountIdRoute: LayoutPublicAccountIdRouteWithChildren, LayoutPublicAboutRoute: LayoutPublicAboutRoute, LayoutPublicSkillRoute: LayoutPublicSkillRoute, - LayoutPublicStakeRoute: LayoutPublicStakeRoute, LayoutPublicIndexRoute: LayoutPublicIndexRoute, LayoutPublicThingsThingIdRoute: LayoutPublicThingsThingIdRoute, LayoutPublicThingsLiveRoute: LayoutPublicThingsLiveRoute, diff --git a/ui/src/routes/_layout/_admin.tsx b/ui/src/routes/_layout/_admin.tsx index 6adcd61a..cf6857f1 100644 --- a/ui/src/routes/_layout/_admin.tsx +++ b/ui/src/routes/_layout/_admin.tsx @@ -1,14 +1,7 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { createFileRoute, Link, redirect } from "@tanstack/react-router"; -import { ChevronDown, ChevronUp, Copy, ExternalLink, Fuel, Wallet } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; +import { createFileRoute, redirect } from "@tanstack/react-router"; import type { SessionData } from "@/app"; -import { sessionQueryOptions, useAuthClient } from "@/app"; -import { Badge, Button, Field, FieldLabel, Input } from "@/components"; +import { sessionQueryOptions } from "@/app"; import { AuthShell } from "@/components/layout/auth-shell"; -import { formatYocto, useRelayerFund, useRelayerInfoQuery } from "@/lib/use-relayer-fund"; -import { cn } from "@/lib/utils"; interface AuthContext { isAuthenticated: boolean; @@ -20,8 +13,6 @@ interface AuthContext { isBanned: boolean; } -const FUND_PRESETS = ["1", "5", "10"] as const; - export const Route = createFileRoute("/_layout/_admin")({ beforeLoad: async ({ context, location }) => { const { queryClient, authClient } = context; @@ -69,199 +60,6 @@ export const Route = createFileRoute("/_layout/_admin")({ function AdminGate() { const { runtimeConfig, session } = Route.useRouteContext(); - return ( - <> - - - - ); -} - -function RelayerPanel() { - const auth = useAuthClient(); - const queryClient = useQueryClient(); - const nearAccountId = auth.near.getAccountId(); - const infoQuery = useRelayerInfoQuery(); - const info = infoQuery.data; - const [expanded, setExpanded] = useState(false); - - const fund = useRelayerFund(info, { - onSuccess: () => { - infoQuery.refetch(); - queryClient.invalidateQueries({ queryKey: ["relay-history"] }); - }, - }); - - const handleConnect = async () => { - const connected = await auth.near.ensureConnected(); - if (connected) { - toast.success("Wallet connected"); - infoQuery.refetch(); - } else { - toast.error("Wallet connection declined"); - } - }; - - const handleCopy = async (value: string) => { - try { - await navigator.clipboard.writeText(value); - toast.success("Copied to clipboard"); - } catch { - toast.error("Copy failed"); - } - }; - - const statusLabel = !info - ? "not configured" - : info.enabled - ? "active" - : info.accountId - ? "needs funding" - : "initialising"; - - const statusVariant = - !info || info.enabled ? "default" : info.accountId ? "destructive" : "secondary"; - - const balance = formatYocto(info?.balance); - const canExpand = !!info?.accountId; - const shortAccount = info?.accountId - ? `${info.accountId.slice(0, 6)}…${info.accountId.slice(-4)}` - : null; - - return ( -
-
-
-
- -
- - relayer - - {statusLabel} - {info?.mode && ( - - {info.mode} - - )} -
- {info?.accountId && ( -
- - {shortAccount} - - -
- )} - {balance && ( -
- {balance} -
- )} -
- - {canExpand && ( - - )} -
-
- - {expanded && canExpand && ( -
- {!nearAccountId ? ( -
-

- Connect a NEAR wallet to send NEAR to the relayer account. -

- -
- ) : ( -
-
-

- fund relayer -

-

- sending from{" "} - {nearAccountId} -

-
- - amount (NEAR) - fund.setAmount(e.target.value)} - disabled={fund.sending} - className="max-w-xs" - /> - -
-
- {FUND_PRESETS.map((preset) => ( - - ))} -
- -
-
- )} -
- )} -
-
-
- ); + const isAdmin = session?.user?.role === "admin"; + return ; } diff --git a/ui/src/routes/_layout/_public/stake.tsx b/ui/src/routes/_layout/_authenticated/stake.tsx similarity index 99% rename from ui/src/routes/_layout/_public/stake.tsx rename to ui/src/routes/_layout/_authenticated/stake.tsx index d3e9b819..a48625c1 100644 --- a/ui/src/routes/_layout/_public/stake.tsx +++ b/ui/src/routes/_layout/_authenticated/stake.tsx @@ -23,7 +23,7 @@ const STAKE_GAS = "300000000000000"; type StakeSearch = { city?: string }; -export const Route = createFileRoute("/_layout/_public/stake")({ +export const Route = createFileRoute("/_layout/_authenticated/stake")({ validateSearch: (search: Record): StakeSearch => ({ city: typeof search.city === "string" ? search.city : undefined, }), @@ -593,7 +593,7 @@ function CityNodeCard({ - ); + ) } function StatBlock({ label, value }: { label: string; value: string | number }) { From d09237013c9f3bd92f1ee308a788554d7b596cd7 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Sat, 15 Aug 2026 22:13:48 -0500 Subject: [PATCH 15/24] feat(cli): lock deploys in FastKV, expose infra export, resolve plugin origin - Add per-account/gateway FastKV deploy lock (apps///lock/deploy.json) acquired before publish and released in a finally block. Stale or concurrent dispatches fail fast with status "locked" and a conflict payload listing owner, nonce, expires, and txHash. opt out with --no-deploy-lock. - bos publish holds the lock for 10 minutes by default; bos deploy holds it for 25 minutes to cover publish + Railway redeploy. Override with BOS_DEPLOY_LOCK_TTL_MS. - Add bos deploy lock inspect|release for ops visibility and stuck-lock recovery. - Add bos infra export that emits the resolved CI infra plan (env + services + account/gateway/project/generatedAt) so CI consumers stop duplicating port and DATABASE_URL knowledge from cli/infra.ts. - Update .github/workflows/deploy.yml to consume the export and drop the hardcoded API_DATABASE_URL / AUTH_DATABASE_URL / CORS_ORIGIN env block. - buildOriginMap now reads runtimeConfig.plugins[id].extendsRef and runtimeConfig.auth?.extendsRef instead of re-parsing raw bos.config.json. - Tests for the lock helpers, the CI plan builder, the resolved-config origin lookup, and the per-command TTL resolution. Co-authored-by: opencode --- .changeset/deploy-lock-and-infra-export.md | 12 + .github/workflows/deploy.yml | 12 +- packages/everything-dev/src/cli.ts | 80 ++++- .../everything-dev/src/cli/deploy-lock.ts | 229 ++++++++++++ packages/everything-dev/src/cli/infra.ts | 180 ++++++++-- packages/everything-dev/src/contract.meta.ts | 20 ++ packages/everything-dev/src/contract.ts | 90 ++++- packages/everything-dev/src/plugin.ts | 88 +++++ packages/everything-dev/src/publish.ts | 325 +++++++++++------- .../tests/unit/deploy-lock.test.ts | 115 +++++++ .../tests/unit/infra-export.test.ts | 199 +++++++++++ .../routes/_layout/_authenticated/stake.tsx | 2 +- 12 files changed, 1188 insertions(+), 164 deletions(-) create mode 100644 .changeset/deploy-lock-and-infra-export.md create mode 100644 packages/everything-dev/src/cli/deploy-lock.ts create mode 100644 packages/everything-dev/tests/unit/deploy-lock.test.ts create mode 100644 packages/everything-dev/tests/unit/infra-export.test.ts diff --git a/.changeset/deploy-lock-and-infra-export.md b/.changeset/deploy-lock-and-infra-export.md new file mode 100644 index 00000000..c7c363da --- /dev/null +++ b/.changeset/deploy-lock-and-infra-export.md @@ -0,0 +1,12 @@ +--- +"everything-dev": minor +--- + +Add a per-account/gateway FastKV-backed deploy lock, a `bos infra export` command that emits the resolved CI infra plan, and read plugin `extendsRef` from the resolved runtime config (instead of re-parsing raw `bos.config.json`). + +- `bos publish --deploy` and `bos deploy` now acquire `apps///lock/deploy.json` before publishing. Stale or concurrent dispatches fail fast with `status: "locked"` and a conflict payload listing the owner, nonce, and txHash. Use `--no-deploy-lock` to opt out (deploy lock is on by default). The lock is released in a `finally` block after publish confirmation. + - `bos publish` holds the lock for 10 minutes by default (the FastKV write window). `bos deploy` holds it for 25 minutes by default to cover publish + Railway redeploy. Override either with `BOS_DEPLOY_LOCK_TTL_MS` (positive integer, milliseconds). +- `bos deploy lock inspect` reports the active lock owner/nonce/expires/txHash; `bos deploy lock release` force-clears a stuck lock. +- `bos infra export [--target ci|local] [--network mainnet|testnet]` emits `{env, services, account, gateway, project, generatedAt}` JSON. The deploy workflow now consumes this to populate `$GITHUB_ENV` instead of repeating `API_DATABASE_URL`, `AUTH_DATABASE_URL`, and `CORS_ORIGIN` literals. Host port comes from `BOS_CI_HOST_PORT` env or `runtimeConfig.host.port`. +- `buildOriginMap` now derives plugin origins from `runtimeConfig.plugins[id].extendsRef` / `runtimeConfig.auth?.extendsRef` (already populated by `loadResolvedConfig`), removing the duplicate raw-JSON read and the parent-runtime fallback logic. +- Three new routes on the `bos` contract: `infraExport`, `deployLockInspect`, `deployLockRelease`. New tests cover the lock helpers, the CI plan builder, and the resolved-config origin lookup. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 300ff4e6..d5d82faf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -32,9 +32,7 @@ jobs: ZE_USER_EMAIL: ${{ secrets.ZEPHYR_USER_EMAIL }} RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} TARGET_BRANCH: ${{ github.ref_name }} - API_DATABASE_URL: postgres://everythingdev:everythingdev@127.0.0.1:5432/api_db - AUTH_DATABASE_URL: postgres://everythingdev:everythingdev@127.0.0.1:5433/auth_db - CORS_ORIGIN: http://127.0.0.1:4100 + BOS_CI_HOST_PORT: "4100" services: postgres-api: image: postgres:17-alpine @@ -95,6 +93,14 @@ jobs: run: | echo "ZE_CI_TOKEN=$ZEPHYR_CI_TOKEN" >> "$GITHUB_ENV" + - name: Emit CI infra plan + id: infra + run: | + bun run bos infra export --target ci --network mainnet > .ci-infra.json + jq -r '.env | to_entries[] | "\(.key)=\(.value)"' .ci-infra.json >> "$GITHUB_ENV" + echo "## CI infra plan" >> "$GITHUB_STEP_SUMMARY" + jq -r '"Services: " + (.services | length | tostring) + "\nEnv vars: " + (.env | keys | length | tostring)' .ci-infra.json >> "$GITHUB_STEP_SUMMARY" + - name: Publish with deploy timeout-minutes: 20 run: bun run bos publish --deploy${{ inputs.verbose == true && ' --verbose' || '' }} diff --git a/packages/everything-dev/src/cli.ts b/packages/everything-dev/src/cli.ts index b1bbd372..ed9de076 100644 --- a/packages/everything-dev/src/cli.ts +++ b/packages/everything-dev/src/cli.ts @@ -1019,12 +1019,24 @@ async function main() { return; } - if (result.status === "error") { + if (result.status === "error" || result.status === "locked") { console.log(); - console.log(colors.error(`${icons.err} Publish failed`)); + console.log( + colors.error( + `${icons.err} ${result.status === "locked" ? "Deploy lock blocks publish" : "Publish failed"}`, + ), + ); if (result.error) { console.log(` ${colors.dim("Error:")} ${result.error}`); } + if (result.lockConflict?.value) { + const owner = result.lockConflict.value.owner; + const nonce = result.lockConflict.value.nonce; + const expiresAt = result.lockConflict.value.expiresAt; + console.log(` ${colors.dim("Lock owner:")} ${owner}`); + console.log(` ${colors.dim("Lock nonce:")} ${nonce}`); + console.log(` ${colors.dim("Lock expires:")} ${new Date(expiresAt).toISOString()}`); + } if (result.deployResults && result.deployResults.length > 0) { const failures = result.deployResults.filter((r: any) => !r.success); if (failures.length > 0) { @@ -1077,12 +1089,24 @@ async function main() { return; } - if (deployResult.status === "error") { + if (deployResult.status === "error" || deployResult.status === "locked") { console.log(); - console.log(colors.error(`${icons.err} Deploy failed`)); + console.log( + colors.error( + `${icons.err} ${deployResult.status === "locked" ? "Deploy lock blocks publish" : "Deploy failed"}`, + ), + ); if (deployResult.error) { console.log(` ${colors.dim("Error:")} ${deployResult.error}`); } + if (deployResult.lockConflict?.value) { + const owner = deployResult.lockConflict.value.owner; + const nonce = deployResult.lockConflict.value.nonce; + const expiresAt = deployResult.lockConflict.value.expiresAt; + console.log(` ${colors.dim("Lock owner:")} ${owner}`); + console.log(` ${colors.dim("Lock nonce:")} ${nonce}`); + console.log(` ${colors.dim("Lock expires:")} ${new Date(expiresAt).toISOString()}`); + } if (deployResult.deployResults && deployResult.deployResults.length > 0) { const failures = deployResult.deployResults.filter((r: any) => !r.success); if (failures.length > 0) { @@ -1145,6 +1169,54 @@ async function main() { process.exit(1); } } + + if (descriptor.key === "infraExport") { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + + if (descriptor.key === "deployLockInspect") { + const inspect = result as any; + console.log(); + console.log(` ${colors.dim("Account:")} ${inspect.account}`); + console.log(` ${colors.dim("Gateway:")} ${inspect.gateway}`); + console.log(` ${colors.dim("Network:")} ${inspect.network}`); + console.log(` ${colors.dim("Config:")} ${inspect.configRegistryUrl}`); + console.log(` ${colors.dim("Lock:")} ${inspect.lockRegistryUrl}`); + console.log( + ` ${colors.dim("Status:")} ${inspect.active ? colors.yellow("ACTIVE") : colors.green("free")}`, + ); + if (inspect.value) { + console.log(` ${colors.dim("Owner:")} ${inspect.value.owner}`); + console.log(` ${colors.dim("Nonce:")} ${inspect.value.nonce}`); + console.log( + ` ${colors.dim("Started:")} ${new Date(inspect.value.startedAt).toISOString()}`, + ); + console.log( + ` ${colors.dim("Expires:")} ${new Date(inspect.value.expiresAt).toISOString()}`, + ); + if (inspect.value.txHash) { + console.log(` ${colors.dim("Tx:")} ${inspect.value.txHash}`); + } + } + console.log(); + return; + } + + if (descriptor.key === "deployLockRelease") { + const release = result as { released: boolean; txHash?: string }; + console.log(); + if (release.released) { + console.log(colors.green(`${icons.ok} Deploy lock released`)); + if (release.txHash) { + console.log(` ${colors.dim("Transaction:")} ${release.txHash}`); + } + } else { + console.log(colors.dim(" No active deploy lock to release.")); + } + console.log(); + return; + } } catch (error) { console.error(`[CLI] ${error instanceof Error ? error.message : String(error)}`); process.exit(1); diff --git a/packages/everything-dev/src/cli/deploy-lock.ts b/packages/everything-dev/src/cli/deploy-lock.ts new file mode 100644 index 00000000..2b2c212e --- /dev/null +++ b/packages/everything-dev/src/cli/deploy-lock.ts @@ -0,0 +1,229 @@ +import { randomBytes } from "node:crypto"; +import { Effect } from "effect"; +import { + buildRegistryConfigUrl, + fetchBosConfigFromFastKv, + getFastKvBaseUrlForNetwork, + getRegistryNamespaceForNetwork, + type NetworkId, + parseBosUrl, +} from "../fastkv"; +import { fetchJsonOrNull } from "../http-client"; +import { executeTransaction, type NearSigningMode } from "../near-cli"; + +const LOCK_SUFFIX = "lock/deploy.json"; +const DEFAULT_TTL_MS = 10 * 60 * 1000; +const RELEASE_SENTINEL_VALUE = "{}"; + +export interface DeployLockValue { + owner: string; + pid: number | string; + startedAt: number; + expiresAt: number; + network: NetworkId; + nonce: string; + txHash?: string; +} + +export interface DeployLockConflict { + active: boolean; + expiresAt: number; + value: DeployLockValue | null; + reason: "active" | "verify-mismatch"; +} + +export type AcquireDeployLockResult = + | { acquired: true; nonce: string; txHash?: string } + | { acquired: false; conflict: DeployLockConflict }; + +export interface LockContext { + account: string; + gateway: string; + network: NetworkId; +} + +export interface AcquireOptions extends LockContext { + privateKey?: string; + signingMode?: NearSigningMode; + ttlMs?: number; + owner?: string; +} + +function buildDeployLockKey(account: string, gateway: string): string { + return `apps/${account}/${gateway}/${LOCK_SUFFIX}`; +} + +function buildDeployLockUrl(account: string, gateway: string, network: NetworkId): string { + const baseUrl = getFastKvBaseUrlForNetwork(network); + const namespace = getRegistryNamespaceForNetwork(network); + const key = buildDeployLockKey(account, gateway); + return `${baseUrl}/v0/latest/${encodeURIComponent(namespace)}/${encodeURIComponent(account)}/${encodeURIComponent(key)}`; +} + +function parseDeployLockValue(raw: unknown): DeployLockValue | null { + if (raw == null) return null; + let parsed: unknown = raw; + if (typeof raw === "string") { + if (raw === RELEASE_SENTINEL_VALUE) return null; + try { + parsed = JSON.parse(raw); + } catch { + return null; + } + } + if (!parsed || typeof parsed !== "object") return null; + const obj = parsed as Record; + if (typeof obj.nonce !== "string" || typeof obj.expiresAt !== "number") return null; + return { + owner: typeof obj.owner === "string" ? obj.owner : "unknown", + pid: typeof obj.pid === "number" || typeof obj.pid === "string" ? obj.pid : -1, + startedAt: typeof obj.startedAt === "number" ? obj.startedAt : 0, + expiresAt: obj.expiresAt, + network: + typeof obj.network === "string" && (obj.network === "mainnet" || obj.network === "testnet") + ? obj.network + : "mainnet", + nonce: obj.nonce, + txHash: typeof obj.txHash === "string" ? obj.txHash : undefined, + }; +} + +export async function fetchDeployLock( + ctx: LockContext, +): Promise<{ active: boolean; value: DeployLockValue | null }> { + const url = buildDeployLockUrl(ctx.account, ctx.gateway, ctx.network); + const payload = await fetchJsonOrNull<{ entries?: Array<{ value: unknown } | null> }>(url, { + retries: 0, + }); + const raw = payload?.entries?.find(Boolean)?.value; + const value = parseDeployLockValue(raw); + if (!value) return { active: false, value: null }; + return { active: value.expiresAt > Date.now(), value }; +} + +function buildLockArgsBase64(key: string, value: string): string { + const payload = JSON.stringify({ [key]: value }); + return Buffer.from(payload).toString("base64"); +} + +async function writeDeployLockValue( + ctx: LockContext, + value: string, + signingMode: NearSigningMode, + options: { privateKey?: string; verbose?: boolean }, +): Promise { + const tx = await Effect.runPromise( + executeTransaction( + { + account: ctx.account, + contract: getRegistryNamespaceForNetwork(ctx.network), + method: "__fastdata_kv", + argsBase64: buildLockArgsBase64(buildDeployLockKey(ctx.account, ctx.gateway), value), + network: ctx.network, + privateKey: signingMode._tag === "privateKey" ? signingMode.privateKey : undefined, + gas: "100Tgas", + deposit: "0NEAR", + verbose: options.verbose ?? false, + }, + signingMode, + ), + ); + return tx.txHash; +} + +export async function acquireDeployLock(options: AcquireOptions): Promise { + const signingMode = options.signingMode ?? { + _tag: "privateKey" as const, + privateKey: options.privateKey ?? "", + }; + const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; + const owner = options.owner ?? "bos-publish"; + const nonce = randomBytes(8).toString("hex"); + const now = Date.now(); + const desired: DeployLockValue = { + owner, + pid: process.pid, + startedAt: now, + expiresAt: now + ttlMs, + network: options.network, + nonce, + }; + + const initial = await fetchDeployLock(options); + if (initial.active && initial.value) { + return { + acquired: false, + conflict: { + active: true, + expiresAt: initial.value.expiresAt, + value: initial.value, + reason: "active" as const, + }, + }; + } + + const txHash = await writeDeployLockValue(options, JSON.stringify(desired), signingMode, { + privateKey: options.privateKey, + }); + + const verify = await fetchDeployLock(options); + if (!verify.active || verify.value?.nonce !== nonce) { + return { + acquired: false, + conflict: { + active: verify.active, + expiresAt: verify.value?.expiresAt ?? 0, + value: verify.value, + reason: "verify-mismatch" as const, + }, + }; + } + + return { acquired: true, nonce, txHash }; +} + +export async function releaseDeployLock( + ctx: LockContext, + options: { privateKey?: string; signingMode?: NearSigningMode; force?: boolean }, +): Promise<{ released: boolean; txHash?: string }> { + const signingMode = options.signingMode ?? { + _tag: "privateKey" as const, + privateKey: options.privateKey ?? "", + }; + + const current = await fetchDeployLock(ctx); + + if (!options.force && !current.active) { + return { released: false }; + } + + const txHash = await writeDeployLockValue(ctx, RELEASE_SENTINEL_VALUE, signingMode, { + privateKey: options.privateKey, + }); + return { released: true, txHash }; +} + +export interface InspectDeployLockResult { + account: string; + gateway: string; + network: NetworkId; + configRegistryUrl: string; + lockRegistryUrl: string; + active: boolean; + value: DeployLockValue | null; +} + +export async function inspectDeployLock(ctx: LockContext): Promise { + const state = await fetchDeployLock(ctx); + return { + account: ctx.account, + gateway: ctx.gateway, + network: ctx.network, + configRegistryUrl: buildRegistryConfigUrl(ctx.account, ctx.gateway), + lockRegistryUrl: buildDeployLockUrl(ctx.account, ctx.gateway, ctx.network), + active: state.active, + value: state.value, + }; +} + +export { buildDeployLockKey, buildDeployLockUrl, parseDeployLockValue }; diff --git a/packages/everything-dev/src/cli/infra.ts b/packages/everything-dev/src/cli/infra.ts index 92a0731f..69b80990 100644 --- a/packages/everything-dev/src/cli/infra.ts +++ b/packages/everything-dev/src/cli/infra.ts @@ -118,6 +118,12 @@ export function normalizeRedisSlug(secret: string): string { return secret.replace(/_REDIS_URL$/, "").toLowerCase(); } +function extendsRefAccount(extendsRef: string | undefined): string | null { + if (!extendsRef || typeof extendsRef !== "string") return null; + const match = extendsRef.match(/^bos:\/\/([^/]+)\//); + return match?.[1] ?? null; +} + export function getSecretGroups(runtimeConfig: RuntimeConfig): SecretGroup[] { const groups: SecretGroup[] = []; const seen = new Set(); @@ -172,48 +178,23 @@ export function normalizeDatabaseSlug(secret: string): string { } export function buildOriginMap( - configDir: string, + _configDir: string, runtimeConfig: RuntimeConfig, ): Map { - const configPath = join(configDir, "bos.config.json"); - const originMap = new Map(); const account = runtimeConfig.account; - const resolveOrigin = (extendsRef: unknown): string | null => { - if (typeof extendsRef === "string") { - const match = extendsRef.match(/^bos:\/\/([^/]+)\//); - return match?.[1] ?? null; - } - return null; - }; - - const rawConfig = existsSync(configPath) - ? (JSON.parse(readFileSync(configPath, "utf-8")) as Record) - : null; - const rawPlugins = rawConfig?.plugins as Record | undefined; - for (const secret of runtimeConfig.api.secrets ?? []) { if (!originMap.has(secret)) originMap.set(secret, account); } - const rawApp = rawConfig?.app as Record | undefined; - const authExtends = (rawApp?.auth as Record | undefined)?.extends; - const authOrigin = resolveOrigin(authExtends) ?? account; + const authOrigin = extendsRefAccount(runtimeConfig.auth?.extendsRef) ?? account; for (const secret of runtimeConfig.auth?.secrets ?? []) { if (!originMap.has(secret)) originMap.set(secret, authOrigin); } for (const [pluginKey, pluginEntry] of Object.entries(runtimeConfig.plugins ?? {})) { - const rawPlugin = rawPlugins?.[pluginKey]; - let pluginOrigin: string; - if (typeof rawPlugin === "string") { - pluginOrigin = resolveOrigin(rawPlugin) ?? account; - } else if (rawPlugin && typeof rawPlugin === "object") { - pluginOrigin = resolveOrigin((rawPlugin as Record).extends) ?? account; - } else { - pluginOrigin = account; - } + const pluginOrigin = extendsRefAccount(pluginEntry.extendsRef) ?? account; for (const secret of pluginEntry.secrets ?? []) { if (!originMap.has(secret)) originMap.set(secret, pluginOrigin); } @@ -714,3 +695,146 @@ export function loadProjectEnv(configDir: string): void { loadDotenv({ path: envPath, processEnv: process.env, quiet: true }); envLoadedDir = configDir; } + +export interface CiServiceSpec { + key: string; + slug: string; + image: string; + env: Record; + ports: string[]; + healthcheck?: { test: string[]; interval: string; timeout: string; retries: number }; + volumes: string[]; + database?: { user: string; password: string; name: string }; +} + +export interface CiInfraPlan { + account: string; + gateway: string; + project: string; + env: Record; + services: CiServiceSpec[]; + generatedAt: string; +} + +export function buildCiInfraPlan( + runtimeConfig: RuntimeConfig, + options: { configDir?: string; hostPortOverride?: number } = {}, +): CiInfraPlan { + const configDir = options.configDir; + const originMap = configDir + ? buildOriginMap(configDir, runtimeConfig) + : new Map(); + const portState = loadPortState(configDir); + const allDatabaseSecrets = uniqueSecrets( + collectAllSecrets(runtimeConfig).filter((s) => s.endsWith("_DATABASE_URL")), + ); + const allDatabaseConfigs = buildDatabaseConfigs( + allDatabaseSecrets, + originMap, + portState.postgresPorts, + ); + const allRedisSecrets = uniqueSecrets( + collectAllSecrets(runtimeConfig).filter((s) => s.endsWith("_REDIS_URL")), + ); + const allRedisConfigs = buildRedisConfigs(allRedisSecrets, originMap, portState.redisPorts); + if (configDir) savePortState(configDir, portState); + + const hostPort = + options.hostPortOverride ?? + (Number.isFinite(Number(process.env.BOS_CI_HOST_PORT)) + ? Number(process.env.BOS_CI_HOST_PORT) + : undefined) ?? + resolveDevHostPort(runtimeConfig); + const env: Record = {}; + const envBySecret = new Map(); + for (const db of allDatabaseConfigs) envBySecret.set(db.secret, db.url); + for (const r of allRedisConfigs) envBySecret.set(r.secret, r.url); + + const groups = getSecretGroups(runtimeConfig); + for (const group of groups) { + for (const secret of group.secrets) { + const value = envBySecret.get(secret); + if (value) env[secret] = value; + } + } + + env["CORS_ORIGIN"] = `http://127.0.0.1:${hostPort}`; + if (!env["BETTER_AUTH_SECRET"]) env["BETTER_AUTH_SECRET"] = ""; + + const services: CiServiceSpec[] = []; + + for (const db of allDatabaseConfigs) { + const fromKey = db.fromKey || runtimeConfig.account; + services.push({ + key: db.slug, + slug: db.slug, + image: "postgres:17-alpine", + env: { + POSTGRES_USER: POSTGRES_USER, + POSTGRES_PASSWORD: POSTGRES_PASSWORD, + POSTGRES_DB: db.databaseName, + }, + ports: [`${db.port}:5432`], + healthcheck: { + test: ["CMD-SHELL", `pg_isready -U ${POSTGRES_USER}`], + interval: "3s", + timeout: "3s", + retries: 10, + }, + volumes: [`${fromKey.replace(/\./g, "_")}_postgres_${db.slug}_data:/var/lib/postgresql/data`], + database: { user: POSTGRES_USER, password: POSTGRES_PASSWORD, name: db.databaseName }, + }); + } + + for (const r of allRedisConfigs) { + services.push({ + key: r.slug, + slug: r.slug, + image: "redis:7-alpine", + env: {}, + ports: [`${r.port}:6379`], + healthcheck: { + test: ["CMD", "redis-cli", "ping"], + interval: "3s", + timeout: "3s", + retries: 10, + }, + volumes: [ + `${(r.fromKey || runtimeConfig.account).replace(/\./g, "_")}_redis_${r.slug}_data:/data`, + ], + }); + } + + return { + account: runtimeConfig.account, + gateway: runtimeConfig.domain ?? runtimeConfig.account, + project: runtimeConfig.account, + env, + services, + generatedAt: new Date().toISOString(), + }; +} + +function collectAllSecrets(runtimeConfig: RuntimeConfig): string[] { + const all: string[] = []; + all.push(...(runtimeConfig.host.secrets ?? [])); + all.push(...(runtimeConfig.api.secrets ?? [])); + if (runtimeConfig.auth) all.push(...(runtimeConfig.auth.secrets ?? [])); + if (runtimeConfig.plugins) { + for (const plugin of Object.values(runtimeConfig.plugins)) { + if (plugin.secrets) all.push(...plugin.secrets); + } + } + return all; +} + +function uniqueSlugs(entries: T[]): T[] { + const seen = new Set(); + const out: T[] = []; + for (const entry of entries) { + if (seen.has(entry.slug)) continue; + seen.add(entry.slug); + out.push(entry); + } + return out; +} diff --git a/packages/everything-dev/src/contract.meta.ts b/packages/everything-dev/src/contract.meta.ts index 71418b4b..2e403533 100644 --- a/packages/everything-dev/src/contract.meta.ts +++ b/packages/everything-dev/src/contract.meta.ts @@ -229,4 +229,24 @@ export const cliCommandMeta = { all: { description: "Kill processes across all config directories" }, }, }, + infraExport: { + commandPath: ["infra", "export"], + summary: "Emit the resolved CI infra plan (env + services) for the current runtime", + interactive: false, + fields: { + target: { description: "Export target: ci (default) or local" }, + network: { description: "NEAR network: mainnet or testnet" }, + configDir: { description: "Override config directory" }, + }, + }, + deployLockInspect: { + commandPath: ["deploy", "lock", "inspect"], + summary: "Inspect the FastKV deploy lock for the current account/gateway", + interactive: false, + }, + deployLockRelease: { + commandPath: ["deploy", "lock", "release"], + summary: "Force-release the FastKV deploy lock", + interactive: false, + }, } as const satisfies Record; diff --git a/packages/everything-dev/src/contract.ts b/packages/everything-dev/src/contract.ts index dfcab759..f6c54382 100644 --- a/packages/everything-dev/src/contract.ts +++ b/packages/everything-dev/src/contract.ts @@ -145,16 +145,35 @@ export const PublishOptionsSchema = z.object({ network: z.enum(["mainnet", "testnet"]).optional(), privateKey: z.string().optional(), env: z.enum(["production", "staging"]).default("production"), + noDeployLock: z.boolean().default(false), }); export const PublishResultSchema = z.object({ - status: z.enum(["published", "error", "dry-run"]), + status: z.enum(["published", "error", "dry-run", "locked"]), registryUrl: z.string(), txHash: z.string().optional(), error: z.string().optional(), built: z.array(z.string()).optional(), skipped: z.array(z.string()).optional(), deployResults: z.array(WorkspaceDeployResultSchema).optional(), + lockConflict: z + .object({ + active: z.boolean(), + expiresAt: z.number(), + reason: z.enum(["active", "verify-mismatch"]), + value: z + .object({ + owner: z.string(), + pid: z.union([z.number(), z.string()]), + startedAt: z.number(), + expiresAt: z.number(), + network: z.enum(["mainnet", "testnet"]), + nonce: z.string(), + txHash: z.string().optional(), + }) + .nullable(), + }) + .optional(), }); export const DeployOptionsSchema = z.object({ @@ -166,10 +185,11 @@ export const DeployOptionsSchema = z.object({ network: z.enum(["mainnet", "testnet"]).optional(), privateKey: z.string().optional(), service: z.string().optional(), + noDeployLock: z.boolean().default(false), }); export const DeployResultSchema = z.object({ - status: z.enum(["deployed", "published", "error", "dry-run"]), + status: z.enum(["deployed", "published", "error", "dry-run", "locked"]), registryUrl: z.string(), txHash: z.string().optional(), built: z.array(z.string()).optional(), @@ -178,6 +198,7 @@ export const DeployResultSchema = z.object({ service: z.string().optional(), error: z.string().optional(), deployResults: z.array(WorkspaceDeployResultSchema).optional(), + lockConflict: PublishResultSchema.shape.lockConflict, }); function parseNearAmount(value: string): number { @@ -441,6 +462,37 @@ export const TypecheckResultSchema = z.object({ error: z.string().optional(), }); +export const InfraExportServiceSchema = z.object({ + key: z.string(), + image: z.string(), + env: z.record(z.string(), z.string()).default({}), + ports: z.array(z.string()), + healthcheck: z + .object({ + test: z.array(z.string()), + interval: z.string(), + timeout: z.string(), + retries: z.number(), + }) + .optional(), + volumes: z.array(z.string()).default([]), +}); + +export const InfraExportOptionsSchema = z.object({ + target: z.enum(["ci", "local"]).default("ci"), + network: z.enum(["mainnet", "testnet"]).optional(), + configDir: z.string().optional(), +}); + +export const InfraExportResultSchema = z.object({ + env: z.record(z.string(), z.string()), + services: z.array(InfraExportServiceSchema), + generatedAt: z.string(), + project: z.string(), + account: z.string(), + gateway: z.string(), +}); + export const bosContract = oc.router({ dev: oc.route({ method: "POST", path: "/dev" }).input(DevOptionsSchema).output(DevResultSchema), start: oc @@ -518,6 +570,37 @@ export const bosContract = oc.router({ .route({ method: "POST", path: "/typecheck" }) .input(TypecheckOptionsSchema) .output(TypecheckResultSchema), + infraExport: oc + .route({ method: "POST", path: "/infra/export" }) + .input(InfraExportOptionsSchema) + .output(InfraExportResultSchema), + deployLockInspect: oc.route({ method: "GET", path: "/deploy/lock" }).output( + z.object({ + account: z.string(), + gateway: z.string(), + network: z.enum(["mainnet", "testnet"]), + configRegistryUrl: z.string(), + lockRegistryUrl: z.string(), + active: z.boolean(), + value: z + .object({ + owner: z.string(), + pid: z.union([z.number(), z.string()]), + startedAt: z.number(), + expiresAt: z.number(), + network: z.enum(["mainnet", "testnet"]), + nonce: z.string(), + txHash: z.string().optional(), + }) + .nullable(), + }), + ), + deployLockRelease: oc.route({ method: "POST", path: "/deploy/lock/release" }).output( + z.object({ + released: z.boolean(), + txHash: z.string().optional(), + }), + ), }); export type DevOptions = z.infer; @@ -566,3 +649,6 @@ export type KillResult = z.infer; export type TypecheckOptions = z.infer; export type TypecheckResult = z.infer; export type TypecheckWorkspaceResult = z.infer; +export type InfraExportOptions = z.infer; +export type InfraExportResult = z.infer; +export type InfraExportService = z.infer; diff --git a/packages/everything-dev/src/plugin.ts b/packages/everything-dev/src/plugin.ts index 9b192b53..d347c8c7 100644 --- a/packages/everything-dev/src/plugin.ts +++ b/packages/everything-dev/src/plugin.ts @@ -15,7 +15,10 @@ import { readJsonFile, selectWorkspaceTargets, } from "./build"; +import { inspectDeployLock, releaseDeployLock } from "./cli/deploy-lock"; import { + buildCiInfraPlan, + type CiInfraPlan, ensureEnvFile, loadProjectEnv, syncGeneratedInfra, @@ -1039,6 +1042,8 @@ export default createPlugin({ packages: input.packages, network: input.network, privateKey: input.privateKey, + skipDeployLock: input.noDeployLock, + deployLockTtlMs: resolveDeployLockTtlFromEnv(), }); if (result.publishConfig) { @@ -1057,6 +1062,7 @@ export default createPlugin({ built: result.built, skipped: result.skipped, deployResults: result.deployResults, + lockConflict: result.lockConflict, }; }), @@ -1081,6 +1087,8 @@ export default createPlugin({ packages: input.packages, network: input.network, privateKey: input.privateKey, + skipDeployLock: input.noDeployLock, + deployLockTtlMs: resolveDeployLockTtlFromEnv({ extended: true }), }); if (result.status === "error") { @@ -1093,6 +1101,7 @@ export default createPlugin({ redeployed: false, error: result.error, deployResults: result.deployResults, + lockConflict: result.lockConflict, }; } @@ -1106,6 +1115,20 @@ export default createPlugin({ }; } + if (result.status === "locked") { + return { + status: "locked" as const, + registryUrl: result.registryUrl, + txHash: result.txHash, + built: result.built, + skipped: result.skipped, + redeployed: false, + error: result.error, + deployResults: result.deployResults, + lockConflict: result.lockConflict, + }; + } + if (result.publishConfig) { const refreshed = await loadResolvedConfig({ cwd: deps.configDir }); if (refreshed?.config) { @@ -2130,9 +2153,74 @@ export default createPlugin({ }; } }), + + infraExport: builder.infraExport.handler(async ({ input }) => { + const configDir = input.configDir ?? deps.configDir; + const ci = deps.runtimeConfig ? buildCiInfraPlan(deps.runtimeConfig, { configDir }) : null; + if (!ci) { + const refreshed = await loadResolvedConfig({ cwd: configDir }); + if (!refreshed?.runtime) { + throw new Error("No resolved runtime config available for infra export"); + } + deps.runtimeConfig = refreshed.runtime; + return buildCiInfraPlan(refreshed.runtime, { configDir }); + } + const result: CiInfraPlan & { account: string; gateway: string } = { + ...ci, + account: deps.bosConfig?.account ?? ci.account, + gateway: ci.gateway ?? deps.bosConfig?.domain ?? deps.bosConfig?.account ?? ci.account, + }; + return result; + }), + + deployLockInspect: builder.deployLockInspect.handler(async () => { + if (!deps.bosConfig) { + throw new Error("No bos.config.json found"); + } + const account = deps.bosConfig.account; + const gateway = deps.bosConfig.staging?.domain ?? deps.bosConfig.domain ?? account; + if (!gateway) { + throw new Error("bos.config.json must define domain to inspect deploy lock"); + } + const network = getNetworkIdForAccount(account); + return inspectDeployLock({ account, gateway, network }); + }), + + deployLockRelease: builder.deployLockRelease.handler(async () => { + if (!deps.bosConfig) { + throw new Error("No bos.config.json found"); + } + const account = deps.bosConfig.account; + const gateway = deps.bosConfig.staging?.domain ?? deps.bosConfig.domain ?? account; + if (!gateway) { + throw new Error("bos.config.json must define domain to release deploy lock"); + } + const network = getNetworkIdForAccount(account); + const privateKey = process.env.NEAR_PRIVATE_KEY || process.env.BOS_NEAR_PRIVATE_KEY; + const result = await releaseDeployLock( + { account, gateway, network }, + { privateKey, force: true }, + ); + return { released: result.released, txHash: result.txHash }; + }), }), }); +const PUBLISH_LOCK_TTL_MS = 10 * 60 * 1000; +const DEPLOY_LOCK_TTL_MS = 25 * 60 * 1000; + +export function resolveDeployLockTtlFromEnv(opts: { extended?: boolean } = {}): number { + const raw = process.env.BOS_DEPLOY_LOCK_TTL_MS; + if (raw !== undefined && raw !== "") { + const parsed = Number(raw); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + } + return opts.extended ? DEPLOY_LOCK_TTL_MS : PUBLISH_LOCK_TTL_MS; +} + +export const PUBLISH_LOCK_TTL_DEFAULT_MS = PUBLISH_LOCK_TTL_MS; +export const DEPLOY_LOCK_TTL_DEFAULT_MS = DEPLOY_LOCK_TTL_MS; + function computeAllowedWorkspaces(overrides: string[], plugins?: string[]): string[] { const workspaces: string[] = []; for (const section of overrides) { diff --git a/packages/everything-dev/src/publish.ts b/packages/everything-dev/src/publish.ts index 4f721102..545767fa 100644 --- a/packages/everything-dev/src/publish.ts +++ b/packages/everything-dev/src/publish.ts @@ -3,6 +3,12 @@ import { join } from "node:path"; import process from "node:process"; import { Effect } from "effect"; import { buildWorkspaceTargets, selectWorkspaceTargets } from "./build"; +import { + acquireDeployLock, + type DeployLockConflict, + fetchDeployLock, + releaseDeployLock, +} from "./cli/deploy-lock"; import { generateCodeArtifacts } from "./code-artifacts"; import { loadResolvedConfig } from "./config"; import type { WorkspaceDeployResult } from "./contract"; @@ -10,6 +16,7 @@ import { buildRegistryConfigUrlForNetwork, fetchBosConfigFromFastKv, getRegistryNamespaceForNetwork, + type NetworkId, } from "./fastkv"; import { ensureNearCli, executeTransaction, resolveNearSigningMode } from "./near-cli"; import { getNetworkIdForAccount } from "./network"; @@ -78,10 +85,12 @@ interface PublishToFastKvInput { packages: string; network?: "mainnet" | "testnet"; privateKey?: string; + skipDeployLock?: boolean; + deployLockTtlMs?: number; } interface PublishToFastKvResult { - status: "published" | "error" | "dry-run"; + status: "published" | "error" | "dry-run" | "locked"; registryUrl: string; txHash?: string; built?: string[]; @@ -89,6 +98,7 @@ interface PublishToFastKvResult { error?: string; publishConfig?: BosConfigInput; deployResults?: WorkspaceDeployResult[]; + lockConflict?: DeployLockConflict; } export async function publishToFastKv(input: PublishToFastKvInput): Promise { @@ -107,7 +117,7 @@ export async function publishToFastKv(input: PublishToFastKvInput): Promise !r.success); - if (failures.length > 0) { - const total = deployResults.length; - console.log(); - console.log( - colors.error( - ` ${icons.err} Deploy failed — ${failures.length} of ${total} workspace${total > 1 ? "s" : ""} failed`, - ), - ); - console.log(); - for (const f of failures) { - const errorLine = (f.error ?? "Failed").split("\n")[0]; - console.log(` ${colors.error(icons.err)} ${padRight(f.key, 28)} ${errorLine}`); - } - console.log(); - if (!input.verbose) { - console.log(colors.dim(" Run with --verbose for full build output.")); + try { + if (input.build) { + console.log(" Ensuring NEAR CLI..."); + await Effect.runPromise(ensureNearCli); + console.log(" NEAR CLI ready"); + + await generateCodeArtifacts(configDir, bosConfig, { + env: "production", + runtimeConfig: runtimeConfig ?? undefined, + }); + + const result = await buildWorkspaceTargets({ + configDir, + bosConfig, + runtimeConfig, + targets, + deploy: true, + verbose: input.verbose, + }); + built = result.built; + skipped = result.skipped; + deployResults = result.deployResults; + + if (deployResults) { + const failures = deployResults.filter((r) => !r.success); + if (failures.length > 0) { + const total = deployResults.length; + console.log(); + console.log( + colors.error( + ` ${icons.err} Deploy failed — ${failures.length} of ${total} workspace${total > 1 ? "s" : ""} failed`, + ), + ); console.log(); + for (const f of failures) { + const errorLine = (f.error ?? "Failed").split("\n")[0]; + console.log(` ${colors.error(icons.err)} ${padRight(f.key, 28)} ${errorLine}`); + } + console.log(); + if (!input.verbose) { + console.log(colors.dim(" Run with --verbose for full build output.")); + console.log(); + } + return { + status: "error" as const, + registryUrl, + built, + skipped, + deployResults, + error: `${failures.length} of ${total} workspaces failed to deploy`, + }; } + } + + const refreshed = await loadResolvedConfig({ cwd: configDir }); + if (!refreshed?.config) { return { - status: "error" as const, + status: "error", registryUrl, built, skipped, deployResults, - error: `${failures.length} of ${total} workspaces failed to deploy`, + error: "Failed to reload bos.config.json after build", }; } - } - const refreshed = await loadResolvedConfig({ cwd: configDir }); - if (!refreshed?.config) { - return { - status: "error", - registryUrl, - built, - skipped, - deployResults, - error: "Failed to reload bos.config.json after build", - }; + bosConfig = refreshed.config; } - bosConfig = refreshed.config; - } + const rawConfigPath = join(configDir, "bos.config.json"); + const rawConfig = JSON.parse(readFileSync(rawConfigPath, "utf-8")) as BosConfigInput; + const publishPayload: BosConfigInput = isStaging + ? { ...rawConfig, domain: gateway } + : rawConfig; - const rawConfigPath = join(configDir, "bos.config.json"); - const rawConfig = JSON.parse(readFileSync(rawConfigPath, "utf-8")) as BosConfigInput; - const publishPayload: BosConfigInput = isStaging ? { ...rawConfig, domain: gateway } : rawConfig; - - const registryEntries: Record = { - [`apps/${account}/${gateway}/bos.config.json`]: JSON.stringify(publishPayload), - }; + const registryEntries: Record = { + [`apps/${account}/${gateway}/bos.config.json`]: JSON.stringify(publishPayload), + }; - const payload = JSON.stringify(registryEntries); - const argsBase64 = Buffer.from(payload).toString("base64"); + const payload = JSON.stringify(registryEntries); + const argsBase64 = Buffer.from(payload).toString("base64"); - console.log(); - console.log(" Publishing to:"); - console.log(` ${colors.cyan(registryUrl)}`); + console.log(); + console.log(" Publishing to:"); + console.log(` ${colors.cyan(registryUrl)}`); - try { - let txHash: string | undefined; + try { + let txHash: string | undefined; - console.log(` Submitting transaction on ${network}...`); + console.log(` Submitting transaction on ${network}...`); - try { - const tx = await Effect.runPromise( - executeTransaction( - { + try { + const tx = await Effect.runPromise( + executeTransaction( + { + account, + contract: getRegistryNamespaceForNetwork(network), + method: "__fastdata_kv", + argsBase64, + network, + privateKey: signingMode._tag === "privateKey" ? signingMode.privateKey : undefined, + gas: "300Tgas", + deposit: "0NEAR", + verbose: input.verbose, + }, + signingMode, + ), + ); + txHash = tx.txHash; + if (txHash && !tx.output?.includes("CodeDoesNotExist")) { + console.log(` Transaction submitted: ${colors.dim(txHash)}`); + } + } catch (error) { + console.log(colors.dim(" Transaction reported an error — verifying publish...")); + try { + await waitForPublishedConfig({ account, - contract: getRegistryNamespaceForNetwork(network), - method: "__fastdata_kv", - argsBase64, - network, - privateKey: signingMode._tag === "privateKey" ? signingMode.privateKey : undefined, - gas: "300Tgas", - deposit: "0NEAR", - verbose: input.verbose, - }, - signingMode, - ), - ); - txHash = tx.txHash; - if (txHash && !tx.output?.includes("CodeDoesNotExist")) { - console.log(` Transaction submitted: ${colors.dim(txHash)}`); + gateway, + publishConfig: publishPayload, + timeoutMs: 30_000, + intervalMs: 2_000, + }); + txHash = extractTransactionHash(error); + } catch { + throw error; + } } + + console.log(" Waiting for publish confirmation..."); + await waitForPublishedConfig({ + account, + gateway, + publishConfig: publishPayload, + }); + + return { + status: "published", + registryUrl, + txHash, + built, + skipped, + deployResults, + publishConfig: publishPayload, + }; } catch (error) { - console.log(colors.dim(" Transaction reported an error — verifying publish...")); + return { + status: "error", + registryUrl, + error: formatNearError(error), + built, + skipped, + deployResults, + }; + } + } finally { + if (!input.skipDeployLock) { try { - await waitForPublishedConfig({ - account, - gateway, - publishConfig: publishPayload, - timeoutMs: 30_000, - intervalMs: 2_000, - }); - txHash = extractTransactionHash(error); - } catch { - throw error; + await releaseDeployLock(lockContext, { privateKey, signingMode }); + } catch (error) { + console.log( + colors.dim( + ` Failed to release deploy lock for ${account}/${gateway}: ${error instanceof Error ? error.message : String(error)}`, + ), + ); } } - - console.log(" Waiting for publish confirmation..."); - await waitForPublishedConfig({ - account, - gateway, - publishConfig: publishPayload, - }); - - return { - status: "published", - registryUrl, - txHash, - built, - skipped, - deployResults, - publishConfig: publishPayload, - }; - } catch (error) { - return { - status: "error", - registryUrl, - error: formatNearError(error), - built, - skipped, - deployResults, - }; } } diff --git a/packages/everything-dev/tests/unit/deploy-lock.test.ts b/packages/everything-dev/tests/unit/deploy-lock.test.ts new file mode 100644 index 00000000..40937e3b --- /dev/null +++ b/packages/everything-dev/tests/unit/deploy-lock.test.ts @@ -0,0 +1,115 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { + buildDeployLockKey, + buildDeployLockUrl, + parseDeployLockValue, +} from "../../src/cli/deploy-lock"; +import { + DEPLOY_LOCK_TTL_DEFAULT_MS, + PUBLISH_LOCK_TTL_DEFAULT_MS, + resolveDeployLockTtlFromEnv, +} from "../../src/plugin"; + +describe("deploy-lock TTL resolution", () => { + const previous = process.env.BOS_DEPLOY_LOCK_TTL_MS; + + afterEach(() => { + if (previous === undefined) delete process.env.BOS_DEPLOY_LOCK_TTL_MS; + else process.env.BOS_DEPLOY_LOCK_TTL_MS = previous; + }); + + it("uses 10 min default for publish", () => { + delete process.env.BOS_DEPLOY_LOCK_TTL_MS; + expect(resolveDeployLockTtlFromEnv()).toBe(PUBLISH_LOCK_TTL_DEFAULT_MS); + expect(resolveDeployLockTtlFromEnv({ extended: false })).toBe(PUBLISH_LOCK_TTL_DEFAULT_MS); + }); + + it("uses 25 min default for deploy", () => { + delete process.env.BOS_DEPLOY_LOCK_TTL_MS; + expect(resolveDeployLockTtlFromEnv({ extended: true })).toBe(DEPLOY_LOCK_TTL_DEFAULT_MS); + }); + + it("honors BOS_DEPLOY_LOCK_TTL_MS override for both modes", () => { + process.env.BOS_DEPLOY_LOCK_TTL_MS = "1800000"; + expect(resolveDeployLockTtlFromEnv()).toBe(1_800_000); + expect(resolveDeployLockTtlFromEnv({ extended: true })).toBe(1_800_000); + }); + + it("falls back to default when env value is invalid", () => { + process.env.BOS_DEPLOY_LOCK_TTL_MS = "not-a-number"; + expect(resolveDeployLockTtlFromEnv()).toBe(PUBLISH_LOCK_TTL_DEFAULT_MS); + expect(resolveDeployLockTtlFromEnv({ extended: true })).toBe(DEPLOY_LOCK_TTL_DEFAULT_MS); + }); +}); + +describe("deploy-lock helpers", () => { + describe("parseDeployLockValue", () => { + it("returns null for empty/sentinel values", () => { + expect(parseDeployLockValue(null)).toBeNull(); + expect(parseDeployLockValue(undefined)).toBeNull(); + expect(parseDeployLockValue("{}")).toBeNull(); + }); + + it("parses stringified JSON values", () => { + const stored = JSON.stringify({ + owner: "alice", + pid: 4242, + startedAt: 1, + expiresAt: Date.now() + 60_000, + network: "mainnet", + nonce: "abc123", + }); + const result = parseDeployLockValue(stored); + expect(result).not.toBeNull(); + expect(result?.owner).toBe("alice"); + expect(result?.pid).toBe(4242); + expect(result?.nonce).toBe("abc123"); + expect(result?.network).toBe("mainnet"); + }); + + it("parses object values", () => { + const result = parseDeployLockValue({ + owner: "deployer", + pid: 99, + startedAt: 1000, + expiresAt: 2000, + network: "testnet", + nonce: "deadbeef", + }); + expect(result?.owner).toBe("deployer"); + expect(result?.network).toBe("testnet"); + }); + + it("returns null when nonce or expiresAt is missing", () => { + expect(parseDeployLockValue({ owner: "x" })).toBeNull(); + expect(parseDeployLockValue({ nonce: "abc" })).toBeNull(); + expect(parseDeployLockValue({ expiresAt: 1000 })).toBeNull(); + }); + }); + + describe("buildDeployLockKey", () => { + it("returns expected apps/.../lock/deploy.json path", () => { + expect(buildDeployLockKey("v1.foo.near", "citynode.app")).toBe( + "apps/v1.foo.near/citynode.app/lock/deploy.json", + ); + }); + }); + + describe("buildDeployLockUrl", () => { + it("builds the FastKV GET URL for mainnet", () => { + const url = buildDeployLockUrl("v1.foo.near", "citynode.app", "mainnet"); + expect(url).toContain("kv.main.fastnear.com"); + expect(url).toContain("v1.foo.near"); + expect(encodeURIComponent("apps/v1.foo.near/citynode.app/lock/deploy.json")).toBeTruthy(); + expect( + decodeURIComponent(encodeURIComponent("apps/v1.foo.near/citynode.app/lock/deploy.json")), + ).toBe("apps/v1.foo.near/citynode.app/lock/deploy.json"); + expect(url).toContain("deploy.json"); + }); + + it("builds the FastKV GET URL for testnet", () => { + const url = buildDeployLockUrl("v1.foo.near", "citynode.app", "testnet"); + expect(url).toContain("kv.test.fastnear.com"); + }); + }); +}); diff --git a/packages/everything-dev/tests/unit/infra-export.test.ts b/packages/everything-dev/tests/unit/infra-export.test.ts new file mode 100644 index 00000000..e3c5fffb --- /dev/null +++ b/packages/everything-dev/tests/unit/infra-export.test.ts @@ -0,0 +1,199 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + buildCiInfraPlan, + buildOriginMap, + ensureEnvFile, + syncGeneratedInfra, + writeGeneratedInfra, +} from "../../src/cli/infra"; +import type { RuntimeConfig, RuntimePluginConfig } from "../../src/types"; + +function buildRuntimeConfig(overrides?: Partial): RuntimeConfig { + return { + env: "development", + account: "dev.everything.near", + networkId: "mainnet", + domain: "dev.everything.dev", + host: { + name: "host", + url: "http://localhost:4100", + entry: "/mf-manifest.json", + port: 4100, + }, + ui: { name: "ui", url: "http://localhost:3003", entry: "/mf-manifest.json" }, + api: { + name: "api", + url: "http://localhost:3001", + entry: "/mf-manifest.json", + secrets: ["API_DATABASE_URL"], + }, + auth: { + name: "auth", + url: "http://localhost:3002", + entry: "/mf-manifest.json", + secrets: ["AUTH_DATABASE_URL", "BETTER_AUTH_SECRET", "CORS_ORIGIN"], + }, + plugins: { + example: { + name: "example", + url: "http://localhost:3010", + entry: "/mf-manifest.json", + source: "local" as const, + secrets: ["EXAMPLE_DATABASE_URL"], + } as RuntimePluginConfig, + }, + ...overrides, + } as RuntimeConfig; +} + +describe("buildCiInfraPlan", () => { + const tempDirs: string[] = []; + + afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) rmSync(dir, { recursive: true, force: true }); + } + }); + + it("emits env vars and services for api+auth+plugin secrets", () => { + const dir = mkdtempSync(join(tmpdir(), "bos-ci-infra-")); + tempDirs.push(dir); + writeGeneratedInfra(dir, buildRuntimeConfig()); + ensureEnvFile(dir); + + const runtime = { + ...buildRuntimeConfig(), + env: "production" as const, + }; + const plan = buildCiInfraPlan(runtime, { configDir: dir }); + + expect(plan.env["API_DATABASE_URL"]).toBe( + "postgres://everythingdev:everythingdev@localhost:5432/api_db", + ); + expect(plan.env["AUTH_DATABASE_URL"]).toBe( + "postgres://everythingdev:everythingdev@localhost:5433/auth_db", + ); + expect(plan.env["EXAMPLE_DATABASE_URL"]).toBe( + "postgres://everythingdev:everythingdev@localhost:5434/example_db", + ); + expect(plan.env["BETTER_AUTH_SECRET"]).toBe(""); + expect(plan.env["CORS_ORIGIN"]).toBe("http://127.0.0.1:4100"); + + expect(plan.services.length).toBeGreaterThanOrEqual(3); + const serviceKeys = plan.services.map((s) => s.key); + expect(serviceKeys).toContain("api"); + expect(serviceKeys).toContain("auth"); + expect(serviceKeys).toContain("example"); + + const apiService = plan.services.find((s) => s.key === "api"); + expect(apiService?.image).toBe("postgres:17-alpine"); + expect(apiService?.ports).toEqual(["5432:5432"]); + expect(apiService?.database).toEqual({ + user: "everythingdev", + password: "everythingdev", + name: "api_db", + }); + }); + + it("honors hostPortOverride and BOS_CI_HOST_PORT env fallback", () => { + const previous = process.env.BOS_CI_HOST_PORT; + try { + process.env.BOS_CI_HOST_PORT = "5173"; + const plan = buildCiInfraPlan(buildRuntimeConfig(), {}); + expect(plan.env["CORS_ORIGIN"]).toBe("http://127.0.0.1:5173"); + + const override = buildCiInfraPlan(buildRuntimeConfig(), { hostPortOverride: 8080 }); + expect(override.env["CORS_ORIGIN"]).toBe("http://127.0.0.1:8080"); + } finally { + if (previous === undefined) delete process.env.BOS_CI_HOST_PORT; + else process.env.BOS_CI_HOST_PORT = previous; + } + }); + + it("tracks stable ports across calls (portMap persistence)", () => { + const dir = mkdtempSync(join(tmpdir(), "bos-ci-infra-ports-")); + tempDirs.push(dir); + const cfg = buildRuntimeConfig(); + syncGeneratedInfra(dir, cfg); + const first = buildCiInfraPlan(cfg, { configDir: dir }); + const second = buildCiInfraPlan(cfg, { configDir: dir }); + + const firstApi = first.services.find((s) => s.key === "api"); + const secondApi = second.services.find((s) => s.key === "api"); + expect(firstApi?.ports).toEqual(secondApi?.ports); + }); +}); + +describe("buildOriginMap from resolved RuntimeConfig", () => { + it("uses plugin extendsRef for plugin origins", () => { + const runtime: RuntimeConfig = { + env: "development", + account: "city.example.near", + networkId: "mainnet", + host: { name: "host", url: "http://localhost:3000", entry: "/mf-manifest.json" }, + ui: { name: "ui", url: "http://localhost:3003", entry: "/mf-manifest.json" }, + api: { + name: "api", + url: "http://localhost:3001", + entry: "/mf-manifest.json", + secrets: ["API_DATABASE_URL"], + }, + auth: { + name: "auth", + url: "http://localhost:3002", + entry: "/mf-manifest.json", + extendsRef: "bos://auth.everything.near/auth.everything.dev#app.auth", + secrets: ["AUTH_DATABASE_URL"], + }, + plugins: { + apps: { + name: "apps", + url: "http://localhost:3010", + entry: "/mf-manifest.json", + source: "local" as const, + extendsRef: "bos://something/near/gateway", + secrets: ["APPS_DATABASE_URL"], + } as RuntimePluginConfig, + }, + } as RuntimeConfig; + + const map = buildOriginMap("", runtime); + + expect(map.get("API_DATABASE_URL")).toBe("city.example.near"); + expect(map.get("AUTH_DATABASE_URL")).toBe("auth.everything.near"); + expect(map.get("APPS_DATABASE_URL")).toBe("something"); + }); + + it("falls back to runtime.account when extendsRef is absent", () => { + const runtime: RuntimeConfig = { + env: "development", + account: "city.example.near", + networkId: "mainnet", + host: { name: "host", url: "http://localhost:3000", entry: "/mf-manifest.json" }, + ui: { name: "ui", url: "http://localhost:3003", entry: "/mf-manifest.json" }, + api: { + name: "api", + url: "http://localhost:3001", + entry: "/mf-manifest.json", + secrets: ["API_DATABASE_URL"], + }, + plugins: { + localonly: { + name: "localonly", + url: "http://localhost:3010", + entry: "/mf-manifest.json", + source: "local" as const, + secrets: ["LOCALONLY_DATABASE_URL"], + } as RuntimePluginConfig, + }, + } as RuntimeConfig; + + const map = buildOriginMap("", runtime); + expect(map.get("API_DATABASE_URL")).toBe("city.example.near"); + expect(map.get("LOCALONLY_DATABASE_URL")).toBe("city.example.near"); + }); +}); diff --git a/ui/src/routes/_layout/_authenticated/stake.tsx b/ui/src/routes/_layout/_authenticated/stake.tsx index a48625c1..85481ed9 100644 --- a/ui/src/routes/_layout/_authenticated/stake.tsx +++ b/ui/src/routes/_layout/_authenticated/stake.tsx @@ -593,7 +593,7 @@ function CityNodeCard({ - ) + ); } function StatBlock({ label, value }: { label: string; value: string | number }) { From 409eae05285abe0f78d54a27649739dccc47a3dc Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Sat, 15 Aug 2026 22:37:03 -0500 Subject: [PATCH 16/24] feat: remove deploy lock, keep infra export Remove FastKV-backed deploy lock feature: - Delete bos deploy lock acquire/release/inspect commands - Remove lock logic from publishToFastKv - Remove lockConflict from PublishResult and DeployResult schemas - Concurrent deploys now follow last-write-wins semantics (harmless for Railway redeploy) Keep bos infra export command: - Emits {env, services, account, gateway, project, generatedAt} JSON - buildOriginMap reads runtimeConfig.*.extendsRef (no raw JSON re-parse) Tests: 263 pass, failures are pre-existing (auth types, template property) --- .changeset/deploy-lock-and-infra-export.md | 12 +- packages/everything-dev/src/cli.ts | 75 +----- .../everything-dev/src/cli/deploy-lock.ts | 229 ------------------ packages/everything-dev/src/contract.meta.ts | 10 - packages/everything-dev/src/contract.ts | 52 +--- packages/everything-dev/src/plugin.ts | 67 ----- packages/everything-dev/src/publish.ts | 74 +----- .../tests/unit/deploy-lock.test.ts | 115 --------- 8 files changed, 13 insertions(+), 621 deletions(-) delete mode 100644 packages/everything-dev/src/cli/deploy-lock.ts delete mode 100644 packages/everything-dev/tests/unit/deploy-lock.test.ts diff --git a/.changeset/deploy-lock-and-infra-export.md b/.changeset/deploy-lock-and-infra-export.md index c7c363da..8a435f5b 100644 --- a/.changeset/deploy-lock-and-infra-export.md +++ b/.changeset/deploy-lock-and-infra-export.md @@ -1,12 +1,10 @@ --- -"everything-dev": minor +"everything-dev": patch --- -Add a per-account/gateway FastKV-backed deploy lock, a `bos infra export` command that emits the resolved CI infra plan, and read plugin `extendsRef` from the resolved runtime config (instead of re-parsing raw `bos.config.json`). +Remove deploy lock feature and add `bos infra export` command. -- `bos publish --deploy` and `bos deploy` now acquire `apps///lock/deploy.json` before publishing. Stale or concurrent dispatches fail fast with `status: "locked"` and a conflict payload listing the owner, nonce, and txHash. Use `--no-deploy-lock` to opt out (deploy lock is on by default). The lock is released in a `finally` block after publish confirmation. - - `bos publish` holds the lock for 10 minutes by default (the FastKV write window). `bos deploy` holds it for 25 minutes by default to cover publish + Railway redeploy. Override either with `BOS_DEPLOY_LOCK_TTL_MS` (positive integer, milliseconds). -- `bos deploy lock inspect` reports the active lock owner/nonce/expires/txHash; `bos deploy lock release` force-clears a stuck lock. -- `bos infra export [--target ci|local] [--network mainnet|testnet]` emits `{env, services, account, gateway, project, generatedAt}` JSON. The deploy workflow now consumes this to populate `$GITHUB_ENV` instead of repeating `API_DATABASE_URL`, `AUTH_DATABASE_URL`, and `CORS_ORIGIN` literals. Host port comes from `BOS_CI_HOST_PORT` env or `runtimeConfig.host.port`. +- Removed `bos deploy lock acquire/release/inspect` commands and FastKV-backed deploy lock logic. Concurrent deploys now follow last-write-wins semantics (harmless redundancy for Railway redeploy). +- `bos infra export [--target ci|local] [--network mainnet|testnet]` emits `{env, services, account, gateway, project, generatedAt}` JSON. The deploy workflow consumes this to populate `$GITHUB_ENV` instead of repeating `API_DATABASE_URL`, `AUTH_DATABASE_URL`, and `CORS_ORIGIN` literals. Host port comes from `BOS_CI_HOST_PORT` env or `runtimeConfig.host.port`. - `buildOriginMap` now derives plugin origins from `runtimeConfig.plugins[id].extendsRef` / `runtimeConfig.auth?.extendsRef` (already populated by `loadResolvedConfig`), removing the duplicate raw-JSON read and the parent-runtime fallback logic. -- Three new routes on the `bos` contract: `infraExport`, `deployLockInspect`, `deployLockRelease`. New tests cover the lock helpers, the CI plan builder, and the resolved-config origin lookup. +- New tests cover the CI plan builder and the resolved-config origin lookup. diff --git a/packages/everything-dev/src/cli.ts b/packages/everything-dev/src/cli.ts index ed9de076..2c3bf892 100644 --- a/packages/everything-dev/src/cli.ts +++ b/packages/everything-dev/src/cli.ts @@ -1019,24 +1019,12 @@ async function main() { return; } - if (result.status === "error" || result.status === "locked") { + if (result.status === "error") { console.log(); - console.log( - colors.error( - `${icons.err} ${result.status === "locked" ? "Deploy lock blocks publish" : "Publish failed"}`, - ), - ); + console.log(colors.error(`${icons.err} Publish failed`)); if (result.error) { console.log(` ${colors.dim("Error:")} ${result.error}`); } - if (result.lockConflict?.value) { - const owner = result.lockConflict.value.owner; - const nonce = result.lockConflict.value.nonce; - const expiresAt = result.lockConflict.value.expiresAt; - console.log(` ${colors.dim("Lock owner:")} ${owner}`); - console.log(` ${colors.dim("Lock nonce:")} ${nonce}`); - console.log(` ${colors.dim("Lock expires:")} ${new Date(expiresAt).toISOString()}`); - } if (result.deployResults && result.deployResults.length > 0) { const failures = result.deployResults.filter((r: any) => !r.success); if (failures.length > 0) { @@ -1089,24 +1077,12 @@ async function main() { return; } - if (deployResult.status === "error" || deployResult.status === "locked") { + if (deployResult.status === "error") { console.log(); - console.log( - colors.error( - `${icons.err} ${deployResult.status === "locked" ? "Deploy lock blocks publish" : "Deploy failed"}`, - ), - ); + console.log(colors.error(`${icons.err} Deploy failed`)); if (deployResult.error) { console.log(` ${colors.dim("Error:")} ${deployResult.error}`); } - if (deployResult.lockConflict?.value) { - const owner = deployResult.lockConflict.value.owner; - const nonce = deployResult.lockConflict.value.nonce; - const expiresAt = deployResult.lockConflict.value.expiresAt; - console.log(` ${colors.dim("Lock owner:")} ${owner}`); - console.log(` ${colors.dim("Lock nonce:")} ${nonce}`); - console.log(` ${colors.dim("Lock expires:")} ${new Date(expiresAt).toISOString()}`); - } if (deployResult.deployResults && deployResult.deployResults.length > 0) { const failures = deployResult.deployResults.filter((r: any) => !r.success); if (failures.length > 0) { @@ -1174,49 +1150,6 @@ async function main() { process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); return; } - - if (descriptor.key === "deployLockInspect") { - const inspect = result as any; - console.log(); - console.log(` ${colors.dim("Account:")} ${inspect.account}`); - console.log(` ${colors.dim("Gateway:")} ${inspect.gateway}`); - console.log(` ${colors.dim("Network:")} ${inspect.network}`); - console.log(` ${colors.dim("Config:")} ${inspect.configRegistryUrl}`); - console.log(` ${colors.dim("Lock:")} ${inspect.lockRegistryUrl}`); - console.log( - ` ${colors.dim("Status:")} ${inspect.active ? colors.yellow("ACTIVE") : colors.green("free")}`, - ); - if (inspect.value) { - console.log(` ${colors.dim("Owner:")} ${inspect.value.owner}`); - console.log(` ${colors.dim("Nonce:")} ${inspect.value.nonce}`); - console.log( - ` ${colors.dim("Started:")} ${new Date(inspect.value.startedAt).toISOString()}`, - ); - console.log( - ` ${colors.dim("Expires:")} ${new Date(inspect.value.expiresAt).toISOString()}`, - ); - if (inspect.value.txHash) { - console.log(` ${colors.dim("Tx:")} ${inspect.value.txHash}`); - } - } - console.log(); - return; - } - - if (descriptor.key === "deployLockRelease") { - const release = result as { released: boolean; txHash?: string }; - console.log(); - if (release.released) { - console.log(colors.green(`${icons.ok} Deploy lock released`)); - if (release.txHash) { - console.log(` ${colors.dim("Transaction:")} ${release.txHash}`); - } - } else { - console.log(colors.dim(" No active deploy lock to release.")); - } - console.log(); - return; - } } catch (error) { console.error(`[CLI] ${error instanceof Error ? error.message : String(error)}`); process.exit(1); diff --git a/packages/everything-dev/src/cli/deploy-lock.ts b/packages/everything-dev/src/cli/deploy-lock.ts deleted file mode 100644 index 2b2c212e..00000000 --- a/packages/everything-dev/src/cli/deploy-lock.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { randomBytes } from "node:crypto"; -import { Effect } from "effect"; -import { - buildRegistryConfigUrl, - fetchBosConfigFromFastKv, - getFastKvBaseUrlForNetwork, - getRegistryNamespaceForNetwork, - type NetworkId, - parseBosUrl, -} from "../fastkv"; -import { fetchJsonOrNull } from "../http-client"; -import { executeTransaction, type NearSigningMode } from "../near-cli"; - -const LOCK_SUFFIX = "lock/deploy.json"; -const DEFAULT_TTL_MS = 10 * 60 * 1000; -const RELEASE_SENTINEL_VALUE = "{}"; - -export interface DeployLockValue { - owner: string; - pid: number | string; - startedAt: number; - expiresAt: number; - network: NetworkId; - nonce: string; - txHash?: string; -} - -export interface DeployLockConflict { - active: boolean; - expiresAt: number; - value: DeployLockValue | null; - reason: "active" | "verify-mismatch"; -} - -export type AcquireDeployLockResult = - | { acquired: true; nonce: string; txHash?: string } - | { acquired: false; conflict: DeployLockConflict }; - -export interface LockContext { - account: string; - gateway: string; - network: NetworkId; -} - -export interface AcquireOptions extends LockContext { - privateKey?: string; - signingMode?: NearSigningMode; - ttlMs?: number; - owner?: string; -} - -function buildDeployLockKey(account: string, gateway: string): string { - return `apps/${account}/${gateway}/${LOCK_SUFFIX}`; -} - -function buildDeployLockUrl(account: string, gateway: string, network: NetworkId): string { - const baseUrl = getFastKvBaseUrlForNetwork(network); - const namespace = getRegistryNamespaceForNetwork(network); - const key = buildDeployLockKey(account, gateway); - return `${baseUrl}/v0/latest/${encodeURIComponent(namespace)}/${encodeURIComponent(account)}/${encodeURIComponent(key)}`; -} - -function parseDeployLockValue(raw: unknown): DeployLockValue | null { - if (raw == null) return null; - let parsed: unknown = raw; - if (typeof raw === "string") { - if (raw === RELEASE_SENTINEL_VALUE) return null; - try { - parsed = JSON.parse(raw); - } catch { - return null; - } - } - if (!parsed || typeof parsed !== "object") return null; - const obj = parsed as Record; - if (typeof obj.nonce !== "string" || typeof obj.expiresAt !== "number") return null; - return { - owner: typeof obj.owner === "string" ? obj.owner : "unknown", - pid: typeof obj.pid === "number" || typeof obj.pid === "string" ? obj.pid : -1, - startedAt: typeof obj.startedAt === "number" ? obj.startedAt : 0, - expiresAt: obj.expiresAt, - network: - typeof obj.network === "string" && (obj.network === "mainnet" || obj.network === "testnet") - ? obj.network - : "mainnet", - nonce: obj.nonce, - txHash: typeof obj.txHash === "string" ? obj.txHash : undefined, - }; -} - -export async function fetchDeployLock( - ctx: LockContext, -): Promise<{ active: boolean; value: DeployLockValue | null }> { - const url = buildDeployLockUrl(ctx.account, ctx.gateway, ctx.network); - const payload = await fetchJsonOrNull<{ entries?: Array<{ value: unknown } | null> }>(url, { - retries: 0, - }); - const raw = payload?.entries?.find(Boolean)?.value; - const value = parseDeployLockValue(raw); - if (!value) return { active: false, value: null }; - return { active: value.expiresAt > Date.now(), value }; -} - -function buildLockArgsBase64(key: string, value: string): string { - const payload = JSON.stringify({ [key]: value }); - return Buffer.from(payload).toString("base64"); -} - -async function writeDeployLockValue( - ctx: LockContext, - value: string, - signingMode: NearSigningMode, - options: { privateKey?: string; verbose?: boolean }, -): Promise { - const tx = await Effect.runPromise( - executeTransaction( - { - account: ctx.account, - contract: getRegistryNamespaceForNetwork(ctx.network), - method: "__fastdata_kv", - argsBase64: buildLockArgsBase64(buildDeployLockKey(ctx.account, ctx.gateway), value), - network: ctx.network, - privateKey: signingMode._tag === "privateKey" ? signingMode.privateKey : undefined, - gas: "100Tgas", - deposit: "0NEAR", - verbose: options.verbose ?? false, - }, - signingMode, - ), - ); - return tx.txHash; -} - -export async function acquireDeployLock(options: AcquireOptions): Promise { - const signingMode = options.signingMode ?? { - _tag: "privateKey" as const, - privateKey: options.privateKey ?? "", - }; - const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; - const owner = options.owner ?? "bos-publish"; - const nonce = randomBytes(8).toString("hex"); - const now = Date.now(); - const desired: DeployLockValue = { - owner, - pid: process.pid, - startedAt: now, - expiresAt: now + ttlMs, - network: options.network, - nonce, - }; - - const initial = await fetchDeployLock(options); - if (initial.active && initial.value) { - return { - acquired: false, - conflict: { - active: true, - expiresAt: initial.value.expiresAt, - value: initial.value, - reason: "active" as const, - }, - }; - } - - const txHash = await writeDeployLockValue(options, JSON.stringify(desired), signingMode, { - privateKey: options.privateKey, - }); - - const verify = await fetchDeployLock(options); - if (!verify.active || verify.value?.nonce !== nonce) { - return { - acquired: false, - conflict: { - active: verify.active, - expiresAt: verify.value?.expiresAt ?? 0, - value: verify.value, - reason: "verify-mismatch" as const, - }, - }; - } - - return { acquired: true, nonce, txHash }; -} - -export async function releaseDeployLock( - ctx: LockContext, - options: { privateKey?: string; signingMode?: NearSigningMode; force?: boolean }, -): Promise<{ released: boolean; txHash?: string }> { - const signingMode = options.signingMode ?? { - _tag: "privateKey" as const, - privateKey: options.privateKey ?? "", - }; - - const current = await fetchDeployLock(ctx); - - if (!options.force && !current.active) { - return { released: false }; - } - - const txHash = await writeDeployLockValue(ctx, RELEASE_SENTINEL_VALUE, signingMode, { - privateKey: options.privateKey, - }); - return { released: true, txHash }; -} - -export interface InspectDeployLockResult { - account: string; - gateway: string; - network: NetworkId; - configRegistryUrl: string; - lockRegistryUrl: string; - active: boolean; - value: DeployLockValue | null; -} - -export async function inspectDeployLock(ctx: LockContext): Promise { - const state = await fetchDeployLock(ctx); - return { - account: ctx.account, - gateway: ctx.gateway, - network: ctx.network, - configRegistryUrl: buildRegistryConfigUrl(ctx.account, ctx.gateway), - lockRegistryUrl: buildDeployLockUrl(ctx.account, ctx.gateway, ctx.network), - active: state.active, - value: state.value, - }; -} - -export { buildDeployLockKey, buildDeployLockUrl, parseDeployLockValue }; diff --git a/packages/everything-dev/src/contract.meta.ts b/packages/everything-dev/src/contract.meta.ts index 2e403533..46e5aa65 100644 --- a/packages/everything-dev/src/contract.meta.ts +++ b/packages/everything-dev/src/contract.meta.ts @@ -239,14 +239,4 @@ export const cliCommandMeta = { configDir: { description: "Override config directory" }, }, }, - deployLockInspect: { - commandPath: ["deploy", "lock", "inspect"], - summary: "Inspect the FastKV deploy lock for the current account/gateway", - interactive: false, - }, - deployLockRelease: { - commandPath: ["deploy", "lock", "release"], - summary: "Force-release the FastKV deploy lock", - interactive: false, - }, } as const satisfies Record; diff --git a/packages/everything-dev/src/contract.ts b/packages/everything-dev/src/contract.ts index f6c54382..857040a4 100644 --- a/packages/everything-dev/src/contract.ts +++ b/packages/everything-dev/src/contract.ts @@ -145,35 +145,16 @@ export const PublishOptionsSchema = z.object({ network: z.enum(["mainnet", "testnet"]).optional(), privateKey: z.string().optional(), env: z.enum(["production", "staging"]).default("production"), - noDeployLock: z.boolean().default(false), }); export const PublishResultSchema = z.object({ - status: z.enum(["published", "error", "dry-run", "locked"]), + status: z.enum(["published", "error", "dry-run"]), registryUrl: z.string(), txHash: z.string().optional(), error: z.string().optional(), built: z.array(z.string()).optional(), skipped: z.array(z.string()).optional(), deployResults: z.array(WorkspaceDeployResultSchema).optional(), - lockConflict: z - .object({ - active: z.boolean(), - expiresAt: z.number(), - reason: z.enum(["active", "verify-mismatch"]), - value: z - .object({ - owner: z.string(), - pid: z.union([z.number(), z.string()]), - startedAt: z.number(), - expiresAt: z.number(), - network: z.enum(["mainnet", "testnet"]), - nonce: z.string(), - txHash: z.string().optional(), - }) - .nullable(), - }) - .optional(), }); export const DeployOptionsSchema = z.object({ @@ -185,11 +166,10 @@ export const DeployOptionsSchema = z.object({ network: z.enum(["mainnet", "testnet"]).optional(), privateKey: z.string().optional(), service: z.string().optional(), - noDeployLock: z.boolean().default(false), }); export const DeployResultSchema = z.object({ - status: z.enum(["deployed", "published", "error", "dry-run", "locked"]), + status: z.enum(["deployed", "published", "error", "dry-run"]), registryUrl: z.string(), txHash: z.string().optional(), built: z.array(z.string()).optional(), @@ -198,7 +178,6 @@ export const DeployResultSchema = z.object({ service: z.string().optional(), error: z.string().optional(), deployResults: z.array(WorkspaceDeployResultSchema).optional(), - lockConflict: PublishResultSchema.shape.lockConflict, }); function parseNearAmount(value: string): number { @@ -574,33 +553,6 @@ export const bosContract = oc.router({ .route({ method: "POST", path: "/infra/export" }) .input(InfraExportOptionsSchema) .output(InfraExportResultSchema), - deployLockInspect: oc.route({ method: "GET", path: "/deploy/lock" }).output( - z.object({ - account: z.string(), - gateway: z.string(), - network: z.enum(["mainnet", "testnet"]), - configRegistryUrl: z.string(), - lockRegistryUrl: z.string(), - active: z.boolean(), - value: z - .object({ - owner: z.string(), - pid: z.union([z.number(), z.string()]), - startedAt: z.number(), - expiresAt: z.number(), - network: z.enum(["mainnet", "testnet"]), - nonce: z.string(), - txHash: z.string().optional(), - }) - .nullable(), - }), - ), - deployLockRelease: oc.route({ method: "POST", path: "/deploy/lock/release" }).output( - z.object({ - released: z.boolean(), - txHash: z.string().optional(), - }), - ), }); export type DevOptions = z.infer; diff --git a/packages/everything-dev/src/plugin.ts b/packages/everything-dev/src/plugin.ts index d347c8c7..95a3363f 100644 --- a/packages/everything-dev/src/plugin.ts +++ b/packages/everything-dev/src/plugin.ts @@ -15,7 +15,6 @@ import { readJsonFile, selectWorkspaceTargets, } from "./build"; -import { inspectDeployLock, releaseDeployLock } from "./cli/deploy-lock"; import { buildCiInfraPlan, type CiInfraPlan, @@ -1042,8 +1041,6 @@ export default createPlugin({ packages: input.packages, network: input.network, privateKey: input.privateKey, - skipDeployLock: input.noDeployLock, - deployLockTtlMs: resolveDeployLockTtlFromEnv(), }); if (result.publishConfig) { @@ -1062,7 +1059,6 @@ export default createPlugin({ built: result.built, skipped: result.skipped, deployResults: result.deployResults, - lockConflict: result.lockConflict, }; }), @@ -1087,8 +1083,6 @@ export default createPlugin({ packages: input.packages, network: input.network, privateKey: input.privateKey, - skipDeployLock: input.noDeployLock, - deployLockTtlMs: resolveDeployLockTtlFromEnv({ extended: true }), }); if (result.status === "error") { @@ -1101,7 +1095,6 @@ export default createPlugin({ redeployed: false, error: result.error, deployResults: result.deployResults, - lockConflict: result.lockConflict, }; } @@ -1115,20 +1108,6 @@ export default createPlugin({ }; } - if (result.status === "locked") { - return { - status: "locked" as const, - registryUrl: result.registryUrl, - txHash: result.txHash, - built: result.built, - skipped: result.skipped, - redeployed: false, - error: result.error, - deployResults: result.deployResults, - lockConflict: result.lockConflict, - }; - } - if (result.publishConfig) { const refreshed = await loadResolvedConfig({ cwd: deps.configDir }); if (refreshed?.config) { @@ -2172,55 +2151,9 @@ export default createPlugin({ }; return result; }), - - deployLockInspect: builder.deployLockInspect.handler(async () => { - if (!deps.bosConfig) { - throw new Error("No bos.config.json found"); - } - const account = deps.bosConfig.account; - const gateway = deps.bosConfig.staging?.domain ?? deps.bosConfig.domain ?? account; - if (!gateway) { - throw new Error("bos.config.json must define domain to inspect deploy lock"); - } - const network = getNetworkIdForAccount(account); - return inspectDeployLock({ account, gateway, network }); - }), - - deployLockRelease: builder.deployLockRelease.handler(async () => { - if (!deps.bosConfig) { - throw new Error("No bos.config.json found"); - } - const account = deps.bosConfig.account; - const gateway = deps.bosConfig.staging?.domain ?? deps.bosConfig.domain ?? account; - if (!gateway) { - throw new Error("bos.config.json must define domain to release deploy lock"); - } - const network = getNetworkIdForAccount(account); - const privateKey = process.env.NEAR_PRIVATE_KEY || process.env.BOS_NEAR_PRIVATE_KEY; - const result = await releaseDeployLock( - { account, gateway, network }, - { privateKey, force: true }, - ); - return { released: result.released, txHash: result.txHash }; - }), }), }); -const PUBLISH_LOCK_TTL_MS = 10 * 60 * 1000; -const DEPLOY_LOCK_TTL_MS = 25 * 60 * 1000; - -export function resolveDeployLockTtlFromEnv(opts: { extended?: boolean } = {}): number { - const raw = process.env.BOS_DEPLOY_LOCK_TTL_MS; - if (raw !== undefined && raw !== "") { - const parsed = Number(raw); - if (Number.isFinite(parsed) && parsed > 0) return parsed; - } - return opts.extended ? DEPLOY_LOCK_TTL_MS : PUBLISH_LOCK_TTL_MS; -} - -export const PUBLISH_LOCK_TTL_DEFAULT_MS = PUBLISH_LOCK_TTL_MS; -export const DEPLOY_LOCK_TTL_DEFAULT_MS = DEPLOY_LOCK_TTL_MS; - function computeAllowedWorkspaces(overrides: string[], plugins?: string[]): string[] { const workspaces: string[] = []; for (const section of overrides) { diff --git a/packages/everything-dev/src/publish.ts b/packages/everything-dev/src/publish.ts index 545767fa..a455eb66 100644 --- a/packages/everything-dev/src/publish.ts +++ b/packages/everything-dev/src/publish.ts @@ -3,12 +3,6 @@ import { join } from "node:path"; import process from "node:process"; import { Effect } from "effect"; import { buildWorkspaceTargets, selectWorkspaceTargets } from "./build"; -import { - acquireDeployLock, - type DeployLockConflict, - fetchDeployLock, - releaseDeployLock, -} from "./cli/deploy-lock"; import { generateCodeArtifacts } from "./code-artifacts"; import { loadResolvedConfig } from "./config"; import type { WorkspaceDeployResult } from "./contract"; @@ -85,12 +79,10 @@ interface PublishToFastKvInput { packages: string; network?: "mainnet" | "testnet"; privateKey?: string; - skipDeployLock?: boolean; - deployLockTtlMs?: number; } interface PublishToFastKvResult { - status: "published" | "error" | "dry-run" | "locked"; + status: "published" | "error" | "dry-run"; registryUrl: string; txHash?: string; built?: string[]; @@ -98,7 +90,6 @@ interface PublishToFastKvResult { error?: string; publishConfig?: BosConfigInput; deployResults?: WorkspaceDeployResult[]; - lockConflict?: DeployLockConflict; } export async function publishToFastKv(input: PublishToFastKvInput): Promise { @@ -142,55 +133,7 @@ export async function publishToFastKv(input: PublishToFastKvInput): Promise { - const previous = process.env.BOS_DEPLOY_LOCK_TTL_MS; - - afterEach(() => { - if (previous === undefined) delete process.env.BOS_DEPLOY_LOCK_TTL_MS; - else process.env.BOS_DEPLOY_LOCK_TTL_MS = previous; - }); - - it("uses 10 min default for publish", () => { - delete process.env.BOS_DEPLOY_LOCK_TTL_MS; - expect(resolveDeployLockTtlFromEnv()).toBe(PUBLISH_LOCK_TTL_DEFAULT_MS); - expect(resolveDeployLockTtlFromEnv({ extended: false })).toBe(PUBLISH_LOCK_TTL_DEFAULT_MS); - }); - - it("uses 25 min default for deploy", () => { - delete process.env.BOS_DEPLOY_LOCK_TTL_MS; - expect(resolveDeployLockTtlFromEnv({ extended: true })).toBe(DEPLOY_LOCK_TTL_DEFAULT_MS); - }); - - it("honors BOS_DEPLOY_LOCK_TTL_MS override for both modes", () => { - process.env.BOS_DEPLOY_LOCK_TTL_MS = "1800000"; - expect(resolveDeployLockTtlFromEnv()).toBe(1_800_000); - expect(resolveDeployLockTtlFromEnv({ extended: true })).toBe(1_800_000); - }); - - it("falls back to default when env value is invalid", () => { - process.env.BOS_DEPLOY_LOCK_TTL_MS = "not-a-number"; - expect(resolveDeployLockTtlFromEnv()).toBe(PUBLISH_LOCK_TTL_DEFAULT_MS); - expect(resolveDeployLockTtlFromEnv({ extended: true })).toBe(DEPLOY_LOCK_TTL_DEFAULT_MS); - }); -}); - -describe("deploy-lock helpers", () => { - describe("parseDeployLockValue", () => { - it("returns null for empty/sentinel values", () => { - expect(parseDeployLockValue(null)).toBeNull(); - expect(parseDeployLockValue(undefined)).toBeNull(); - expect(parseDeployLockValue("{}")).toBeNull(); - }); - - it("parses stringified JSON values", () => { - const stored = JSON.stringify({ - owner: "alice", - pid: 4242, - startedAt: 1, - expiresAt: Date.now() + 60_000, - network: "mainnet", - nonce: "abc123", - }); - const result = parseDeployLockValue(stored); - expect(result).not.toBeNull(); - expect(result?.owner).toBe("alice"); - expect(result?.pid).toBe(4242); - expect(result?.nonce).toBe("abc123"); - expect(result?.network).toBe("mainnet"); - }); - - it("parses object values", () => { - const result = parseDeployLockValue({ - owner: "deployer", - pid: 99, - startedAt: 1000, - expiresAt: 2000, - network: "testnet", - nonce: "deadbeef", - }); - expect(result?.owner).toBe("deployer"); - expect(result?.network).toBe("testnet"); - }); - - it("returns null when nonce or expiresAt is missing", () => { - expect(parseDeployLockValue({ owner: "x" })).toBeNull(); - expect(parseDeployLockValue({ nonce: "abc" })).toBeNull(); - expect(parseDeployLockValue({ expiresAt: 1000 })).toBeNull(); - }); - }); - - describe("buildDeployLockKey", () => { - it("returns expected apps/.../lock/deploy.json path", () => { - expect(buildDeployLockKey("v1.foo.near", "citynode.app")).toBe( - "apps/v1.foo.near/citynode.app/lock/deploy.json", - ); - }); - }); - - describe("buildDeployLockUrl", () => { - it("builds the FastKV GET URL for mainnet", () => { - const url = buildDeployLockUrl("v1.foo.near", "citynode.app", "mainnet"); - expect(url).toContain("kv.main.fastnear.com"); - expect(url).toContain("v1.foo.near"); - expect(encodeURIComponent("apps/v1.foo.near/citynode.app/lock/deploy.json")).toBeTruthy(); - expect( - decodeURIComponent(encodeURIComponent("apps/v1.foo.near/citynode.app/lock/deploy.json")), - ).toBe("apps/v1.foo.near/citynode.app/lock/deploy.json"); - expect(url).toContain("deploy.json"); - }); - - it("builds the FastKV GET URL for testnet", () => { - const url = buildDeployLockUrl("v1.foo.near", "citynode.app", "testnet"); - expect(url).toContain("kv.test.fastnear.com"); - }); - }); -}); From bf29af5ad6b06ad17d480f39313c8295a80cfa11 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Tue, 18 Aug 2026 14:11:01 -0500 Subject: [PATCH 17/24] wip --- ...etter-near-auth-1-10-0-ephemeral-marker.md | 5 + .changeset/siwn-ephemeral-relayer.md | 8 + AGENTS.md | 15 + bos.config.json | 13 +- bun.lock | 604 ++++++++++-------- package.json | 2 +- 6 files changed, 365 insertions(+), 282 deletions(-) create mode 100644 .changeset/better-near-auth-1-10-0-ephemeral-marker.md create mode 100644 .changeset/siwn-ephemeral-relayer.md diff --git a/.changeset/better-near-auth-1-10-0-ephemeral-marker.md b/.changeset/better-near-auth-1-10-0-ephemeral-marker.md new file mode 100644 index 00000000..cb31e969 --- /dev/null +++ b/.changeset/better-near-auth-1-10-0-ephemeral-marker.md @@ -0,0 +1,5 @@ +--- +"everything-dev": patch +--- + +Bump `better-near-auth` from 1.9.0 to 1.10.0 across the catalog and pin it exactly. The new release adds an optional `ephemeral?: true` marker on `RelayerEphemeralConfig`; mark the SIWN relayer block in `bos.config.json → app.auth.variables.siwn` with that explicit flag while keeping the existing `whitelistedContracts`, `maxGasPerTransaction`, and `maxDepositPerTransaction` constraints. Runtime behavior is unchanged — the marker is documentation only and `getRelayerInfo().mode` still reports `"ephemeral"`. diff --git a/.changeset/siwn-ephemeral-relayer.md b/.changeset/siwn-ephemeral-relayer.md new file mode 100644 index 00000000..afd14429 --- /dev/null +++ b/.changeset/siwn-ephemeral-relayer.md @@ -0,0 +1,8 @@ +--- +--- + +Confirm the SIWN auth relayer is in `RelayerEphemeralConfig` ("Ephemeral with settings") mode: a rich-object `relayer` block in `bos.config.json → app.auth.variables.siwn` with `whitelistedContracts`, `maxGasPerTransaction`, and `maxDepositPerTransaction` and no `accountId` / `privateKey`. better-near-auth 1.9.0's `initRelayer` resolves this to an auto-generated ED25519 keypair on first startup, encrypted with `BETTER_AUTH_SECRET` (HKDF-SHA256 → AES-256-GCM) and persisted in the `relayerKey` table. + +The vestigial `NEAR_RELAYER_PRIVATE_KEY` line is removed from `.env.example` in favor of an inline comment pointing operators at `/admin/relayer` (which surfaces a "needs funding" prompt using `getRelayerInfo().enabled === false` once the auto-generated implicit account has zero balance). Operators funding the implicit account via `authClient.near.getNearClient().transfer()` enables relay without ever leaving the existing ephemeral-mode config. + +AGENTS.md gains a "SIWN Auth Relayer" subsection under "Common Patterns" documenting the operational rules (funding flow, parent-key requirement for sub-account creation, why the implicit relayer account can't own sub-accounts, and the path back to `RelayerExplicitConfig` if a named-account relayer is needed). diff --git a/AGENTS.md b/AGENTS.md index ddca75f8..8a174c20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -351,6 +351,21 @@ const { runtimeConfig } = Route.useLoaderData(); const appName = getActiveRuntime(runtimeConfig)?.title ?? getAccount(runtimeConfig); ``` +### SIWN Auth Relayer (gasless NEP-366 relay) + +The auth plugin's `siwn({ relayer: ... })` block in `bos.config.json → app.auth.variables.siwn` is **ephemeral mode** — the rich-object shape with `whitelistedContracts`, `maxGasPerTransaction`, and `maxDepositPerTransaction` but no `accountId` / `privateKey`. From the better-near-auth skill: that's `RelayerEphemeralConfig` ("Ephemeral with settings"). + +**Operational rules:** + +- On first startup the server generates an ED25519 keypair per network, derives an implicit hex account from the public key, and encrypts the private key with `BETTER_AUTH_SECRET` (HKDF-SHA256 → AES-256-GCM) into the `relayerKey` table. Same keypair recovers on every restart. +- After first startup the server logs the implicit account id. **Fund that account with NEAR** to enable relay — otherwise every relay attempt fails with insufficient balance from the RPC. +- Funding workflow: admins hit `getRelayerInfo`; the `/admin/relayer` page surfaces a "needs funding" banner on `/admin` when `enabled === false` and `accountId` is set, then the admin's connected wallet transfers NEAR to the implicit account via `authClient.near.getNearClient().transfer()`. +- The implicit relayer account is *not* a `.near` named account, so it cannot own sub-accounts. `siwn.subAccount.parentAccount` must be a named account (this project uses `v1.citynode.near` / `v1.citynode.testnet`), and the parent key is supplied via `NEAR_SUB_ACCOUNT_PARENT_KEY_MAINNET` / `NEAR_SUB_ACCOUNT_PARENT_KEY_TESTNET` secrets. Without the parent key the sub-account endpoint explains why in the error message and returns a `not-configured` reason. +- `NEAR_RELAYER_PRIVATE_KEY` is vestigial in ephemeral mode and is omitted from `.env.example`. Only reintroduce (plus explicit `relayer: { accountId, privateKey }`) when moving to `RelayerExplicitConfig`. +- The mode is observable at runtime: `getRelayerInfo()` returns `{ accountId, mode: "ephemeral", publicKey, balance, enabled }`. + +To switch to `RelayerExplicitConfig`, replace the rich-object shape with `relayer: { accountId: "relayer..near", privateKey: process.env.RELAYER_PRIVATE_KEY, whitelistedContracts: [...], maxGasPerTransaction: "...", maxDepositPerTransaction: "0" }` and re-add the env var. The ephemeral key in the `relayerKey` table is ignored once an explicit key is provided. + ## Security ### Shared Singleton Trust Model diff --git a/bos.config.json b/bos.config.json index 07a09d83..92f467af 100644 --- a/bos.config.json +++ b/bos.config.json @@ -12,7 +12,7 @@ "app": { "host": { "development": "local:host", - "production": "https://elliot-braem-7380-host-everything-dev-nearbuilder-58d75d144-ze.zephyrcloud.app", + "production": "https://elliot-braem-7404-host-everything-dev-nearbuilder-171db3e27-ze.zephyrcloud.app", "secrets": [ "CORS_ORIGIN", "CSP_STRICT" @@ -21,14 +21,14 @@ }, "ui": { "development": "local:ui", - "production": "https://elliot-braem-7376-ui-everything-dev-nearbuilders-640fbb40f-ze.zephyrcloud.app", - "ssr": "https://elliot-braem-7379-ui-everything-dev-nearbuilders-9e2e0e650-ze.zephyrcloud.app", + "production": "https://elliot-braem-7400-ui-everything-dev-nearbuilders-1b6dbfe53-ze.zephyrcloud.app", + "ssr": "https://elliot-braem-7398-ui-everything-dev-nearbuilders-e8019531b-ze.zephyrcloud.app", "integrity": "sha384-UQX/yKubluHrdSEKhNtBs39QAbtXHs/iJ0aqkyPIW/ey/k26xRVvCEKsL2iK+cmb", - "ssrIntegrity": "sha384-V8ObgdJVdJ/jwU5xZbwX0R0+APdf2pli4B+ApUAfkB9vtzpxGVrFNDC3qS0FsL+i" + "ssrIntegrity": "sha384-99Rxrhwagd4U98ryETO5m987SDaqG2DBzRA76gh5bJHqP1x1rTJlapRhC6zASS6j" }, "api": { "development": "local:api", - "production": "https://elliot-braem-7377-api-everything-dev-nearbuilders-d0f1039d8-ze.zephyrcloud.app", + "production": "https://elliot-braem-7402-api-everything-dev-nearbuilders-79d1a502b-ze.zephyrcloud.app", "secrets": [ "API_DATABASE_URL" ], @@ -50,6 +50,7 @@ "testnet": "v1.citynode.testnet" }, "relayer": { + "ephemeral": true, "whitelistedContracts": [ "v1.citynode.near" ], @@ -75,7 +76,7 @@ "plugins": { "apps": { "development": "local:plugins/apps", - "production": "https://elliot-braem-7378-everything-dev-apps-plugin-ever-bd36fe158-ze.zephyrcloud.app", + "production": "https://elliot-braem-7401-everything-dev-apps-plugin-ever-e71038daa-ze.zephyrcloud.app", "variables": { "registryNamespace": "v1.citynode.near" }, diff --git a/bun.lock b/bun.lock index 58d69aae..46d876c9 100644 --- a/bun.lock +++ b/bun.lock @@ -391,7 +391,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "better-auth": "1.6.25", - "better-near-auth": "1.8.3", + "better-near-auth": "1.10.0", "drizzle-kit": "^0.31.8", "drizzle-orm": "^0.45.1", "effect": "3.21.2", @@ -420,7 +420,7 @@ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], @@ -440,7 +440,7 @@ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], @@ -450,9 +450,9 @@ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@better-auth/api-key": ["@better-auth/api-key@1.6.25", "", { "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.25", "@better-auth/utils": "0.4.2", "better-auth": "^1.6.25", "better-call": "1.3.7" } }, "sha512-A6f3YLN8Ve+D4R7f8jrSKh8Mpu9+bmc8PIb9BFgGgspcedM1PjpSEI5JKAWPUhiGgMeJdCGeCpRP+27jQmW9/w=="], @@ -558,67 +558,67 @@ "@electric-sql/pglite": ["@electric-sql/pglite@0.4.6", "", {}, "sha512-qmlmfN8UyKCee35qkV0r/MBp+Znl8FjBz7OpoglNvww3GJpw0/DLP0o1ZymvLNmcD5DTLOQdzKPtF8Hd3mdl1w=="], - "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + "@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], "@every-plugin/template": ["@every-plugin/template@workspace:plugins/_template"], @@ -634,7 +634,7 @@ "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], - "@hono/node-server": ["@hono/node-server@2.0.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg=="], + "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -708,21 +708,21 @@ "@jsonjoy.com/codegen": ["@jsonjoy.com/codegen@1.0.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g=="], - "@jsonjoy.com/fs-core": ["@jsonjoy.com/fs-core@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew=="], + "@jsonjoy.com/fs-core": ["@jsonjoy.com/fs-core@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.68.1", "@jsonjoy.com/fs-node-utils": "4.68.1", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw=="], - "@jsonjoy.com/fs-fsa": ["@jsonjoy.com/fs-fsa@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w=="], + "@jsonjoy.com/fs-fsa": ["@jsonjoy.com/fs-fsa@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.68.1", "@jsonjoy.com/fs-node-builtins": "4.68.1", "@jsonjoy.com/fs-node-utils": "4.68.1", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg=="], - "@jsonjoy.com/fs-node": ["@jsonjoy.com/fs-node@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/fs-print": "4.64.0", "@jsonjoy.com/fs-snapshot": "4.64.0", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA=="], + "@jsonjoy.com/fs-node": ["@jsonjoy.com/fs-node@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.68.1", "@jsonjoy.com/fs-node-builtins": "4.68.1", "@jsonjoy.com/fs-node-utils": "4.68.1", "@jsonjoy.com/fs-print": "4.68.1", "@jsonjoy.com/fs-snapshot": "4.68.1", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ=="], - "@jsonjoy.com/fs-node-builtins": ["@jsonjoy.com/fs-node-builtins@4.64.0", "", { "peerDependencies": { "tslib": "2" } }, "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA=="], + "@jsonjoy.com/fs-node-builtins": ["@jsonjoy.com/fs-node-builtins@4.68.1", "", { "peerDependencies": { "tslib": "2" } }, "sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw=="], - "@jsonjoy.com/fs-node-to-fsa": ["@jsonjoy.com/fs-node-to-fsa@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-fsa": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw=="], + "@jsonjoy.com/fs-node-to-fsa": ["@jsonjoy.com/fs-node-to-fsa@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-fsa": "4.68.1", "@jsonjoy.com/fs-node-builtins": "4.68.1", "@jsonjoy.com/fs-node-utils": "4.68.1" }, "peerDependencies": { "tslib": "2" } }, "sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g=="], - "@jsonjoy.com/fs-node-utils": ["@jsonjoy.com/fs-node-utils@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.64.0", "glob-to-regex.js": "^1.0.1" }, "peerDependencies": { "tslib": "2" } }, "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w=="], + "@jsonjoy.com/fs-node-utils": ["@jsonjoy.com/fs-node-utils@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.68.1", "glob-to-regex.js": "^1.0.1" }, "peerDependencies": { "tslib": "2" } }, "sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw=="], - "@jsonjoy.com/fs-print": ["@jsonjoy.com/fs-print@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-node-utils": "4.64.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw=="], + "@jsonjoy.com/fs-print": ["@jsonjoy.com/fs-print@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-node-utils": "4.68.1", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow=="], - "@jsonjoy.com/fs-snapshot": ["@jsonjoy.com/fs-snapshot@4.64.0", "", { "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q=="], + "@jsonjoy.com/fs-snapshot": ["@jsonjoy.com/fs-snapshot@4.68.1", "", { "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/fs-node-utils": "4.68.1", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ=="], "@jsonjoy.com/json-pack": ["@jsonjoy.com/json-pack@1.21.0", "", { "dependencies": { "@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg=="], @@ -770,39 +770,39 @@ "@module-federation/automatic-vendor-federation": ["@module-federation/automatic-vendor-federation@1.2.1", "", { "dependencies": { "find-package-json": "^1.2.0" }, "peerDependencies": { "webpack": "^5.0.0-beta.16" } }, "sha512-73wxkXM7pbRZ6GGM90JP5IPTPvY3fvrhQyTVdMCUx85cQRWqnbzbibcsz3pkOMOeXyYAO4tXXsG13yNaEEGhJA=="], - "@module-federation/bridge-react-webpack-plugin": ["@module-federation/bridge-react-webpack-plugin@2.8.0", "", { "dependencies": { "@module-federation/sdk": "2.8.0" } }, "sha512-7AaaiE4YOXFb+st6xlVDK65aNKZYR9S9ykH0q2mkHAHTgquXciAjypS8JuhmKn7Lob/2rkplMOc4YsGxG3Ng9Q=="], + "@module-federation/bridge-react-webpack-plugin": ["@module-federation/bridge-react-webpack-plugin@2.8.2", "", { "dependencies": { "@module-federation/sdk": "2.8.2" } }, "sha512-cEhnpCsWHqUndQC6WKtwat5BGz+IU0UdCjzyXrZtx1UqHX1jRB0+DZxB4DKYKtxTko8fsV0FUCJ/0FjKZY6z8g=="], - "@module-federation/cli": ["@module-federation/cli@2.8.0", "", { "dependencies": { "@module-federation/dts-plugin": "2.8.0", "@module-federation/sdk": "2.8.0", "commander": "11.1.0", "jiti": "2.4.2" }, "bin": { "mf": "bin/mf.js" } }, "sha512-yTxdWkCJPPo+IGASz+NqdW13cw3DhjSBEn9r85aYn1ahckDwB1WcZe0OjDWMqCcR6yi0BMLedk0SWrUeUj0fWw=="], + "@module-federation/cli": ["@module-federation/cli@2.8.2", "", { "dependencies": { "@module-federation/dts-plugin": "2.8.2", "@module-federation/sdk": "2.8.2", "commander": "11.1.0", "jiti": "2.4.2" }, "bin": { "mf": "bin/mf.js" } }, "sha512-SrRe2UOzjYux/9Zf7AYymGGsYpJgrIHUPF+T9JQ+rZ7MC9Uiy5rNUtSYdxNrdFpVaRZYXMrI4b82iUy66CU6UA=="], - "@module-federation/dts-plugin": ["@module-federation/dts-plugin@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/managers": "2.8.0", "@module-federation/sdk": "2.8.0", "@module-federation/third-party-dts-extractor": "2.8.0", "adm-zip": "0.5.10", "isomorphic-ws": "5.0.0", "undici": "7.28.0", "ws": "8.21.0" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24" }, "optionalPeers": ["vue-tsc"] }, "sha512-defjq4jOWMEfeejezPWLP5sc8kw0O6FqTT7/E5rbZPEVyjB1A0U3ynhW6GDE5/6hk9/TzdbWS+fBNi4MqUOY6Q=="], + "@module-federation/dts-plugin": ["@module-federation/dts-plugin@2.8.2", "", { "dependencies": { "@module-federation/error-codes": "2.8.2", "@module-federation/managers": "2.8.2", "@module-federation/sdk": "2.8.2", "@module-federation/third-party-dts-extractor": "2.8.2", "adm-zip": "0.6.0", "isomorphic-ws": "5.0.0", "undici": "7.29.0", "ws": "8.21.0" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24" }, "optionalPeers": ["vue-tsc"] }, "sha512-pwZFW8b2LZTymMMC+o2M9xMXDIQKAHGtCRqj/IkOp0jHRYrKjK9cma9xoUNst8/R3sEn878/i9wgv/43BnCEyg=="], - "@module-federation/enhanced": ["@module-federation/enhanced@2.8.0", "", { "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.8.0", "@module-federation/cli": "2.8.0", "@module-federation/dts-plugin": "2.8.0", "@module-federation/error-codes": "2.8.0", "@module-federation/inject-external-runtime-core-plugin": "2.8.0", "@module-federation/managers": "2.8.0", "@module-federation/manifest": "2.8.0", "@module-federation/rspack": "2.8.0", "@module-federation/runtime-tools": "2.8.0", "@module-federation/sdk": "2.8.0", "@module-federation/webpack-bundler-runtime": "2.8.0", "schema-utils": "4.3.0", "tapable": "2.3.0" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24", "webpack": "^5.0.0" }, "optionalPeers": ["typescript", "vue-tsc", "webpack"], "bin": { "mf": "bin/mf.js" } }, "sha512-h8vkLdhK7tlcSPmyYNGfGyt0pSzfDB0tYVYdyUt2tXwQRfaJAi3bsIpujMXElw3MXtOfSHESa6M/hPKrtWTHBw=="], + "@module-federation/enhanced": ["@module-federation/enhanced@2.8.2", "", { "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.8.2", "@module-federation/cli": "2.8.2", "@module-federation/dts-plugin": "2.8.2", "@module-federation/error-codes": "2.8.2", "@module-federation/inject-external-runtime-core-plugin": "2.8.2", "@module-federation/managers": "2.8.2", "@module-federation/manifest": "2.8.2", "@module-federation/rspack": "2.8.2", "@module-federation/runtime-tools": "2.8.2", "@module-federation/sdk": "2.8.2", "@module-federation/webpack-bundler-runtime": "2.8.2", "schema-utils": "4.3.0", "tapable": "2.3.0" }, "peerDependencies": { "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24", "webpack": "^5.0.0" }, "optionalPeers": ["typescript", "vue-tsc", "webpack"], "bin": { "mf": "bin/mf.js" } }, "sha512-XVCp1dz7ADd2YgjuvGVsS7IVJ8ViuAUCEz9gAb4M0qMJCjcPfzXQ1CzV1/Me1lKaPPwNj/cJ3NXTRvXjMHanGg=="], - "@module-federation/error-codes": ["@module-federation/error-codes@2.8.0", "", {}, "sha512-Gaog9904EmxYOQV0hli3XQ7jXeFaADfh5bnBtTCtbZ37Qd/Sz9kQfd+gYQRyIj7RGmkv9DPiN/SsmrTMrTymKw=="], + "@module-federation/error-codes": ["@module-federation/error-codes@2.8.2", "", {}, "sha512-8inlDv48QOjA//CLQ3epjoHEiMQGsz1Pmtu2N+s7gQVggn6AYHpjnMe8AsyGxtpaPg3wbX0HmBZtRFggpXUB9A=="], - "@module-federation/inject-external-runtime-core-plugin": ["@module-federation/inject-external-runtime-core-plugin@2.8.0", "", { "peerDependencies": { "@module-federation/runtime-tools": "2.8.0" } }, "sha512-fW3jD1ZVds6r/Ul8TtUA42RsB0LfT1yjo5KjqgirH9QrmEMH21x44e3e6BC6IN820XKawRDSmz2kFA5YHIQp7Q=="], + "@module-federation/inject-external-runtime-core-plugin": ["@module-federation/inject-external-runtime-core-plugin@2.8.2", "", { "peerDependencies": { "@module-federation/runtime-tools": "2.8.2" } }, "sha512-RetbaupJGiT2FtmA3WWGm72xZkNSJ6Xyy53jSgtu9HRnR/5xt/wiQl+Pe/iEfzgfLsUnUAkOsSfiPBXrDtNizw=="], - "@module-federation/managers": ["@module-federation/managers@2.8.0", "", { "dependencies": { "@module-federation/sdk": "2.8.0" } }, "sha512-SnVBCwmi962WGg6hLFElxZUCnrRJdR6glE2ZKPBY/iK07AHUN2ZxuaBCBsVzyws+xLZGHZxBHmVstijTh8dSUA=="], + "@module-federation/managers": ["@module-federation/managers@2.8.2", "", { "dependencies": { "@module-federation/sdk": "2.8.2" } }, "sha512-OQfnoUwy1IUfn6DaI2S/DPKgnElkYlP+gG8mbUQlJVEW1Zg/8V/dZbNgJUKikTJly6yR1r08PK59g7239ITYMw=="], - "@module-federation/manifest": ["@module-federation/manifest@2.8.0", "", { "dependencies": { "@module-federation/dts-plugin": "2.8.0", "@module-federation/managers": "2.8.0", "@module-federation/sdk": "2.8.0" } }, "sha512-wfVeBXc4/C2F70nRFSPqJhkcwbDgo+wQyEn3jbjJTDoUqxxhYBfHFs1ACBYOk3Qm97L7hHclHGtUG0/nvDEfAA=="], + "@module-federation/manifest": ["@module-federation/manifest@2.8.2", "", { "dependencies": { "@module-federation/dts-plugin": "2.8.2", "@module-federation/managers": "2.8.2", "@module-federation/sdk": "2.8.2" } }, "sha512-mJUZo7QFL46NXoEWNby3CfPFFm2235J7LO6bXAAMrEIT4qF8QHLiqfoo5dZmrisRgM6e+muriWtGvPFMprnXyA=="], - "@module-federation/node": ["@module-federation/node@2.7.47", "", { "dependencies": { "@module-federation/enhanced": "2.8.0", "@module-federation/runtime": "2.8.0", "@module-federation/sdk": "2.8.0", "encoding": "0.1.13", "node-fetch": "2.7.0", "tapable": "2.3.0" }, "peerDependencies": { "webpack": "^5.40.0" }, "optionalPeers": ["webpack"] }, "sha512-mifMvCjWmLl53GS+badQws0j2bsu1ICpdGzCbez4I6kSpaYA8v86L6dwcHtVHIZtkUC6cjAZBDcgpxs4fK3nFQ=="], + "@module-federation/node": ["@module-federation/node@2.7.49", "", { "dependencies": { "@module-federation/enhanced": "2.8.2", "@module-federation/runtime": "2.8.2", "@module-federation/sdk": "2.8.2", "encoding": "0.1.13", "node-fetch": "2.7.0", "tapable": "2.3.0" }, "peerDependencies": { "webpack": "^5.40.0" }, "optionalPeers": ["webpack"] }, "sha512-xNGYfhA2aqFpogb/uq6lwBeEbnmDLV6PwHzSe97mRrSSr00eUKAMwlLG6PcQP6ynbkPeDG86RYj/YUC7EWgLMA=="], - "@module-federation/rsbuild-plugin": ["@module-federation/rsbuild-plugin@2.8.0", "", { "dependencies": { "@module-federation/enhanced": "2.8.0", "@module-federation/node": "2.7.47", "@module-federation/sdk": "2.8.0" }, "peerDependencies": { "@rsbuild/core": "^1.3.21 || ^2.0.0-0" }, "optionalPeers": ["@rsbuild/core"] }, "sha512-rul5OPvLx599rWoAhCtKJ3UYqyM3Dxg0RWEfad3JdnJFqkcSLBojZXgHJE1vbF7DRdGQNmNscKw34iEyn89NwQ=="], + "@module-federation/rsbuild-plugin": ["@module-federation/rsbuild-plugin@2.8.2", "", { "dependencies": { "@module-federation/enhanced": "2.8.2", "@module-federation/node": "2.7.49", "@module-federation/sdk": "2.8.2" }, "peerDependencies": { "@rsbuild/core": "^1.3.21 || ^2.0.0-0" }, "optionalPeers": ["@rsbuild/core"] }, "sha512-rUzx5quE/pqEiZk0ESyPj4QCDCvSpqhgoq/+zjuz9vyHAuMvXL/U0si6bOclBg4UGGFlLmq5+XVIJR43iAADJg=="], - "@module-federation/rspack": ["@module-federation/rspack@2.8.0", "", { "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.8.0", "@module-federation/dts-plugin": "2.8.0", "@module-federation/inject-external-runtime-core-plugin": "2.8.0", "@module-federation/managers": "2.8.0", "@module-federation/manifest": "2.8.0", "@module-federation/runtime-tools": "2.8.0", "@module-federation/sdk": "2.8.0" }, "peerDependencies": { "@rspack/core": "^0.7.0 || ^1.0.0 || ^2.0.0-0", "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24" }, "optionalPeers": ["typescript", "vue-tsc"] }, "sha512-TPcrkHpaZgL25Vx3c8oSNwyv7/KktC7uo6HTQdVWlFzbq5RSoMMGkWoir5pY5124isae2/p6v5xAuqICi4r0Zg=="], + "@module-federation/rspack": ["@module-federation/rspack@2.8.2", "", { "dependencies": { "@module-federation/bridge-react-webpack-plugin": "2.8.2", "@module-federation/dts-plugin": "2.8.2", "@module-federation/inject-external-runtime-core-plugin": "2.8.2", "@module-federation/managers": "2.8.2", "@module-federation/manifest": "2.8.2", "@module-federation/runtime-tools": "2.8.2", "@module-federation/sdk": "2.8.2" }, "peerDependencies": { "@rspack/core": "^0.7.0 || ^1.0.0 || ^2.0.0-0", "typescript": "^4.9.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vue-tsc": ">=1.0.24" }, "optionalPeers": ["typescript", "vue-tsc"] }, "sha512-HEDirYhVYvx7IzP9jes6KLPMqSoSQwuLfBzPSOgBqY7sIH/e9zRSRO1qh5C8OXYKHi23WcQpGY+EeecIK9wxWw=="], - "@module-federation/runtime": ["@module-federation/runtime@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/runtime-core": "2.8.0", "@module-federation/sdk": "2.8.0" } }, "sha512-cGtUBQ1/TVy7KrXy6xPgy3FEmOGyIYkBA2T4iGH3ZH5PNPPTmqN9jF2AfneTSOj0RtBr7Pxq3CUt81E/UCvK1A=="], + "@module-federation/runtime": ["@module-federation/runtime@2.8.2", "", { "dependencies": { "@module-federation/error-codes": "2.8.2", "@module-federation/runtime-core": "2.8.2", "@module-federation/sdk": "2.8.2" } }, "sha512-SUoP+PD5EjSPSi6FxEPGIZoRkFifxdeYcVQbJE9mO0VEjF51gAk3/TgX8k0vzUryOBPmXekLr9SfQXU6DqUtvA=="], - "@module-federation/runtime-core": ["@module-federation/runtime-core@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/sdk": "2.8.0" } }, "sha512-Tf98+epGGiPSHqmQHuXa2uXZMMvjGf1IqJDR1/FpXfmobv5ECN0mGZCjUHGNSyxvoDyXKIkKwJu7IwEoh0ouQA=="], + "@module-federation/runtime-core": ["@module-federation/runtime-core@2.8.2", "", { "dependencies": { "@module-federation/error-codes": "2.8.2", "@module-federation/sdk": "2.8.2" } }, "sha512-PEkkK9MUp+nUCeQMS4ox3QGZfwwxgfjGA7P4umEnr5c3y8DLNDR+26tyHf/Gkjen2VsjlcyL+mEAQo1Zi4IE3g=="], "@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="], - "@module-federation/sdk": ["@module-federation/sdk@2.8.0", "", {}, "sha512-yBP+9+0Z8nlvKEXAZS3AsQVy7bFbZf8eMivGk4q4ZdwG3TsLMlsPjb1dQb2i7gcAG6ux9y2LWLkj/0LVk74cnQ=="], + "@module-federation/sdk": ["@module-federation/sdk@2.8.2", "", {}, "sha512-OPS/lbQjraLXoWniQpCwQ/vqgURHTrhsackSNcOPmcJHM3LyR+DabxUc0pl8jAqExsW2l+uepQq7+/Gkei871w=="], - "@module-federation/third-party-dts-extractor": ["@module-federation/third-party-dts-extractor@2.8.0", "", {}, "sha512-nAMlr74OKIylkfRwlunOhytQbmsgb3gCqdXWnPQhG+ZtqWXGELLfMT4a1Q1ht3cS+sRpWj2SZRqK2M7GadI6tA=="], + "@module-federation/third-party-dts-extractor": ["@module-federation/third-party-dts-extractor@2.8.2", "", {}, "sha512-Xf3iZ4iDi972XMOMbmUm/c5Vwwb6cTsU+Jpoz96lUom6Yps9FQX48elIdhgcQsL7/K64PqdOONsrlZVo8zphKA=="], - "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@2.8.0", "", { "dependencies": { "@module-federation/error-codes": "2.8.0", "@module-federation/runtime": "2.8.0", "@module-federation/sdk": "2.8.0" } }, "sha512-82fDy9v+7qV5fiN8TKVhOdrxhmAZnUIX/IKivYX5ulCt8aoOzVFTiwm/P1GQUDD8z6dqR48xgJdZdf0548Mc9w=="], + "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@2.8.2", "", { "dependencies": { "@module-federation/error-codes": "2.8.2", "@module-federation/runtime": "2.8.2", "@module-federation/sdk": "2.8.2" } }, "sha512-g4xQgfgMMCKgJjVMBh7nIYGjLGNDYwSZ4lfpTdkVyWDnxmR3SL6VPQTJEZHRYPyqvrTtZE1zdDhDd++yNRvLdA=="], "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], @@ -844,13 +844,17 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + "@neodrag/core": ["@neodrag/core@3.0.0-next.11", "", {}, "sha512-3WQWxyrbxiaK9zS5JU2wJsW2gpoQlZBXVghduBh61JpqaeE0T0cte8R0qYK2RuJo3J2TYQYqxO19CpG/C1i5eg=="], + + "@neodrag/solid": ["@neodrag/solid@3.0.0-next.11", "", { "peerDependencies": { "@neodrag/core": "3.0.0-next.11", "solid-js": "^1.0.0" } }, "sha512-vCBIn/pimjWMQ6vhTS2/O1XNAwzVtc4eUhdbQ91WykbZWWqQ5NocDXt/1OdYrEkeRzJcpCv8wEz5PnMkgKP81Q=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], - "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], + "@noble/ciphers": ["@noble/ciphers@2.3.0", "", {}, "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw=="], - "@noble/curves": ["@noble/curves@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0" } }, "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ=="], + "@noble/curves": ["@noble/curves@2.3.0", "", { "dependencies": { "@noble/hashes": "2.3.0" } }, "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg=="], - "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], + "@noble/hashes": ["@noble/hashes@2.3.0", "", {}, "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -862,7 +866,7 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], - "@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + "@orpc/client": ["@orpc/client@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-fetch": "1.15.0", "@orpc/standard-server-peer": "1.15.0" } }, "sha512-Qt0FdPSGySdQwy83iUWOq+Iqhw2gM9qHtyxfBbcw1mwOz1s4O2BwUFA3ymVTLIRRNYRgPrQLaBZhp8CacqeePg=="], "@orpc/contract": ["@orpc/contract@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/shared": "1.14.3", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-docXs4ALK3TADAnscEywjqvV1Dy+4+B6ihfo33hayvJdxZdpVmxjHOf7pcAYaJFJ6+LgKYoskaVVKad6LLxFlg=="], @@ -872,9 +876,9 @@ "@orpc/json-schema": ["@orpc/json-schema@1.14.3", "", { "dependencies": { "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "json-schema-typed": "^8.0.2" } }, "sha512-Qcz2PzyZG2etpfB8ywy4Upf4SaI2x6x4fA8utVQXf5GcTPWfbTVz78MDDWnNtYEXSPj1BS95HqHC0AeGvYIB+g=="], - "@orpc/openapi": ["@orpc/openapi@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi-client": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-0tZN91VoT6MEkOfw+ERKyozsnDXzmDSsBeMgEHN3Hl1WVU97T9l4aZzLlZILSNl3fat3HmduEDy/boNGmAWJkQ=="], + "@orpc/openapi": ["@orpc/openapi@1.15.0", "", { "dependencies": { "@orpc/client": "1.15.0", "@orpc/contract": "1.15.0", "@orpc/interop": "1.15.0", "@orpc/openapi-client": "1.15.0", "@orpc/server": "1.15.0", "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-nwdsVkIRTVhW+2kjtEwHQqfkYnmbJN6HTHvBfnt0KYrPftJ7qvwDo4WnoxfS7Q1jdUlVu8griT5cKZ1gAIYcpQ=="], - "@orpc/openapi-client": ["@orpc/openapi-client@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-1vp+hi858XDrCwYdhONl15YKNlHtj5F5gI3dq/McRRZ45tZ9/Ma03hxzABOOcayaT1L9nX7gPxhX7l5EuRf2zw=="], + "@orpc/openapi-client": ["@orpc/openapi-client@1.15.0", "", { "dependencies": { "@orpc/client": "1.15.0", "@orpc/contract": "1.15.0", "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-MGyMAC6dvQ/YoUARpTN4L1MAnK4L2T3+Z7LA8PVva2rZ8XpKABZRcfqufyPfAC0nu/0uPeebnxHaXZ/OeUIOFg=="], "@orpc/server": ["@orpc/server@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-aws-lambda": "1.14.3", "@orpc/standard-server-fastify": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-node": "1.14.3", "@orpc/standard-server-peer": "1.14.3", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-VQG1sgruPhWdzT/ChltJ5Ju9v1A8F+s8EQ1MMSI33z0AthZ3IuuMZdqMIOo5YSuHROoFxzMJgCShOWYR9qXhQA=="], @@ -896,7 +900,7 @@ "@orpc/zod": ["@orpc/zod@1.14.3", "", { "dependencies": { "@orpc/json-schema": "1.14.3", "@orpc/openapi": "1.14.3", "@orpc/shared": "1.14.3", "escape-string-regexp": "^5.0.0", "wildcard-match": "^5.1.4" }, "peerDependencies": { "@orpc/contract": "1.14.3", "@orpc/server": "1.14.3", "zod": ">=3.25.0" } }, "sha512-+SIDmqfkTLCeeZVN6Cic4aWeiBqf2O9F4Vto9npqOEXT1szIpHKJtCmdZBsTOSD46LV5Tcg1emOde9eUeY2EBg=="], - "@oxc-project/types": ["@oxc-project/types@0.140.0", "", {}, "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ=="], + "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], "@parcel/watcher": ["@parcel/watcher@2.6.0", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.4" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.6.0", "@parcel/watcher-darwin-arm64": "2.6.0", "@parcel/watcher-darwin-x64": "2.6.0", "@parcel/watcher-freebsd-x64": "2.6.0", "@parcel/watcher-linux-arm-glibc": "2.6.0", "@parcel/watcher-linux-arm-musl": "2.6.0", "@parcel/watcher-linux-arm64-glibc": "2.6.0", "@parcel/watcher-linux-arm64-musl": "2.6.0", "@parcel/watcher-linux-x64-glibc": "2.6.0", "@parcel/watcher-linux-x64-musl": "2.6.0", "@parcel/watcher-win32-arm64": "2.6.0", "@parcel/watcher-win32-x64": "2.6.0" } }, "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w=="], @@ -924,27 +928,27 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A=="], - "@peculiar/asn1-android": ["@peculiar/asn1-android@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA=="], + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ErGsrDbWYhMu/H43L3qohwCwPrjdinn8GGm4Q52evA1u2juJxqkPXv+26vBC35hhMoUkzYIOTLrFzSbXanRU+A=="], - "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA=="], + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "@peculiar/asn1-x509-attr": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-VKQz6sJYgSxtGaK6UdnNBUx7hmSdg0K331qrEWh5qxpQsyZGWBjxbq05AJ2bTWWjd7d+nJIxFzwvsR5T7DWC/A=="], - "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg=="], + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-SbxRzHiWnRdiDuiiji/RLsJxu2au4hmSZSKK7GQOgNr2BVvheAlFQST9qqzRchUcZ6wvcyRuPXIfYXVzLoZ/5g=="], - "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ=="], + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-vNspHtTd9h6e8c2lMW+B/VHEUD+HRFV0fj/Gvz7SaJbwiecA8dxd96UTFPYI1PR8k7qwjjW9AFEyI+LuyVi4Kw=="], - "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-rsa": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg=="], + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.9.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.0", "@peculiar/asn1-pkcs8": "^2.9.0", "@peculiar/asn1-rsa": "^2.9.0", "@peculiar/asn1-schema": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-A6bX+gZr69U38Pg1mWvPrM1eRba6L6kLR8iVG+bJtKj3qSv2rSNmlXLtej7ZOkEWt6xL6PiJD73uFEdQI3BXCg=="], - "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA=="], + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-1JH4FliKQ3trkMD17X+bKGsph5TsiM+AiiqnVKr1wxA/GoUSDHiWG/zROzLAbDIA7eclBCjQsp8hrBEJk7LRUQ=="], - "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pfx": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ=="], + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.9.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.0", "@peculiar/asn1-pfx": "^2.9.0", "@peculiar/asn1-pkcs8": "^2.9.0", "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "@peculiar/asn1-x509-attr": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-igArY6bpCI6tOPm2EU9QPrXuKq2s1iraLCTym4UlooYdzcRDIPCRUN9TAEcqZ5rZhA+HiPdFKTbDP9vneN/tsg=="], - "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg=="], + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-vOD7Q4UmQWhlMYuWJawS2sD+/JcPKJNtyQuFZx09r+KFhm5/HxfGak63y/m56cpBjmb5k6PeRlQf1fvLQAieUA=="], - "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="], + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-AKvPMOM7LfK0uFe1m7o7+veOa8xQGPqsqOrKi3QKgCElzwjGp39mbhr2g7mt/v/mXQHiIvJmDj5cJS173x8Q9Q=="], - "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg=="], + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-b9Na83rhRFQBd5CuMmuEqokYhmyWUbG7mNZl/thGhBwLpCdiZ85TZ3WFhF7OVgOekC5uKhLk4C3HKtdiScCMdQ=="], - "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA=="], + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.9.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.0", "@peculiar/asn1-x509": "^2.9.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-f+u+EyGjfPvPzvr0+rlh/TFEO7LWt84mEwQynCUYr4F6urf/8s6+ESJ23qfSn1aamkZR6AuBNaC62iEsGvp0+w=="], "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], @@ -954,7 +958,7 @@ "@pingpay/onramp-types": ["@pingpay/onramp-types@0.0.3", "", { "dependencies": { "zod": "^3.25.67" }, "peerDependencies": { "typescript": "^5" } }, "sha512-qo1BCCgd9vCd3RXkE34Sl1XP2yD8opbsf3r/cgWjM6/JBXzdpcN9p2ysec4xjo2SoHNUQ+ZfEH49eYLjpbmHOQ=="], - "@playwright/test": ["@playwright/test@1.62.0", "", { "dependencies": { "playwright": "1.62.0" }, "bin": { "playwright": "cli.js" } }, "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA=="], + "@playwright/test": ["@playwright/test@1.62.1", "", { "dependencies": { "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" } }, "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -1082,35 +1086,33 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.0", "", { "os": "none", "cpu": "arm64" }, "sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ=="], - "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.2.0", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.4", "", { "os": "none", "cpu": "arm64" }, "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], @@ -1150,11 +1152,11 @@ "@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.2", "", { "dependencies": { "error-stack-parser": "^2.1.4" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-k+/VrfTNgo+KirjI6V+8CWRj6y+DH9jOUWv8JorYY4vKf/9xfnZ8xHzuB4iqCwTtoZl9YnxOaOuoyjJipc2tiQ=="], - "@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="], + "@scure/base": ["@scure/base@2.3.0", "", {}, "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA=="], - "@scure/bip32": ["@scure/bip32@2.2.0", "", { "dependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-zFr7t2F+a9+5tB7QbarF2HQNYrgjCNaoLAupZdKkrFMYMozJf5zqH2WJCQibMzm1qQ0QogrxVGO3qXfQDYMaQg=="], + "@scure/bip32": ["@scure/bip32@2.3.0", "", { "dependencies": { "@noble/curves": "2.3.0", "@noble/hashes": "2.3.0", "@scure/base": "2.3.0" } }, "sha512-mMPjNcXxJsvNveIgRIXrnwd4omu0wdXo4JowAUZO7/82+/bpAwVltclx6MG6nv2M3dA6IbfVv4xwWxaREm3OcA=="], - "@scure/bip39": ["@scure/bip39@2.2.0", "", { "dependencies": { "@noble/hashes": "2.2.0", "@scure/base": "2.2.0" } }, "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A=="], + "@scure/bip39": ["@scure/bip39@2.3.0", "", { "dependencies": { "@noble/hashes": "2.3.0" } }, "sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], @@ -1210,7 +1212,7 @@ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], - "@tanstack/devtools": ["@tanstack/devtools@0.13.0", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "@tanstack/devtools-ui": "0.6.0", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-p/nOH9bS/OO/u3402zPjoGu+Mz6Fzi/iRqJuYghuuYRUY32kZt+C0/d+pP/bi6/2JTi1FdT6oEXI2lWlA5tXxw=="], + "@tanstack/devtools": ["@tanstack/devtools@0.14.0", "", { "dependencies": { "@neodrag/solid": "3.0.0-next.11", "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.2", "@tanstack/devtools-ui": "0.7.0", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-tN0SEi1BVaJYtkZbgYpvLsInjwNRAZkR3r6slSvz9Jm+bfkhcdjuOSrZp0cAwOGwdnauHZ1mYgtfe2AfC/V1NQ=="], "@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.8", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.5.0" } }, "sha512-cG3iZkGWCwN330bLBKa8+9r4Of2AXNoz2zUqcsy/4XsD3105ghVBx78cGyvJj9fSclNomPxoqAnDGXXhg1WLvA=="], @@ -1218,9 +1220,9 @@ "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.4", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw=="], - "@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.6.0", "", { "dependencies": { "clsx": "^2.1.1", "dayjs": "^1.11.19", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-CVaM6rT6Nl5ijo83vJYFa2SjofvpuOl/uOvbYGhBrRgUhhelNHhx8zZX+hnZCHmIr0/lzM65hsocnZ72592Rvg=="], + "@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.7.0", "", { "dependencies": { "clsx": "^2.1.1", "dayjs": "^1.11.19", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-px/a+JgRSVHDj/hID1cwfsIi+ly5l7t3Yw7fLw4G556HXCNgDri36fupt9h7oBeEKntpsx3ep/XtZn51rrCv2g=="], - "@tanstack/form-core": ["@tanstack/form-core@1.33.2", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.11.0" } }, "sha512-F60zJd15bGrXKonc1kpRYnNRNfiES7F+hgvrPMrsZznPLqZtO2DIg76OU6R25kCYkqYQY5xvuKteuWcUsc587A=="], + "@tanstack/form-core": ["@tanstack/form-core@1.33.5", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.11.0" } }, "sha512-3dfx9MBP0aq5sXKteikG629X9oviptrQj0IFRk9YGcb+lB7Kv5x8S17oOSk1wUWgjQZ4xVJEMbKwOAODymocgA=="], "@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="], @@ -1232,9 +1234,9 @@ "@tanstack/query-devtools": ["@tanstack/query-devtools@5.101.4", "", {}, "sha512-z5IPHnDX3aUWeTWlRKLyooBQekaCAw4xRpZqPQ390RiWTDBcTynjpPT221BArw0u2+pnQMdGvPQI9YNNubBcmA=="], - "@tanstack/react-devtools": ["@tanstack/react-devtools@0.10.9", "", { "dependencies": { "@tanstack/devtools": "0.13.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-lS6mtccEmUaodsWiRORGM/MGKT0jgzcy5v+eY6pzOPxEgzTHUDhca+WGxShFqKxmF4oneRxXjww1gkvMrWq6uw=="], + "@tanstack/react-devtools": ["@tanstack/react-devtools@0.10.10", "", { "dependencies": { "@tanstack/devtools": "0.14.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-hYH34MSVbajs1pUc22ftSapyKQp2gOh0w34a7ouYOdIcbXD6YPckSR2YpAWGq1rrs6N0l/rvV6LM/G1pgN5ARg=="], - "@tanstack/react-form": ["@tanstack/react-form@1.33.2", "", { "dependencies": { "@tanstack/form-core": "1.33.2", "@tanstack/react-store": "^0.11.0" }, "peerDependencies": { "@tanstack/react-start": "*", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@tanstack/react-start"] }, "sha512-nEfayOu+27q5cZ5E0G5dmnddqLcLjdFCatbL/LCs/iLD469a1o1yYJlr8RISV3GfnqsBpm0hf+8kM4okh5fPCw=="], + "@tanstack/react-form": ["@tanstack/react-form@1.33.5", "", { "dependencies": { "@tanstack/form-core": "1.33.5", "@tanstack/react-store": "^0.11.0" }, "peerDependencies": { "@tanstack/react-start": "*", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@tanstack/react-start"] }, "sha512-LlRB28qJwO/QCGaHvWnbdh4haBgTFiZVmzA2uzxSBS3YA7/IqrQ6HOBK70CkFQ+DbflZ7NawsmSln13h5iIdTA=="], "@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="], @@ -1320,15 +1322,17 @@ "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], - "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + "@types/parse-path": ["@types/parse-path@7.1.0", "", { "dependencies": { "parse-path": "*" } }, "sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q=="], + + "@types/pg": ["@types/pg@8.23.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gPGYzOgqj8wcPJ3GSojYWD1i3wjym3ezWgis/Zk2gY8u29x9tEsP3oaBYFwqsFYmKOm6sawDu3IHNwladQc0ig=="], "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], - "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="], "@types/retry": ["@types/retry@0.12.2", "", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], @@ -1402,51 +1406,55 @@ "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.8.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7cSJH6PaKLRBdCfiB4pM6EukvgOk5xV4tyuLOIOEqrHsbnV7brtyff7CjhZbeGozdIHoOnKOi5R7rrmCWN3QSw=="], + "@yuku-codegen/binding-android-arm64": ["@yuku-codegen/binding-android-arm64@0.8.7", "", { "os": "android", "cpu": "arm64" }, "sha512-C/0zV5IhgVdYhGJTwrY0v8dknxlhiKwtVJkMUaexu9/QvRmzlV4vfU3hZlUSgqc2BxQHntL1mCVbDq8j0FRFDw=="], + + "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.8.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/u+REDMI4a0/lsJXTM4c53/w31OGZLOReZIyg62uhgLs0kc8NHsj/nOcxTdlQjq5gi0zhdkccD9LaLTXcdzPvw=="], - "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.8.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mhooLL+L5ytMxgz4ueXCIirU796X2xj97d4KSQW1HxZGzX6h8wOk5bIAhGqcmOL2bqAmMZ+UkBTbPC9VpzKb/g=="], + "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-sMMzFOwCo4WXR+/6zIBThOocSC50iIIZZdfiIDbaLvj0Ax/rWt/iavyfEAqajyvzydLyCqR/ZItdLWSRlu1umw=="], - "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.8.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MHLOAlgGhdOh0ZfnWWnno4ljlFB04/Lox/7MIGYIvISMKFvejfC9Atb1t9G7pTVBvy+l5uqxzHGBSJUsDOOkTA=="], + "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.8.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MpdpKXix9P+Y1rKgjvcNeNtGjXeL1CmttNhYINrWls8kRpm4xM/oBGTmn6w7to8lAlwj5jm8q03dQPl5mRv4Qw=="], - "@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ur3Awo45Sc5/Fglr8WN5XIf4IwAsq8wLd917Du8ow6mStxsBTHqFiH+tT7d5jV0FqcJnwy8EHVczmgde0zTo1A=="], + "@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.8.7", "", { "os": "linux", "cpu": "arm" }, "sha512-rr1srFLlPAmC1vtxfc9C1YLDe3iH09YjfSeeIidBqKhzx1MATjOAq4mjlRUOnhr/L27MotWIYOFKwVsd5JZFOg=="], - "@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-FIy7Ttx8oeUCd/8Y6IjnOsu+lRc6En+V/H67BlVphOeCySZAo5LU8VWrb4tv0DvjaSOzdm3DmdmRzmzN7NCqWQ=="], + "@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.8.7", "", { "os": "linux", "cpu": "arm" }, "sha512-eAufXh8qBRpiSO6ueaMDL+yyoXIGLhpUce72YbcACtZU2qhExwBIJyEtQf5kH2Ki0X2aqkSjSfog4OPtKXan3Q=="], - "@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-s+25wl1TLvf+7LzasPEi1RR1sDfVAU0i1QH601mn3vj+HudFYBYNZtUBKFaZvl645QR6vcaDaGHnOaMZhBR3ig=="], + "@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.8.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-gw4w6wPoHObBrdIC4duVWLmJOvpdE25j5D7yrM5mACNlK4klRz/lv8hK+ssQk9EJHBgZjSaqZIJVFgqNYbfv7A=="], - "@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pRha3Cjm4AnA5wEuhpg+8XXoGfwz5X61/9a/VNxeau47+kH6xjJspkEloIy/HbHgUnvVRqVHoEHczdGZT2J9NA=="], + "@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.8.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-L68N6Y4XkqcIaKo3Ra88JEvBEH4AHff44A4INcrxeVWZ8CZtu2tCpfVxe3hR8qQQMcvBSQlDn24KpkCKEhVvfA=="], - "@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GySXmiw5Dw99Ba3GMV2ExQVckihuangnrIpSJagPx8RFUNtwfmzpr2ibf8k31eEAE4YUofV7mEfJN5tN6+POnQ=="], + "@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.8.7", "", { "os": "linux", "cpu": "x64" }, "sha512-dzyAbltJmf3Cqlb8HcFuYIf5Yn0fl1vTr3XJ9HiVNNvOlhqPSArOqtw9vI1p6/VTXTjrMLXlU+s+/kNHiIy/Cw=="], - "@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8YFfcPZz44v8YyekWVKkNz/cD4t6DW62/dr03OVtMfwUtHfmo/8wpby0JKMleAOXbgWItSGw81LtwqM4I9oS0A=="], + "@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.8.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Otw4MH3404q0Bbvl+YTdW9aoUV5vXmUw8260bWvt1XlaoIX/ceSgI4ygheRDMfBPoHt6FDayQaO9OLVUZAgkFA=="], - "@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YhENbgkuzjsil+zDNV35oU3PQMDg2RXh8BPt915WCNAbIIHcqgYupHJF3564206+DbPkZtcDvcoa34Wb5B8tbQ=="], + "@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.8.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-qo/jyrzryiBuKEsFiuWaBCBe3tRMynQ0qFWFgOEjcCMQeZfBm+wKiVEUEFXXLc7bh8YezguAWp0Mtnhq4ARNyA=="], - "@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.8.0", "", { "os": "win32", "cpu": "x64" }, "sha512-qvzSRABXe6/ndubx+RNwgbFVbs7Pqroz/q/UR6vm++xsmfcpkMF43B23jgNl2xi2IUrEt8C/L1bBslXp+LtgnA=="], + "@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.8.7", "", { "os": "win32", "cpu": "x64" }, "sha512-D5lDsVDx6m00E6bWySlWdH72Ca4TPSaphDqB6QjU6MpuNLIJqoGoatYyq2rOmBE8Zv/kunot/o58KGL03P3eiA=="], - "@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.8.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-04AakSJhI4mPrqhZzXdFyaEDh0YkfeqbnyYY3aCrmxeWfR/Xr8+kFn5sh+wZYN/5HatPniELKHixJuUCUyfBvg=="], + "@yuku-parser/binding-android-arm64": ["@yuku-parser/binding-android-arm64@0.8.7", "", { "os": "android", "cpu": "arm64" }, "sha512-eGKYiUDX7Y0V7tDTmg+JTVnXnjMqfXXsorZ+EDf5kxwchQ3Or1HS14MzI2fw+jFhHR85fCWt+mtX33Yao73hIQ=="], - "@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.8.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BQJGI9bDeyb/X2rwhtXoBTQ9EtbkJtteX6C7cZ9jow0pqqmoOufgHPP2+m65GLk2eVNCBcfl3xlTGpJ7RFcviw=="], + "@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.8.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re0RHelKLnjEURulY2/KxW+Ngb8zuNA4BRZuMwgGQNzVumT6u4U2N2hc01oeYVNVof0i7GrXE4UCNBgbpRRnjQ=="], - "@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.8.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-04hmgnU152wya88raI+RQhxZPgwXcHbfdNZLu3x5ggKnJVHLD8xZZLcWIKSs59CiEXL6PKVX1/cx8GoTYlaCaQ=="], + "@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hn8DROtQkjlA1ACbPgj4a7eP9IuVOI504oiTwpkWPbpaDWD9KdmnVYCqW+1LfenNK/g7O9NhWGpXEdaCNX7lIA=="], - "@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+cuJWUK13lwce721XGRjz7izSr2Q1U0RlHkDzdkohWpRWlttTqRdxNnaW13nDc47GBDrNsXMHx+KLcebeOeeGg=="], + "@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.8.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-bAP2OV8wRuzplX/jYxv9+vvqQT8JxyNphI8fLfXGL054Xs+4/J5u33cIm3y4rxY8rdoLmmdiJs2Tq7r7lrDRfA=="], - "@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-rOVPRqt9cm1YP/wPV+yyZh0FYr6UR/wm/BXslvLuge0LDewVvDrm/AxcDrwWuPAu61Nzdg2rMVfcNnrplCbC1w=="], + "@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.8.7", "", { "os": "linux", "cpu": "arm" }, "sha512-kTYwJQQgmZeAWdDIWabiReIZMpmfLueIj1tCmjStUtFGhR1Z0qwxonKVfUC4N7h/VhGGzLZ//7O1kgt1QKqgCg=="], - "@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pAoIozKr6E+ptpaQz4CZv1O7cay2f4m7kbd+DSQug5MKdUBTZZ18GdWSPvq0fwQYraH9hZlyLqrMaeP5W/ncrA=="], + "@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.8.7", "", { "os": "linux", "cpu": "arm" }, "sha512-uL4jE8HPT2BLlxAXyD10LqgPuXa9eDa0BKpCdSANmzIJghq/2eZo3/gQNtaxPZMupWoxjYzSad9IXrwu7aYPXQ=="], - "@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JCAg50ahXuYlrsIi1jmymX9X/9B0JYBRroAHnYttN44tAvCo1PFqukHrw1up6HvEOoLA0OjlM0Zwh60u3Gc/Zg=="], + "@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.8.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-3gVN4pWSKZmXiNX7cU164dR9MPvesCHnlH6nPfpK+yQsCuYphjKKplcb4SnZBrhiqmXbgb2HR0c2TS0IZUPhgA=="], - "@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tEeVQ14etp7lpUqXzq+X5AlQzFH+m3TVDiCKIq17zxnxJ117DOVvoveWSkSMt/lj68z48SvZUMHe7xLvqtO1lg=="], + "@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.8.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0mwfEjoLpxzXeZw802Wa4RaELsQiPtWqG6INcy8j4GtvNFtl4LCX3eGO1XLn9pyLAISLzTRyU3zUCBUPin8lg=="], - "@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+UGRYnF37nnbZNMsMjSGDXKUTIxYUmbbk3Lzib88sLK7kg2y80vjchYYoYsDUK9kAgqPWyA9hKGURHy/Kk9Few=="], + "@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.8.7", "", { "os": "linux", "cpu": "x64" }, "sha512-lnbWdPmerE5D1uH1G4IEZKnPzCrWCStRGrtgpSIe1RibAo5bZIjDbbbPYXmMHCEh4F+x/JaJpElh26a3r+BPbg=="], - "@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-l+7Va9/sX1ccRjzjJj6MR0arKsvHB5b419+pKbwzn+/18A/xfccWqmxXrn0C0NyVLlbEb3AV9Is+AbF72O85yA=="], + "@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.8.7", "", { "os": "linux", "cpu": "x64" }, "sha512-769uwndMvMzUvATWbAcEvyLHKA+DzhHSCl/obBUrRdYfRo26yxui6S8y3z7uJ+Naup7UKrDxrpK7OnQkxkl9KQ=="], - "@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.8.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dolKDTJv2xrWowDBEkvSaDvCsVFZOA7DCUuD+7DatglS68WbuxS4LudU5bzSeAOL5vgPpyGm82469cMLnL+6vQ=="], + "@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.8.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-mEB/9PlaAkisJ6KWGz0zvywXoU6+80dTlR2LwS7s/jcXXoU6fm2+sitBZXtqu3+Q4DcDgPxM45uWMCzPs0TSRw=="], - "@yuku-toolchain/types": ["@yuku-toolchain/types@0.8.0", "", {}, "sha512-hL/raFM5V9UT2lVE/lIWDTWvANqSB8TvMVp+PgICehBa96KhTl/UpGm370JXaacukR+QnaHM2Yv6O/UcrzAFGg=="], + "@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.8.7", "", { "os": "win32", "cpu": "x64" }, "sha512-8vNB2DP0ou61nGb8tc/qfi41gfyDXz1MHr2zqL3nR+cJ6CEbiuWV/l/a/vv151gCgiZLLAyGkQGENpozdg716w=="], + + "@yuku-toolchain/types": ["@yuku-toolchain/types@0.8.7", "", {}, "sha512-2Z53dNxAJL6UvFoIrDZvYf3zlO8s4VJK4O2hhaB4mXVwwpX/7ajtss3cmfqKvamlNLWyt9FSWs4eoYdlbxpnHA=="], "@zorsh/zorsh": ["@zorsh/zorsh@0.5.0", "", {}, "sha512-aQ3jO0uoFfgPLkg68iP2nVzt9yFpBs/vrSiZ5G2TRmpNuhSW0lL1hMvszbalogf4JSuU7lcVFcPiiu/xC8/5AA=="], @@ -1454,11 +1462,11 @@ "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], - "adm-zip": ["adm-zip@0.5.10", "", {}, "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ=="], + "adm-zip": ["adm-zip@0.6.0", "", {}, "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -1504,7 +1512,7 @@ "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - "axios": ["axios@1.18.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g=="], + "axios": ["axios@1.19.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw=="], "axios-retry": ["axios-retry@4.5.0", "", { "dependencies": { "is-retry-allowed": "^2.2.0" }, "peerDependencies": { "axios": "0.x || 1.x" } }, "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ=="], @@ -1514,7 +1522,7 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.5", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-xJo6a6YZnwZfnyGmQKWMbVOcii7XRibjOskRh+WJ9UHQoX16xrQrcIgAMQOzfvs8XiLMx6ih/fsLPF73iY2D1A=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA=="], "batch": ["batch@0.6.1", "", {}, "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw=="], @@ -1522,7 +1530,7 @@ "better-call": ["better-call@1.3.7", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Al51/hjp2SSp6CRTa3F2ptcx4yQVS1xWKoY6jcVXqNYOap6mHFP2jUBn5EwIL4iIed1/Sq4hlQ+Umm6EflZG+w=="], - "better-near-auth": ["better-near-auth@1.8.3", "", { "dependencies": { "@scure/base": "^2.2.0", "nanostores": "^1.1.0", "near-kit": "^0.14.0", "zod": "^4.4.3" }, "peerDependencies": { "@better-auth/core": "^1.6.25", "better-auth": "^1.6.25", "typescript": "^5.9.3" } }, "sha512-RKr1UYk6Xs6wPCIRq3oRHLKmxOEuDBo8ofaoD66DKBI1qJZTT/rZbcc5hj0O29JJ8+QiUgQfsQ/Qa67kEziP/A=="], + "better-near-auth": ["better-near-auth@1.10.0", "", { "dependencies": { "@scure/base": "^2.2.0", "nanostores": "^1.1.0", "near-kit": "^0.14.0", "zod": "^4.4.3" }, "peerDependencies": { "@better-auth/core": "^1.6.25", "better-auth": "^1.6.25", "typescript": "^5.9.3" } }, "sha512-qLMm+pM+3wUHewLvEwpgT0LDFAIXhRwoKr0IgNcKC0Eu+DALaqe6x8KGX7FmIffY9ETevGubNjth2cmG1AaOMg=="], "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], @@ -1532,13 +1540,13 @@ "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], - "bonjour-service": ["bonjour-service@1.4.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-2Kd5UYlFUVgAKMTyuBLl6w49wqfOnbxHqmuH0oCl/n7TfAikR0zoowNOP5BU4dfXmm+Vr9JyEN370auSMx+CNg=="], + "bonjour-service": ["bonjour-service@1.4.4", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw=="], - "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], @@ -1556,7 +1564,7 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + "caniuse-lite": ["caniuse-lite@1.0.30001809", "", {}, "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1636,15 +1644,15 @@ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], + "dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -1696,7 +1704,7 @@ "effect": ["effect@3.21.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-rXd2FGDM8KdjSIrc+mqEELo7ScW7xTVxEf1iInmPSpIde9/nyGuFM710cjTo7/EreGXiUX2MOonPpprbz2XHCg=="], - "electron-to-chromium": ["electron-to-chromium@1.5.396", "", {}, "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.408", "", {}, "sha512-SLoprcYpJ/OH2v2ps0+N5biv9H4/KBT3+YmmDew64TwK5y9j2wv7pMOFY7IorVkyMtEyLSCRlXKLsNlakeAlPw=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -1706,7 +1714,7 @@ "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="], + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -1718,15 +1726,15 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="], + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="], + "es-toolkit": ["es-toolkit@1.51.0", "", {}, "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw=="], - "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -1752,9 +1760,9 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="], + "eventsource": ["eventsource@4.1.1", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-D6bTRWh6KahHTK/m4WnjPQyEinNPf9eFLEZSEoj7d6fTibspnAVYfzHvirL7u/aoX5d9YYfIkBVAhmigUELk9w=="], - "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], "every-plugin": ["every-plugin@workspace:packages/every-plugin"], @@ -1768,7 +1776,7 @@ "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], - "exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="], + "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -1780,7 +1788,7 @@ "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="], + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -1804,7 +1812,7 @@ "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "flatted": ["flatted@3.4.3", "", {}, "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], @@ -1814,7 +1822,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], + "framer-motion": ["framer-motion@12.43.0", "", { "dependencies": { "motion-dom": "^12.43.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -1836,13 +1844,13 @@ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], "giget": ["giget@3.3.1", "", { "bin": { "giget": "dist/cli.mjs" } }, "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg=="], - "git-up": ["git-up@7.0.0", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^8.1.0" } }, "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ=="], + "git-up": ["git-up@8.1.1", "", { "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^9.2.0" } }, "sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g=="], - "git-url-parse": ["git-url-parse@15.0.0", "", { "dependencies": { "git-up": "^7.0.0" } }, "sha512-5reeBufLi+i4QD3ZFftcJs9jC26aULFLBU23FeKM/b1rI0K6ofIeAblmDVO7Ht22zTDE9+CkJ3ZVb0CgJmz3UQ=="], + "git-url-parse": ["git-url-parse@16.1.0", "", { "dependencies": { "git-up": "^8.1.0" } }, "sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -1890,9 +1898,9 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "highlight.js": ["highlight.js@11.11.2", "", {}, "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ=="], - "hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="], + "hono": ["hono@4.13.2", "", {}, "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA=="], "hono-rate-limiter": ["hono-rate-limiter@0.5.3", "", { "peerDependencies": { "hono": "^4.10.8", "unstorage": "^1.17.3" }, "optionalPeers": ["unstorage"] }, "sha512-M0DxbVMpPELEzLi0AJg1XyBHLGJXz7GySjsPoK+gc5YeeBsdGDGe+2RvVuCAv8ydINiwlbxqYMNxUEyYfRji/A=="], @@ -1938,7 +1946,7 @@ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "ipaddr.js": ["ipaddr.js@2.4.0", "", {}, "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ=="], + "ipaddr.js": ["ipaddr.js@2.5.0", "", {}, "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], @@ -1998,13 +2006,13 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="], + "jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="], - "js-base64": ["js-base64@3.9.2", "", {}, "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA=="], + "js-base64": ["js-base64@3.9.3", "", {}, "sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -2020,35 +2028,35 @@ "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - "kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="], + "kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="], "launch-editor": ["launch-editor@2.14.1", "", { "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.4" } }, "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA=="], "libsql": ["libsql@0.5.29", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.29", "@libsql/darwin-x64": "0.5.29", "@libsql/linux-arm-gnueabihf": "0.5.29", "@libsql/linux-arm-musleabihf": "0.5.29", "@libsql/linux-arm64-gnu": "0.5.29", "@libsql/linux-arm64-musl": "0.5.29", "@libsql/linux-x64-gnu": "0.5.29", "@libsql/linux-x64-musl": "0.5.29", "@libsql/win32-x64-msvc": "0.5.29" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg=="], - "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -2060,7 +2068,7 @@ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.27.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw=="], + "lucide-react": ["lucide-react@1.31.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -2102,7 +2110,7 @@ "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], - "memfs": ["memfs@4.64.0", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.64.0", "@jsonjoy.com/fs-fsa": "4.64.0", "@jsonjoy.com/fs-node": "4.64.0", "@jsonjoy.com/fs-node-builtins": "4.64.0", "@jsonjoy.com/fs-node-to-fsa": "4.64.0", "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/fs-print": "4.64.0", "@jsonjoy.com/fs-snapshot": "4.64.0", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw=="], + "memfs": ["memfs@4.68.1", "", { "dependencies": { "@jsonjoy.com/fs-core": "4.68.1", "@jsonjoy.com/fs-fsa": "4.68.1", "@jsonjoy.com/fs-node": "4.68.1", "@jsonjoy.com/fs-node-builtins": "4.68.1", "@jsonjoy.com/fs-node-to-fsa": "4.68.1", "@jsonjoy.com/fs-node-utils": "4.68.1", "@jsonjoy.com/fs-print": "4.68.1", "@jsonjoy.com/fs-snapshot": "4.68.1", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw=="], "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], @@ -2180,7 +2188,7 @@ "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "minimizer-webpack-plugin": ["minimizer-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "@minify-html/node": "*", "@swc/core": "*", "@swc/css": "*", "@swc/html": "*", "clean-css": "*", "cssnano": "*", "csso": "*", "esbuild": "*", "html-minifier-terser": "*", "lightningcss": "*", "postcss": "*", "uglify-js": "*", "webpack": "^5.1.0" }, "optionalPeers": ["@minify-html/node", "@swc/core", "@swc/css", "@swc/html", "clean-css", "cssnano", "csso", "esbuild", "html-minifier-terser", "lightningcss", "postcss", "uglify-js"] }, "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw=="], @@ -2190,7 +2198,7 @@ "mipd": ["mipd@0.0.7", "", { "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg=="], - "motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], + "motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="], "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], @@ -2210,7 +2218,7 @@ "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], - "nanostores": ["nanostores@1.4.1", "", {}, "sha512-PGd3uPojJB9Z07d5NX3Db/SOSBbyy3wLMUGq0GpnEEJfVzY9mq7daPMAZ3jObV5D3Jn+YKND636eI5ULg7F80Q=="], + "nanostores": ["nanostores@1.5.1", "", {}, "sha512-DNIX+HyFpo14fKGe0NsX9/aPzdKGiSZwX5xEMpDwQrDdo2iTiUYunOCCQLYwJrziKVe8ZOtVvcv0dEVI8lMx2g=="], "near-kit": ["near-kit@0.14.0", "", { "dependencies": { "@napi-rs/keyring": "^1.3.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.0.1", "@scure/base": "^2.2.0", "@scure/bip32": "^2.2.0", "@scure/bip39": "^2.2.0", "@zorsh/zorsh": "^0.5.0", "tar": "^7.5.13", "zod": "^4.4.3" }, "peerDependencies": { "@hot-labs/near-connect": ">=0.11.0", "@near-wallet-selector/core": ">=8.0.0" }, "optionalPeers": ["@hot-labs/near-connect", "@near-wallet-selector/core"] }, "sha512-RWv/CNhctWJeIxIFDgBXbxym24U93huJYy8zK0u5uEVtrqsb8gwkJ2nRz80tJuoBT16xQbTJi3dzhk6mrWoZuw=="], @@ -2234,7 +2242,7 @@ "node-persist": ["node-persist@4.0.4", "", { "dependencies": { "p-limit": "^3.1.0" } }, "sha512-8sPAz/7tw1mCCc8xBG4f0wi+flHkSSgQeX998iQ75Pu27evA6UUWCjSE7xnrYTg2q33oU5leJ061EKPDv6BocQ=="], - "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + "node-releases": ["node-releases@2.0.53", "", {}, "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], @@ -2246,7 +2254,7 @@ "obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="], - "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -2284,7 +2292,7 @@ "parse-path": ["parse-path@7.1.0", "", { "dependencies": { "protocols": "^2.0.0" } }, "sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw=="], - "parse-url": ["parse-url@8.1.0", "", { "dependencies": { "parse-path": "^7.0.0" } }, "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w=="], + "parse-url": ["parse-url@9.2.0", "", { "dependencies": { "@types/parse-path": "^7.0.0", "parse-path": "^7.0.0" } }, "sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -2304,7 +2312,7 @@ "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], - "pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="], + "pg": ["pg@8.23.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.16.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg=="], "pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="], @@ -2314,7 +2322,7 @@ "pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="], - "pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="], + "pg-protocol": ["pg-protocol@1.16.0", "", {}, "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], @@ -2328,11 +2336,11 @@ "pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="], - "playwright": ["playwright@1.62.0", "", { "dependencies": { "playwright-core": "1.62.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw=="], + "playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], - "playwright-core": ["playwright-core@1.62.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA=="], + "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], - "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], @@ -2366,7 +2374,7 @@ "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], - "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + "pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="], "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], @@ -2410,7 +2418,7 @@ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "recast": ["recast@0.23.12", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA=="], + "recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="], "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], @@ -2442,7 +2450,7 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rolldown": ["rolldown@1.2.0", "", { "dependencies": { "@oxc-project/types": "=0.140.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.0", "@rolldown/binding-darwin-arm64": "1.2.0", "@rolldown/binding-darwin-x64": "1.2.0", "@rolldown/binding-freebsd-x64": "1.2.0", "@rolldown/binding-linux-arm-gnueabihf": "1.2.0", "@rolldown/binding-linux-arm64-gnu": "1.2.0", "@rolldown/binding-linux-arm64-musl": "1.2.0", "@rolldown/binding-linux-ppc64-gnu": "1.2.0", "@rolldown/binding-linux-s390x-gnu": "1.2.0", "@rolldown/binding-linux-x64-gnu": "1.2.0", "@rolldown/binding-linux-x64-musl": "1.2.0", "@rolldown/binding-openharmony-arm64": "1.2.0", "@rolldown/binding-wasm32-wasi": "1.2.0", "@rolldown/binding-win32-arm64-msvc": "1.2.0", "@rolldown/binding-win32-x64-msvc": "1.2.0" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA=="], + "rolldown": ["rolldown@1.2.4", "", { "dependencies": { "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.4", "@rolldown/binding-darwin-arm64": "1.2.4", "@rolldown/binding-darwin-x64": "1.2.4", "@rolldown/binding-freebsd-x64": "1.2.4", "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", "@rolldown/binding-linux-arm64-gnu": "1.2.4", "@rolldown/binding-linux-arm64-musl": "1.2.4", "@rolldown/binding-linux-ppc64-gnu": "1.2.4", "@rolldown/binding-linux-s390x-gnu": "1.2.4", "@rolldown/binding-linux-x64-gnu": "1.2.4", "@rolldown/binding-linux-x64-musl": "1.2.4", "@rolldown/binding-openharmony-arm64": "1.2.4", "@rolldown/binding-win32-arm64-msvc": "1.2.4", "@rolldown/binding-win32-x64-msvc": "1.2.4" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w=="], "rolldown-plugin-dts": ["rolldown-plugin-dts@0.27.14", "", { "dependencies": { "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.5", "obug": "^2.1.4", "yuku-ast": "^0.8.0", "yuku-codegen": "^0.8.0", "yuku-parser": "^0.8.0" }, "peerDependencies": { "@typescript/native-preview": "*", "@volar/typescript": "~2.4.0", "rolldown": "^1.0.0", "typescript": "^5.0.0 || ^6.0.0 || ~7.0.0", "vue-tsc": "~3.2.0 || ~3.3.0" }, "optionalPeers": ["@typescript/native-preview", "@volar/typescript", "typescript", "vue-tsc"] }, "sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw=="], @@ -2468,9 +2476,9 @@ "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], - "seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="], + "seroval": ["seroval@1.6.2", "", {}, "sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ=="], - "seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="], + "seroval-plugins": ["seroval-plugins@1.6.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ=="], "serve-index": ["serve-index@1.9.2", "", { "dependencies": { "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.8.0", "mime-types": "~2.1.35", "parseurl": "~1.3.3" } }, "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ=="], @@ -2512,7 +2520,7 @@ "solid-js": ["solid-js@1.9.14", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ=="], - "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "sonner": ["sonner@2.0.8", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], @@ -2574,7 +2582,7 @@ "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], - "terser": ["terser@5.49.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA=="], + "terser": ["terser@5.50.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w=="], "thingies": ["thingies@2.6.1", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw=="], @@ -2588,13 +2596,13 @@ "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], - "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -2618,7 +2626,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], + "tsx": ["tsx@4.23.12", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q=="], "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], @@ -2662,7 +2670,7 @@ "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], - "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "update-browserslist-db": ["update-browserslist-db@1.3.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], @@ -2678,15 +2686,15 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "verkit": ["verkit@0.3.1", "", {}, "sha512-w2Eo8LSIIoW7qxNBzT7/17k+bh8plXo7G3dHjEIDqPlnluhzaxr9JX8F28VSYEtDvc1/a3WBDih6xNUZseebXg=="], + "verkit": ["verkit@0.3.2", "", {}, "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "viem": ["viem@2.55.16", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.33", "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-bKs/0PNIkiaG45ZM0ERddHSVtku7hvyebK2i3bMLuyJQevh15tpDh3MiE3PhLjx5kxGrvizaXQw67gWc9Jl7fQ=="], + "viem": ["viem@2.55.17", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.33", "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-XKASpfG9jznxsU/J7Bj2k9aS0UZgU3sBcwVoQ6slD5T1RmNJHh3khmFVJuRKjrOe3uqHb0FJKbb6jwE49WtVDQ=="], - "vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="], + "vite": ["vite@8.2.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.25", "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw=="], "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], @@ -2700,7 +2708,7 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack": ["webpack@5.109.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.24.2", "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "graceful-fs": "^4.2.11", "mime-db": "^1.54.0", "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "watchpack": "^2.5.2", "webpack-sources": "^3.5.1" }, "peerDependencies": { "webpack-cli": "*" }, "optionalPeers": ["webpack-cli"], "bin": { "webpack": "bin/webpack.js" } }, "sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg=="], + "webpack": ["webpack@5.109.2", "", { "dependencies": { "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.24.4", "es-module-lexer": "^2.1.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "graceful-fs": "^4.2.11", "mime-db": "^1.54.0", "minimizer-webpack-plugin": "^5.6.1", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "watchpack": "^2.5.2", "webpack-sources": "^3.5.1" }, "peerDependencies": { "webpack-cli": "*" }, "optionalPeers": ["webpack-cli"], "bin": { "webpack": "bin/webpack.js" } }, "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw=="], "webpack-bundle-analyzer": ["webpack-bundle-analyzer@4.10.2", "", { "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", "commander": "^7.2.0", "debounce": "^1.2.1", "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", "html-escaper": "^2.0.2", "opener": "^1.5.2", "picocolors": "^1.0.0", "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { "webpack-bundle-analyzer": "lib/bin/analyzer.js" } }, "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw=="], @@ -2744,25 +2752,25 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "yuku-ast": ["yuku-ast@0.8.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.0" } }, "sha512-trBzFsSa6k32vzNUCH6pFhAoTzWD/NifSYOIQ/6v14vXKh7TRhd2vDNIwRguGdXvcyfBNEEGcsfxFHrnJbS+FQ=="], + "yuku-ast": ["yuku-ast@0.8.7", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.7" } }, "sha512-h6+4bDfyootiMB9vckk5uKo5r5j0GHrkr17FQTDNfEsFT3DWlN9uu1HJwQwc64pgmLCI945fWM3lbTIqxjT3GQ=="], - "yuku-codegen": ["yuku-codegen@0.8.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.0" }, "optionalDependencies": { "@yuku-codegen/binding-darwin-arm64": "0.8.0", "@yuku-codegen/binding-darwin-x64": "0.8.0", "@yuku-codegen/binding-freebsd-x64": "0.8.0", "@yuku-codegen/binding-linux-arm-gnu": "0.8.0", "@yuku-codegen/binding-linux-arm-musl": "0.8.0", "@yuku-codegen/binding-linux-arm64-gnu": "0.8.0", "@yuku-codegen/binding-linux-arm64-musl": "0.8.0", "@yuku-codegen/binding-linux-x64-gnu": "0.8.0", "@yuku-codegen/binding-linux-x64-musl": "0.8.0", "@yuku-codegen/binding-win32-arm64": "0.8.0", "@yuku-codegen/binding-win32-x64": "0.8.0" } }, "sha512-f82SDo8moLRymtdYN7/cz2yRbWE6Pmbmph+mLj24QkIR8ASG/c2nETdHzBhV/3rR/r8K+qmy7+JqQV3thHAm8g=="], + "yuku-codegen": ["yuku-codegen@0.8.7", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.7" }, "optionalDependencies": { "@yuku-codegen/binding-android-arm64": "0.8.7", "@yuku-codegen/binding-darwin-arm64": "0.8.7", "@yuku-codegen/binding-darwin-x64": "0.8.7", "@yuku-codegen/binding-freebsd-x64": "0.8.7", "@yuku-codegen/binding-linux-arm-gnu": "0.8.7", "@yuku-codegen/binding-linux-arm-musl": "0.8.7", "@yuku-codegen/binding-linux-arm64-gnu": "0.8.7", "@yuku-codegen/binding-linux-arm64-musl": "0.8.7", "@yuku-codegen/binding-linux-x64-gnu": "0.8.7", "@yuku-codegen/binding-linux-x64-musl": "0.8.7", "@yuku-codegen/binding-win32-arm64": "0.8.7", "@yuku-codegen/binding-win32-x64": "0.8.7" } }, "sha512-adwDZSh8oVDzhE6Du9PwVWxcOxeV0e2EVhUuMKWfhSY4wkrDq9eqixlxFF3l/XGUV1E7UFzhpz9393MUumkyNw=="], - "yuku-parser": ["yuku-parser@0.8.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.0", "yuku-ast": "^0.8.0" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.8.0", "@yuku-parser/binding-darwin-x64": "0.8.0", "@yuku-parser/binding-freebsd-x64": "0.8.0", "@yuku-parser/binding-linux-arm-gnu": "0.8.0", "@yuku-parser/binding-linux-arm-musl": "0.8.0", "@yuku-parser/binding-linux-arm64-gnu": "0.8.0", "@yuku-parser/binding-linux-arm64-musl": "0.8.0", "@yuku-parser/binding-linux-x64-gnu": "0.8.0", "@yuku-parser/binding-linux-x64-musl": "0.8.0", "@yuku-parser/binding-win32-arm64": "0.8.0", "@yuku-parser/binding-win32-x64": "0.8.0" } }, "sha512-obrazyE8Cyh79xTQS9wv44khnhvkXG1CDSSy0bg+Pjjla+iXkKXK2b6+LTYXSN3qvVfQxc0MN5qhNKYr9sl3Zw=="], + "yuku-parser": ["yuku-parser@0.8.7", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.7", "yuku-ast": "^0.8.7" }, "optionalDependencies": { "@yuku-parser/binding-android-arm64": "0.8.7", "@yuku-parser/binding-darwin-arm64": "0.8.7", "@yuku-parser/binding-darwin-x64": "0.8.7", "@yuku-parser/binding-freebsd-x64": "0.8.7", "@yuku-parser/binding-linux-arm-gnu": "0.8.7", "@yuku-parser/binding-linux-arm-musl": "0.8.7", "@yuku-parser/binding-linux-arm64-gnu": "0.8.7", "@yuku-parser/binding-linux-arm64-musl": "0.8.7", "@yuku-parser/binding-linux-x64-gnu": "0.8.7", "@yuku-parser/binding-linux-x64-musl": "0.8.7", "@yuku-parser/binding-win32-arm64": "0.8.7", "@yuku-parser/binding-win32-x64": "0.8.7" } }, "sha512-vRD9nwt4L3aYpxNqeSC4WqLv58xrXef0Ong1Mc45CTXTIpvLafx7JO05sczmQZwdLEZvywrLOGdNC5+Rp5N1BQ=="], - "zephyr-agent": ["zephyr-agent@1.1.2", "", { "dependencies": { "@toon-format/toon": "^0.9.0", "axios": "^1.15.0", "axios-retry": "^4.5.0", "debug": "^4.3.4", "eventsource": "^4.0.0", "git-url-parse": "^15.0.0", "https-proxy-agent": "^7.0.6", "is-ci": "^4.1.0", "jose": "^5.10.0", "node-persist": "^4.0.1", "open": "^10.1.0", "proper-lockfile": "^4.1.2", "tslib": "^2.8.1", "zephyr-edge-contract": "1.1.2" } }, "sha512-mJlj9VL2P+wzaLge5sF27LrYBP+wVj+5BiNaQIPTsLSiU9Om3CXHXWTR6ss0w4FIdYazrJyAq2KFw3PgKsBDgA=="], + "zephyr-agent": ["zephyr-agent@1.2.2", "", { "dependencies": { "@toon-format/toon": "^0.9.0", "axios": "^1.18.1", "axios-retry": "^4.5.0", "debug": "^4.4.3", "eventsource": "^4.1.0", "git-url-parse": "^16.1.0", "https-proxy-agent": "^7.0.6", "is-ci": "^4.1.0", "jiti": "^2.7.0", "jose": "^5.10.0", "node-persist": "^4.0.4", "open": "^10.2.0", "proper-lockfile": "^4.1.2", "zephyr-edge-contract": "1.2.2" } }, "sha512-lN/QvWcNj/+5amGqp+FbvpOXGF1yFUptpl8kWSkTMbYiMJGX6qL/5E7QRT9wtD9bgPsqq8HsSG+MkZMPNG3jlw=="], - "zephyr-edge-contract": ["zephyr-edge-contract@1.1.2", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-ZeHYPIrDifKutPzTsjPjHP9JIdPqTpyBESeHHZzG8VSzo6UT58tpwUnDDgJtF/ONwresnD/0ypEYxfBxtcrV6Q=="], + "zephyr-edge-contract": ["zephyr-edge-contract@1.2.2", "", {}, "sha512-2/PdLJ8CBFL7t9L047ejL/adhxv7oVC9kTgvckOuk6EauU9Y1CQ88JsnPB4EkKb/fYjZmurtp43E79MhRU5Rjw=="], - "zephyr-rsbuild-plugin": ["zephyr-rsbuild-plugin@1.1.2", "", { "dependencies": { "zephyr-rspack-plugin": "1.1.2" }, "peerDependencies": { "@rsbuild/core": "^1.0.0 || ^2.0.0-0" } }, "sha512-MhXfQ9oCP76LBDDc8Yl21ridsXFMobvTIjjyYbi+4LpOIof8ap+ZLyVtE14S1w4VyDerZNIzenOEJGbvJ5I/9g=="], + "zephyr-rsbuild-plugin": ["zephyr-rsbuild-plugin@1.2.2", "", { "dependencies": { "zephyr-agent": "1.2.2", "zephyr-rspack-plugin": "1.2.2", "zephyr-xpack-internal": "1.2.2" }, "peerDependencies": { "@rsbuild/core": "^1.0.0 || ^2.0.0-0" } }, "sha512-jeKcM16VHCUutttPgXuj6wPeJ9mT/OVEE/vlHPGOqkmu/vEow8Zf1pVKvurgxP63knoS1lFN8XKzuVZ09Ul9KA=="], - "zephyr-rspack-plugin": ["zephyr-rspack-plugin@1.1.2", "", { "dependencies": { "tslib": "^2.8.1", "zephyr-agent": "1.1.2", "zephyr-xpack-internal": "1.1.2" }, "peerDependencies": { "@rspack/core": "^1.0.0 || ^2.0.0-0" } }, "sha512-OFIghyy2GXaLB/RLJCEWmRoXQf0FNvNB2VQJL3mWofk8oNRu1PxYCxCJ3ThAGN2tRJv0sCjMpzqpuHU+h3Kv/A=="], + "zephyr-rspack-plugin": ["zephyr-rspack-plugin@1.2.2", "", { "dependencies": { "zephyr-agent": "1.2.2", "zephyr-xpack-internal": "1.2.2" }, "peerDependencies": { "@rspack/core": "^1.0.0 || ^2.0.0-0" } }, "sha512-W2Mj9LSLL3HPDdP1WzNnSAp4fnQufSBDzsCPDQy+s9fLZm0kLZMxcS29f06E78t05YyvOg+ZrWqB1y/d7VENYw=="], - "zephyr-xpack-internal": ["zephyr-xpack-internal@1.1.2", "", { "dependencies": { "@module-federation/automatic-vendor-federation": "^1.2.1", "tslib": "^2.8.1", "zephyr-agent": "1.1.2", "zephyr-edge-contract": "1.1.2" } }, "sha512-IYs5aMOxGXH3YGv7Z9uW5pH9/r15UNNEg4CdSuWDDTJ7MskMVmHjSYwM57AYxHvMw/loVXOY1PQDVaUxklOUfQ=="], + "zephyr-xpack-internal": ["zephyr-xpack-internal@1.2.2", "", { "dependencies": { "@module-federation/automatic-vendor-federation": "^1.2.1", "zephyr-agent": "1.2.2", "zephyr-edge-contract": "1.2.2" } }, "sha512-SejAjAhoYX2lSTlFVybeZzbcDeVbk299XIQ458qIx57Vy9u01/CIWeBo14qZam1pEcZT2j/3Xio/6wZfIdk3bg=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + "zustand": ["zustand@5.0.15", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -2772,9 +2780,9 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@effect/platform-node/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "@effect/platform-node/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "@effect/platform-node-shared/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "@effect/platform-node-shared/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -2792,7 +2800,7 @@ "@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "@libsql/isomorphic-ws/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "@libsql/isomorphic-ws/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], @@ -2806,26 +2814,58 @@ "@module-federation/cli/jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], - "@module-federation/dts-plugin/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], - "@module-federation/dts-plugin/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "@module-federation/enhanced/@module-federation/runtime-tools": ["@module-federation/runtime-tools@2.8.0", "", { "dependencies": { "@module-federation/runtime": "2.8.0", "@module-federation/webpack-bundler-runtime": "2.8.0" } }, "sha512-3yOqjdSHXxX4HA3GhlXg3hghGAXW2RJUsnwXCcik2/lTxOHizKI8f3RM+GGCKPxDVqtw43IShe3tA12jNL5A/A=="], + "@module-federation/enhanced/@module-federation/runtime-tools": ["@module-federation/runtime-tools@2.8.2", "", { "dependencies": { "@module-federation/runtime": "2.8.2", "@module-federation/webpack-bundler-runtime": "2.8.2" } }, "sha512-eW/yPvZB2LbpbyPXPTnOeF1ieWl165D9QcPV0y5Bj1QYGDjL2cb+dnzrBp0fmFtJhCYqmAdsVNoYItVa3yuJ3g=="], - "@module-federation/inject-external-runtime-core-plugin/@module-federation/runtime-tools": ["@module-federation/runtime-tools@2.8.0", "", { "dependencies": { "@module-federation/runtime": "2.8.0", "@module-federation/webpack-bundler-runtime": "2.8.0" } }, "sha512-3yOqjdSHXxX4HA3GhlXg3hghGAXW2RJUsnwXCcik2/lTxOHizKI8f3RM+GGCKPxDVqtw43IShe3tA12jNL5A/A=="], + "@module-federation/inject-external-runtime-core-plugin/@module-federation/runtime-tools": ["@module-federation/runtime-tools@2.8.2", "", { "dependencies": { "@module-federation/runtime": "2.8.2", "@module-federation/webpack-bundler-runtime": "2.8.2" } }, "sha512-eW/yPvZB2LbpbyPXPTnOeF1ieWl165D9QcPV0y5Bj1QYGDjL2cb+dnzrBp0fmFtJhCYqmAdsVNoYItVa3yuJ3g=="], - "@module-federation/rspack/@module-federation/runtime-tools": ["@module-federation/runtime-tools@2.8.0", "", { "dependencies": { "@module-federation/runtime": "2.8.0", "@module-federation/webpack-bundler-runtime": "2.8.0" } }, "sha512-3yOqjdSHXxX4HA3GhlXg3hghGAXW2RJUsnwXCcik2/lTxOHizKI8f3RM+GGCKPxDVqtw43IShe3tA12jNL5A/A=="], + "@module-federation/rspack/@module-federation/runtime-tools": ["@module-federation/runtime-tools@2.8.2", "", { "dependencies": { "@module-federation/runtime": "2.8.2", "@module-federation/webpack-bundler-runtime": "2.8.2" } }, "sha512-eW/yPvZB2LbpbyPXPTnOeF1ieWl165D9QcPV0y5Bj1QYGDjL2cb+dnzrBp0fmFtJhCYqmAdsVNoYItVa3yuJ3g=="], "@module-federation/runtime-tools/@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="], "@module-federation/runtime-tools/@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="], + "@orpc/client/@orpc/shared": ["@orpc/shared@1.15.0", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-A3/JE7pQYSrrRm6/WYJxV3GBhpMdJRPM3h47slQtWBUZe9Sao5En5WBc4tISGQNP2emcez6qIvDziSgD2T+img=="], + + "@orpc/client/@orpc/standard-server": ["@orpc/standard-server@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0" } }, "sha512-bje/xn6thDqJY/JQ7xoOjmD1KWE4FsyZOPgnPxwqTZLx/r00stRhLuFvk1hDEGj9UlG7NLZEijDVx0wqvcyDzA=="], + + "@orpc/client/@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-XYVfgmIt71YrPJSI7RKRiNWjcyktPYHpevkjeohGNBP5aSMAUL95quZArAWr+XOPp4U56XRao54I6wFMZa4How=="], + + "@orpc/client/@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-fsTN+FrdPkseVx99yRenGJYSp0xOrEk9fODk8oh3zzricNYzlZ8GGgCYtDNgt9vFIR/J05dc+Nk76KhcPBQpLw=="], + + "@orpc/contract/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + "@orpc/experimental-publisher/@orpc/client": ["@orpc/client@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2", "@orpc/standard-server-fetch": "1.14.2", "@orpc/standard-server-peer": "1.14.2" } }, "sha512-/tFAua/w/mao2kQtJqjoCYEojHrKMisxOCK8qtkMKOUcXVxWMl+QWhP/MykjzFgkFdO9mzKOu1h7vJvpH73EBA=="], "@orpc/experimental-publisher/@orpc/shared": ["@orpc/shared@1.14.2", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-5YtbVz4yGbJgcyj7CmEv5FAy4xagCS/hP/MMAlHpJKBSlMHuD7FrDO4LQLFUmSkcBcbLhzX8Ll1ziUw3vtPasw=="], "@orpc/experimental-publisher/@orpc/standard-server": ["@orpc/standard-server@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2" } }, "sha512-XHySJICwDsJf211gcxtJBpzB1ldZrSHDW2mqbBQg+I2AewvTqWiqeGZV+SPvmq87q4IfBzncSuwMrRKplUJhsw=="], + "@orpc/json-schema/@orpc/openapi": ["@orpc/openapi@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi-client": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-0tZN91VoT6MEkOfw+ERKyozsnDXzmDSsBeMgEHN3Hl1WVU97T9l4aZzLlZILSNl3fat3HmduEDy/boNGmAWJkQ=="], + + "@orpc/openapi/@orpc/contract": ["@orpc/contract@1.15.0", "", { "dependencies": { "@orpc/client": "1.15.0", "@orpc/shared": "1.15.0", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-zo8+x+5iqnIMrFdcj+ibVgn6dJneWzLNcD3kZQxrJFGFNAFoxvZbwryR5wOdcHkaSCeS3N7Ib2NydU6pihSYmg=="], + + "@orpc/openapi/@orpc/interop": ["@orpc/interop@1.15.0", "", {}, "sha512-Ah48o/rc00rN1yP5HcaoVsDCjdUhoh3JP1VibBbvwijhGvdOwRwiw5RWiGu442GSKcrPt9etAGhWXH88q8C3ww=="], + + "@orpc/openapi/@orpc/server": ["@orpc/server@1.15.0", "", { "dependencies": { "@orpc/client": "1.15.0", "@orpc/contract": "1.15.0", "@orpc/interop": "1.15.0", "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-aws-lambda": "1.15.0", "@orpc/standard-server-fastify": "1.15.0", "@orpc/standard-server-fetch": "1.15.0", "@orpc/standard-server-node": "1.15.0", "@orpc/standard-server-peer": "1.15.0", "cookie": "^1.1.1" }, "peerDependencies": { "crossws": ">=0.3.4", "ws": ">=8.18.1" }, "optionalPeers": ["crossws", "ws"] }, "sha512-l9FnL1MjQ8g77mB10/8D61okuKHLJBWhuwmd6OF65POHzBF177kMCcbHvRFEqUuxSbzP2eNiZGf3kXPMHmIKiQ=="], + + "@orpc/openapi/@orpc/shared": ["@orpc/shared@1.15.0", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-A3/JE7pQYSrrRm6/WYJxV3GBhpMdJRPM3h47slQtWBUZe9Sao5En5WBc4tISGQNP2emcez6qIvDziSgD2T+img=="], + + "@orpc/openapi/@orpc/standard-server": ["@orpc/standard-server@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0" } }, "sha512-bje/xn6thDqJY/JQ7xoOjmD1KWE4FsyZOPgnPxwqTZLx/r00stRhLuFvk1hDEGj9UlG7NLZEijDVx0wqvcyDzA=="], + + "@orpc/openapi-client/@orpc/contract": ["@orpc/contract@1.15.0", "", { "dependencies": { "@orpc/client": "1.15.0", "@orpc/shared": "1.15.0", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-zo8+x+5iqnIMrFdcj+ibVgn6dJneWzLNcD3kZQxrJFGFNAFoxvZbwryR5wOdcHkaSCeS3N7Ib2NydU6pihSYmg=="], + + "@orpc/openapi-client/@orpc/shared": ["@orpc/shared@1.15.0", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.4.4" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-A3/JE7pQYSrrRm6/WYJxV3GBhpMdJRPM3h47slQtWBUZe9Sao5En5WBc4tISGQNP2emcez6qIvDziSgD2T+img=="], + + "@orpc/openapi-client/@orpc/standard-server": ["@orpc/standard-server@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0" } }, "sha512-bje/xn6thDqJY/JQ7xoOjmD1KWE4FsyZOPgnPxwqTZLx/r00stRhLuFvk1hDEGj9UlG7NLZEijDVx0wqvcyDzA=="], + + "@orpc/server/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + + "@orpc/tanstack-query/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + + "@orpc/zod/@orpc/openapi": ["@orpc/openapi@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi-client": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-0tZN91VoT6MEkOfw+ERKyozsnDXzmDSsBeMgEHN3Hl1WVU97T9l4aZzLlZILSNl3fat3HmduEDy/boNGmAWJkQ=="], + "@pingpay/onramp-sdk/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "@pingpay/onramp-sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -2834,19 +2874,17 @@ "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@rspack/dev-server/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "@rspack/dev-server/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], @@ -2854,11 +2892,11 @@ "@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.5.0", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-H+OH3zC6Vhu/K0NaVfQKknEKawc/+2PT+D3SB3Ox0V8SiMlTo0abbmH2rH0721R2aNYbjdMXA1oENOd8E2UVoA=="], - "@tanstack/devtools-event-bus/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "@tanstack/devtools-event-bus/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "@tanstack/form-core/@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="], + "@tanstack/form-core/@tanstack/store": ["@tanstack/store@0.11.1", "", {}, "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA=="], - "@tanstack/react-form/@tanstack/react-store": ["@tanstack/react-store@0.11.0", "", { "dependencies": { "@tanstack/store": "0.11.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w=="], + "@tanstack/react-form/@tanstack/react-store": ["@tanstack/react-store@0.11.1", "", { "dependencies": { "@tanstack/store": "0.11.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ=="], "@tanstack/router-generator/prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], @@ -2896,6 +2934,10 @@ "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "every-plugin/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + + "every-plugin/@orpc/openapi": ["@orpc/openapi@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi-client": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-0tZN91VoT6MEkOfw+ERKyozsnDXzmDSsBeMgEHN3Hl1WVU97T9l4aZzLlZILSNl3fat3HmduEDy/boNGmAWJkQ=="], + "everything-dev/@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], "everything-dev/vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], @@ -2906,6 +2948,8 @@ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "host/@orpc/openapi": ["@orpc/openapi@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/interop": "1.14.3", "@orpc/openapi-client": "1.14.3", "@orpc/server": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "json-schema-typed": "^8.0.2", "rou3": "^0.7.12" } }, "sha512-0tZN91VoT6MEkOfw+ERKyozsnDXzmDSsBeMgEHN3Hl1WVU97T9l4aZzLlZILSNl3fat3HmduEDy/boNGmAWJkQ=="], + "hpack.js/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "http-proxy/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -2914,7 +2958,7 @@ "ink/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "ink/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "ink/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], @@ -2944,7 +2988,7 @@ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - "postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "postcss/nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -2956,7 +3000,7 @@ "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "read-yaml-file/js-yaml": ["js-yaml@3.15.0", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog=="], + "read-yaml-file/js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="], "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -2976,6 +3020,10 @@ "sockjs/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "solid-js/seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="], + + "solid-js/seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], @@ -2988,6 +3036,8 @@ "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "ui/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], "viem/@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], @@ -3000,8 +3050,6 @@ "viem/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], - "webpack/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], "webpack/tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], @@ -3012,7 +3060,7 @@ "webpack-dev-middleware/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "webpack-dev-server/ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + "webpack-dev-server/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -3086,9 +3134,47 @@ "@orpc/experimental-publisher/@orpc/client/@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.14.2", "", { "dependencies": { "@orpc/shared": "1.14.2", "@orpc/standard-server": "1.14.2" } }, "sha512-uzbgGaxvlZ0IA2lasaLck+yrbR3bKoqJnsZehEdbSm6eWaKln4COvxQJ2PuPow3gA6tklNnGkTfmRmGIHvS/rg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@orpc/json-schema/@orpc/openapi/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + + "@orpc/json-schema/@orpc/openapi/@orpc/openapi-client": ["@orpc/openapi-client@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-1vp+hi858XDrCwYdhONl15YKNlHtj5F5gI3dq/McRRZ45tZ9/Ma03hxzABOOcayaT1L9nX7gPxhX7l5EuRf2zw=="], + + "@orpc/openapi/@orpc/server/@orpc/standard-server-aws-lambda": ["@orpc/standard-server-aws-lambda@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-fetch": "1.15.0", "@orpc/standard-server-node": "1.15.0" } }, "sha512-laFW7C4t1gEV0NIt4EjCVCBU3kFPnj2FObJLQkTH+DkEOzxVy6zyJ1coIncDSeOPxzbf7rG8cs0B2vQQyuROag=="], + + "@orpc/openapi/@orpc/server/@orpc/standard-server-fastify": ["@orpc/standard-server-fastify@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-node": "1.15.0" }, "peerDependencies": { "fastify": ">=5.6.1" }, "optionalPeers": ["fastify"] }, "sha512-fcOl2004o3RnZxW7rhLuDCrCKSi/H2eWnyljeeoY+xykcygRf0ONCs2K2bSdxPm6cmvQGAXa9m8VBglG9oERVQ=="], + + "@orpc/openapi/@orpc/server/@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-XYVfgmIt71YrPJSI7RKRiNWjcyktPYHpevkjeohGNBP5aSMAUL95quZArAWr+XOPp4U56XRao54I6wFMZa4How=="], + + "@orpc/openapi/@orpc/server/@orpc/standard-server-node": ["@orpc/standard-server-node@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0", "@orpc/standard-server-fetch": "1.15.0" } }, "sha512-3ye9SIIhYfJckQwyyGQQRHAEF5vW6QDbax7oi1mOQ9UBHaRDH5kcL28mT0JnETrUvgBoDRx+rfCsQaAOB4n3zw=="], - "@tanstack/react-form/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.11.0", "", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="], + "@orpc/openapi/@orpc/server/@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.15.0", "", { "dependencies": { "@orpc/shared": "1.15.0", "@orpc/standard-server": "1.15.0" } }, "sha512-fsTN+FrdPkseVx99yRenGJYSp0xOrEk9fODk8oh3zzricNYzlZ8GGgCYtDNgt9vFIR/J05dc+Nk76KhcPBQpLw=="], + + "@orpc/zod/@orpc/openapi/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + + "@orpc/zod/@orpc/openapi/@orpc/openapi-client": ["@orpc/openapi-client@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-1vp+hi858XDrCwYdhONl15YKNlHtj5F5gI3dq/McRRZ45tZ9/Ma03hxzABOOcayaT1L9nX7gPxhX7l5EuRf2zw=="], + + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "@tanstack/react-form/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.11.1", "", {}, "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA=="], "@tanstack/zod-form-adapter/@tanstack/form-core/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="], @@ -3098,7 +3184,7 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "c12/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -3154,12 +3240,18 @@ "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "every-plugin/@orpc/openapi/@orpc/openapi-client": ["@orpc/openapi-client@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-1vp+hi858XDrCwYdhONl15YKNlHtj5F5gI3dq/McRRZ45tZ9/Ma03hxzABOOcayaT1L9nX7gPxhX7l5EuRf2zw=="], + "everything-dev/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "host/@orpc/openapi/@orpc/client": ["@orpc/client@1.14.3", "", { "dependencies": { "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3", "@orpc/standard-server-fetch": "1.14.3", "@orpc/standard-server-peer": "1.14.3" } }, "sha512-0HzeD/BgPctvFnd6ltjuQvx4/POXo0K01Tee/3whAm3ohXnlGqCfhzR2VMN8zBaGs1SYe6AFzjmGG928Ej3pAg=="], + + "host/@orpc/openapi/@orpc/openapi-client": ["@orpc/openapi-client@1.14.3", "", { "dependencies": { "@orpc/client": "1.14.3", "@orpc/contract": "1.14.3", "@orpc/shared": "1.14.3", "@orpc/standard-server": "1.14.3" } }, "sha512-1vp+hi858XDrCwYdhONl15YKNlHtj5F5gI3dq/McRRZ45tZ9/Ma03hxzABOOcayaT1L9nX7gPxhX7l5EuRf2zw=="], + "hpack.js/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "hpack.js/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -3180,50 +3272,12 @@ "serve-index/http-errors/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], "viem/@scure/bip32/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], "viem/@scure/bip39/@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], - "vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], - - "vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], - - "vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], - - "vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], - - "vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], - - "vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], - - "vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], - - "vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], - - "vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], - - "vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], - - "vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], - - "vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], - - "vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], - - "vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], - - "vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], - - "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], - - "vite/rolldown/@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], - - "vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], } } diff --git a/package.json b/package.json index 3c1d9266..2e44e3b6 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "better-auth": "1.6.25", "@better-auth/api-key": "1.6.25", "@better-auth/passkey": "1.6.25", - "better-near-auth": "1.8.3", + "better-near-auth": "1.10.0", "every-plugin": "^2.10.1", "everything-dev": "^1.53.0", "typescript": "^5.9.3", From 14a1a1996c3ec2c60868078bb556e9baf2ebbd72 Mon Sep 17 00:00:00 2001 From: Elliot Braem Date: Wed, 19 Aug 2026 15:08:12 -0500 Subject: [PATCH 18/24] skills --- .agents/skills/ask-matt/PHASE-BOUNDARIES.md | 55 +++++ .agents/skills/ask-matt/SKILL.md | 90 ++++++++ .agents/skills/ask-matt/agents/openai.yaml | 5 + .agents/skills/claude-handoff/SKILL.md | 18 ++ .../skills/claude-handoff/agents/openai.yaml | 5 + .agents/skills/code-review/SKILL.md | 87 ++++++++ .agents/skills/code-review/agents/openai.yaml | 3 + .agents/skills/codebase-design/DEEPENING.md | 37 +++ .../skills/codebase-design/DESIGN-IT-TWICE.md | 44 ++++ .agents/skills/codebase-design/SKILL.md | 114 ++++++++++ .../skills/codebase-design/agents/openai.yaml | 3 + .agents/skills/diagnosing-bugs/SKILL.md | 138 ++++++++++++ .../skills/diagnosing-bugs/agents/openai.yaml | 3 + .../scripts/hitl-loop.template.sh | 44 ++++ .agents/skills/domain-modeling/ADR-FORMAT.md | 47 ++++ .../skills/domain-modeling/CONTEXT-FORMAT.md | 60 +++++ .agents/skills/domain-modeling/SKILL.md | 74 ++++++ .../skills/domain-modeling/agents/openai.yaml | 3 + .../git-guardrails-claude-code/SKILL.md | 95 ++++++++ .../agents/openai.yaml | 3 + .../scripts/block-dangerous-git.sh | 25 +++ .agents/skills/grill-me/SKILL.md | 7 + .agents/skills/grill-me/agents/openai.yaml | 5 + .agents/skills/grill-with-docs/SKILL.md | 7 + .../skills/grill-with-docs/agents/openai.yaml | 5 + .agents/skills/grilling/SKILL.md | 22 ++ .agents/skills/grilling/agents/openai.yaml | 3 + .agents/skills/handoff/SKILL.md | 16 ++ .agents/skills/handoff/agents/openai.yaml | 5 + .agents/skills/implement/SKILL.md | 15 ++ .agents/skills/implement/agents/openai.yaml | 5 + .../HTML-REPORT.md | 123 ++++++++++ .../improve-codebase-architecture/SKILL.md | 71 ++++++ .../agents/openai.yaml | 5 + .agents/skills/loop-me/SKILL.md | 32 +++ .agents/skills/loop-me/agents/openai.yaml | 5 + .agents/skills/migrate-to-shoehorn/SKILL.md | 118 ++++++++++ .../migrate-to-shoehorn/agents/openai.yaml | 3 + .agents/skills/prototype/LOGIC.md | 67 ++++++ .agents/skills/prototype/SKILL.md | 26 +++ .agents/skills/prototype/UI.md | 112 ++++++++++ .agents/skills/prototype/agents/openai.yaml | 3 + .agents/skills/research/SKILL.md | 12 + .agents/skills/research/agents/openai.yaml | 3 + .../skills/resolving-merge-conflicts/SKILL.md | 14 ++ .../agents/openai.yaml | 3 + .agents/skills/scaffold-exercises/SKILL.md | 106 +++++++++ .../scaffold-exercises/agents/openai.yaml | 3 + .../skills/setup-matt-pocock-skills/SKILL.md | 116 ++++++++++ .../agents/openai.yaml | 5 + .../skills/setup-matt-pocock-skills/domain.md | 51 +++++ .../issue-tracker-github.md | 45 ++++ .../issue-tracker-gitlab.md | 46 ++++ .../issue-tracker-local.md | 30 +++ .../setup-matt-pocock-skills/triage-labels.md | 15 ++ .agents/skills/setup-pre-commit/SKILL.md | 91 ++++++++ .../setup-pre-commit/agents/openai.yaml | 3 + .agents/skills/setup-ts-deep-modules/SKILL.md | 102 +++++++++ .../setup-ts-deep-modules/agents/openai.yaml | 5 + .../dependency-cruiser.config.cjs | 95 ++++++++ .agents/skills/tdd/SKILL.md | 38 ++++ .agents/skills/tdd/agents/openai.yaml | 3 + .agents/skills/tdd/mocking.md | 59 +++++ .agents/skills/tdd/tests.md | 77 +++++++ .agents/skills/teach/GLOSSARY-FORMAT.md | 35 +++ .../skills/teach/LEARNING-RECORD-FORMAT.md | 46 ++++ .agents/skills/teach/MISSION-FORMAT.md | 31 +++ .agents/skills/teach/RESOURCES-FORMAT.md | 32 +++ .agents/skills/teach/SKILL.md | 140 ++++++++++++ .agents/skills/teach/agents/openai.yaml | 5 + .agents/skills/to-questionnaire/SKILL.md | 54 +++++ .../to-questionnaire/agents/openai.yaml | 5 + .agents/skills/to-spec/SKILL.md | 75 +++++++ .agents/skills/to-spec/agents/openai.yaml | 5 + .agents/skills/to-tickets/SKILL.md | 105 +++++++++ .agents/skills/to-tickets/agents/openai.yaml | 5 + .agents/skills/triage/AGENT-BRIEF.md | 207 +++++++++++++++++ .agents/skills/triage/OUT-OF-SCOPE.md | 105 +++++++++ .agents/skills/triage/SKILL.md | 112 ++++++++++ .agents/skills/triage/agents/openai.yaml | 5 + .agents/skills/wait-what/SKILL.md | 7 + .agents/skills/wait-what/agents/openai.yaml | 5 + .agents/skills/wayfinder/SKILL.md | 128 +++++++++++ .agents/skills/wayfinder/agents/openai.yaml | 5 + .agents/skills/wizard/SKILL.md | 44 ++++ .agents/skills/wizard/agents/openai.yaml | 3 + .agents/skills/wizard/template.sh | 204 +++++++++++++++++ .agents/skills/writing-beats/SKILL.md | 67 ++++++ .../skills/writing-beats/agents/openai.yaml | 5 + .../writing-for-agents/SKILL-MECHANICS.md | 22 ++ .agents/skills/writing-for-agents/SKILL.md | 81 +++++++ .../writing-for-agents/agents/openai.yaml | 3 + .agents/skills/writing-fragments/SKILL.md | 79 +++++++ .../writing-fragments/agents/openai.yaml | 5 + .env.example | 3 +- bos.config.json | 52 ++--- bun.lock | 88 +------- package.json | 2 +- skills-lock.json | 210 ++++++++++++++++++ ui/src/app.ts | 1 - ui/src/components/index.ts | 1 + ui/src/components/user-nav.tsx | 17 +- ui/src/lib/use-relayer-fund.ts | 111 --------- ui/src/lib/use-relayer.ts | 21 ++ ui/src/routes/_layout/_admin/admin.tsx | 14 +- ui/src/routes/_layout/_admin/admin/index.tsx | 16 +- .../routes/_layout/_admin/admin/relayer.tsx | 95 ++++++-- ui/src/routes/_layout/_anon.tsx | 5 - ui/src/routes/_layout/_anon/login.tsx | 4 + .../routes/_layout/_authenticated/stake.tsx | 1 + 110 files changed, 4526 insertions(+), 270 deletions(-) create mode 100644 .agents/skills/ask-matt/PHASE-BOUNDARIES.md create mode 100644 .agents/skills/ask-matt/SKILL.md create mode 100644 .agents/skills/ask-matt/agents/openai.yaml create mode 100644 .agents/skills/claude-handoff/SKILL.md create mode 100644 .agents/skills/claude-handoff/agents/openai.yaml create mode 100644 .agents/skills/code-review/SKILL.md create mode 100644 .agents/skills/code-review/agents/openai.yaml create mode 100644 .agents/skills/codebase-design/DEEPENING.md create mode 100644 .agents/skills/codebase-design/DESIGN-IT-TWICE.md create mode 100644 .agents/skills/codebase-design/SKILL.md create mode 100644 .agents/skills/codebase-design/agents/openai.yaml create mode 100644 .agents/skills/diagnosing-bugs/SKILL.md create mode 100644 .agents/skills/diagnosing-bugs/agents/openai.yaml create mode 100644 .agents/skills/diagnosing-bugs/scripts/hitl-loop.template.sh create mode 100644 .agents/skills/domain-modeling/ADR-FORMAT.md create mode 100644 .agents/skills/domain-modeling/CONTEXT-FORMAT.md create mode 100644 .agents/skills/domain-modeling/SKILL.md create mode 100644 .agents/skills/domain-modeling/agents/openai.yaml create mode 100644 .agents/skills/git-guardrails-claude-code/SKILL.md create mode 100644 .agents/skills/git-guardrails-claude-code/agents/openai.yaml create mode 100755 .agents/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh create mode 100644 .agents/skills/grill-me/SKILL.md create mode 100644 .agents/skills/grill-me/agents/openai.yaml create mode 100644 .agents/skills/grill-with-docs/SKILL.md create mode 100644 .agents/skills/grill-with-docs/agents/openai.yaml create mode 100644 .agents/skills/grilling/SKILL.md create mode 100644 .agents/skills/grilling/agents/openai.yaml create mode 100644 .agents/skills/handoff/SKILL.md create mode 100644 .agents/skills/handoff/agents/openai.yaml create mode 100644 .agents/skills/implement/SKILL.md create mode 100644 .agents/skills/implement/agents/openai.yaml create mode 100644 .agents/skills/improve-codebase-architecture/HTML-REPORT.md create mode 100644 .agents/skills/improve-codebase-architecture/SKILL.md create mode 100644 .agents/skills/improve-codebase-architecture/agents/openai.yaml create mode 100644 .agents/skills/loop-me/SKILL.md create mode 100644 .agents/skills/loop-me/agents/openai.yaml create mode 100644 .agents/skills/migrate-to-shoehorn/SKILL.md create mode 100644 .agents/skills/migrate-to-shoehorn/agents/openai.yaml create mode 100644 .agents/skills/prototype/LOGIC.md create mode 100644 .agents/skills/prototype/SKILL.md create mode 100644 .agents/skills/prototype/UI.md create mode 100644 .agents/skills/prototype/agents/openai.yaml create mode 100644 .agents/skills/research/SKILL.md create mode 100644 .agents/skills/research/agents/openai.yaml create mode 100644 .agents/skills/resolving-merge-conflicts/SKILL.md create mode 100644 .agents/skills/resolving-merge-conflicts/agents/openai.yaml create mode 100644 .agents/skills/scaffold-exercises/SKILL.md create mode 100644 .agents/skills/scaffold-exercises/agents/openai.yaml create mode 100644 .agents/skills/setup-matt-pocock-skills/SKILL.md create mode 100644 .agents/skills/setup-matt-pocock-skills/agents/openai.yaml create mode 100644 .agents/skills/setup-matt-pocock-skills/domain.md create mode 100644 .agents/skills/setup-matt-pocock-skills/issue-tracker-github.md create mode 100644 .agents/skills/setup-matt-pocock-skills/issue-tracker-gitlab.md create mode 100644 .agents/skills/setup-matt-pocock-skills/issue-tracker-local.md create mode 100644 .agents/skills/setup-matt-pocock-skills/triage-labels.md create mode 100644 .agents/skills/setup-pre-commit/SKILL.md create mode 100644 .agents/skills/setup-pre-commit/agents/openai.yaml create mode 100644 .agents/skills/setup-ts-deep-modules/SKILL.md create mode 100644 .agents/skills/setup-ts-deep-modules/agents/openai.yaml create mode 100644 .agents/skills/setup-ts-deep-modules/dependency-cruiser.config.cjs create mode 100644 .agents/skills/tdd/SKILL.md create mode 100644 .agents/skills/tdd/agents/openai.yaml create mode 100644 .agents/skills/tdd/mocking.md create mode 100644 .agents/skills/tdd/tests.md create mode 100644 .agents/skills/teach/GLOSSARY-FORMAT.md create mode 100644 .agents/skills/teach/LEARNING-RECORD-FORMAT.md create mode 100644 .agents/skills/teach/MISSION-FORMAT.md create mode 100644 .agents/skills/teach/RESOURCES-FORMAT.md create mode 100644 .agents/skills/teach/SKILL.md create mode 100644 .agents/skills/teach/agents/openai.yaml create mode 100644 .agents/skills/to-questionnaire/SKILL.md create mode 100644 .agents/skills/to-questionnaire/agents/openai.yaml create mode 100644 .agents/skills/to-spec/SKILL.md create mode 100644 .agents/skills/to-spec/agents/openai.yaml create mode 100644 .agents/skills/to-tickets/SKILL.md create mode 100644 .agents/skills/to-tickets/agents/openai.yaml create mode 100644 .agents/skills/triage/AGENT-BRIEF.md create mode 100644 .agents/skills/triage/OUT-OF-SCOPE.md create mode 100644 .agents/skills/triage/SKILL.md create mode 100644 .agents/skills/triage/agents/openai.yaml create mode 100644 .agents/skills/wait-what/SKILL.md create mode 100644 .agents/skills/wait-what/agents/openai.yaml create mode 100644 .agents/skills/wayfinder/SKILL.md create mode 100644 .agents/skills/wayfinder/agents/openai.yaml create mode 100644 .agents/skills/wizard/SKILL.md create mode 100644 .agents/skills/wizard/agents/openai.yaml create mode 100644 .agents/skills/wizard/template.sh create mode 100644 .agents/skills/writing-beats/SKILL.md create mode 100644 .agents/skills/writing-beats/agents/openai.yaml create mode 100644 .agents/skills/writing-for-agents/SKILL-MECHANICS.md create mode 100644 .agents/skills/writing-for-agents/SKILL.md create mode 100644 .agents/skills/writing-for-agents/agents/openai.yaml create mode 100644 .agents/skills/writing-fragments/SKILL.md create mode 100644 .agents/skills/writing-fragments/agents/openai.yaml delete mode 100644 ui/src/lib/use-relayer-fund.ts create mode 100644 ui/src/lib/use-relayer.ts diff --git a/.agents/skills/ask-matt/PHASE-BOUNDARIES.md b/.agents/skills/ask-matt/PHASE-BOUNDARIES.md new file mode 100644 index 00000000..fb58ef9f --- /dev/null +++ b/.agents/skills/ask-matt/PHASE-BOUNDARIES.md @@ -0,0 +1,55 @@ +# Phase boundaries + +A **phase** is a chunk of work inside a session: the grilling, the implementation, the QA. The definition is fuzzy on purpose: a phase ends when you think *"ok, we're done with that"*. + +The **phase boundary** is the gap between two phases, and it is the only place this decision belongs. Mid-phase there is no decision to make: continue, or split the work that's left into subagents. Compacting mid-phase makes the agent lose the thread. + +## The five options + +| Option | What it does | +| ------------ | --------------------------------------------------------------- | +| **Continue** | Stay in the session. No context switch at all. | +| **`/clear`** | Empty the context window and start from nothing. | +| **`/handoff`** | Write a portable markdown file and seed a session anywhere with it. | +| **Subagent** | Send the task to its own context window and get a report back. | +| **`/compact`** | Compress this context and seed a fresh session with the summary. | + +## The tree + +Work top to bottom at the boundary. The first **yes** wins. + +**1. Can you continue in this session?** Two things make the answer yes: the next phase needs this phase as a **primary source**, or you have enough [smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone) left (~150k tokens) for the next phase to fit. Grilling → implementation is the standard yes: the implementation wants the reasoning verbatim, not a summary of it. Continue costs nothing and loses nothing, so rule it out before anything else. + +**2. Is the context irrelevant to what comes next?** Is everything in this session (the exploration, the decisions, the dead ends) disposable? If so, **`/clear`**. It is the cheapest move on the board: it takes no time and hands back the whole window. `/clear` also isn't terminal: the old session stays resumable. + +The cost of getting this wrong is one-way. Clear a *relevant* context and you lose the **why** behind what you built, and no amount of reading the diff back gets it returned. + +**3. Do you need to hand off?** `/handoff` is narrow. You need it only when you are: + +- swapping to a **new harness** (Claude → Codex), +- moving to a **new directory** or repo, +- sending the work to a **colleague**, +- or forking a side task you found **mid-phase** without derailing what you're doing. + +That list is the whole clause. What `/handoff` buys is **portability**: a file that travels. If nothing is travelling, you don't need it. + +**4. Can the task be done AFK?** Is it scoped tightly enough to run with you away from the keyboard, no steering? Then send it to a **subagent** and leave this session untouched. Automated review is the standard case: the agent reads the diff and reports, and you aren't needed while it does. + +**5. Otherwise, `/compact`.** Relevant context, same harness, same directory, and you need to stay in the loop: this is where the tree lands, and it lands here often. Pass it an instruction (`/compact we're going to QA this area`) so the summary keeps what the next phase needs. + +`/compact` is the **default, not the first reach**. It sits at the bottom because the four questions above it are all cheaper or more precise. The failure mode when people start here is a fresh session that is confidently wrong about a decision the summary flattened. + +## Primary and secondary sources + +Every move except **Continue** turns a **primary source** into a **secondary source**: the session as it happened, replaced by a summary of it. The trade is always the same shape: + +| Source | Information | Noise | Room to move | +| --------------------------------- | ----------- | ----- | ------------ | +| Primary (Continue) | Full | Lots | Little | +| Secondary (`/compact`, `/handoff`) | Lossy | Less | Lots | + +This is why question 1 comes first. You only pay the lossiness when staying costs more than it saves. + +## These are judgement calls + +The questions are not objective: each has taste in it, and the same boundary can go two ways on two days. The value is in asking them **in order**, at the boundary rather than in the middle of the work. diff --git a/.agents/skills/ask-matt/SKILL.md b/.agents/skills/ask-matt/SKILL.md new file mode 100644 index 00000000..ae8eb9b2 --- /dev/null +++ b/.agents/skills/ask-matt/SKILL.md @@ -0,0 +1,90 @@ +--- +name: ask-matt +description: Ask which skill or flow fits your situation. A router over the skills in this repo. +disable-model-invocation: true +--- + +# Ask Matt + +You don't remember every skill, so ask. + +A **flow** is a path through the skills. Most paths run along one **main flow**, and two **on-ramps** merge onto it. Everything else is standalone, or a vocabulary layer that runs underneath. + +## The main flow: idea → ship + +The route most work travels. You have an idea and want it built. + +1. **`/grill-with-docs`** sharpens the idea by interview. Start here whenever you are **working in a working directory**: it's stateful, retaining what it learns in `CONTEXT.md` and ADRs. (No working directory? Use `/grill-me` instead, covered under Standalone. Both run the same `/grilling` primitive; `grill-with-docs` is the one that leaves a paper trail, which makes it the better of the two whenever a repo is there to leave it in.) +2. **Branch: can you settle every question in conversation?** If a question needs a runnable answer (state, business logic, a UI you have to see), detour through a prototype, bridged by **`/handoff`** in both directions (a prototype lives in its own directory, which is exactly what `/handoff` is for; see Phase boundaries): + - **`/handoff`** out, then open a fresh session against that file, + - **`/prototype`** to answer the question with throwaway code, + - **`/handoff`** back what you learned, and reference it from the original idea thread. +3. **Branch: is this a multi-session build?** + - **Yes** → **`/to-spec`** (turn the thread into a spec), then **`/to-tickets`** to split it into tracer-bullet tickets, each declaring its **blocking edges**. On a local tracker that's one file per ticket under `.scratch//issues/`, worked blockers-first by hand; on a real tracker the edges become native blocking links, so any ticket whose blockers are done can be grabbed: kick off **`/implement`** per ticket, **`/clear`ing context between each one**. Each ticket is self-contained, so the last one's context is disposable. + - **No** → **`/implement`** right here, in the same context window. + + Either way, **`/implement`** builds each issue by driving **`/tdd`** internally (one red-green slice at a time), then closes out by running **`/code-review`**, a two-axis review (Standards + Spec) of the diff, before committing. Reach for **`/tdd`** on its own when you just want to build a concrete behaviour test-first without a full spec, and **`/code-review`** on its own whenever you want to review a branch or PR against a fixed point. + +### Context hygiene + +Keep steps 1–3 in **one unbroken context window** (don't compact or clear until after `/to-tickets`) so the grilling, spec, and tickets all build on the same thinking. Each `/implement` then starts fresh, working from the ticket. + +The limit on this is the **[smart zone](https://www.aihero.dev/ai-coding-dictionary/smart-zone)**: the window (~150k tokens on state-of-the-art models) within which the model still reasons sharply. If a session approaches it before `/to-tickets`, don't push on degraded; `/compact` at the nearest phase boundary and carry on (see Phase boundaries). + +## On-ramps + +A starting situation that generates work, then merges onto the main flow. + +- **Bugs and requests piling up** → **`/triage`**. It moves issues through triage roles and produces agent-ready issues, which **`/implement`** later picks up. + + Triage is only for issues **you didn't create**: bug reports, incoming feature requests, anything that arrives raw. Tickets that `/to-tickets` produced are already agent-ready, so **don't triage them**. + +- **Something's broken** → **`/diagnosing-bugs`**. For the hard ones: the bug that resists a first glance, the intermittent flake, the regression that crept in between two known-good states. It refuses to theorise until it has a **tight feedback loop** (one command that already goes red on *this* bug), then fixes with a regression test. Its post-mortem hands off to **`/improve-codebase-architecture`** when the real finding is that there's no good seam to lock the bug down. + +- **A huge, foggy effort: a greenfield project or a huge feature build, too big for one session** → **`/wayfinder`**, the most cognitively demanding flow here. When the way from here to the destination isn't visible yet, it charts a **shared map** of **decision tickets** on the issue tracker and resolves them one at a time, producing **decisions, not deliverables**, until the fog is pushed back and the way is clear. Where **`/grill-with-docs`** sharpens an idea you can hold in one session, wayfinder is for the idea you can't, and it's slower and denser, so save it for exactly that, never a well-scoped feature. + + When the map clears, **it hands off, it doesn't build**: merge onto the main flow at **`/to-spec`**, which collapses the map's linked decisions into a buildable plan, then `/to-tickets` and `/implement` as usual. Looping the map straight into `/implement` skips that collapse and throws the linked detail away, so go straight to `/implement` only when the effort turned out genuinely small. + +## Codebase health + +Not feature work, just upkeep. + +- **`/improve-codebase-architecture`** runs whenever you have a spare moment to keep the codebase good for agents to operate in. It surfaces **deepening opportunities**; picking one _generates an idea_ you can take into the main flow at `/grill-with-docs`. It's the survey that finds the candidates; **`/codebase-design`** (below) is the bench you design the chosen one on. + +## Vocabulary underneath + +Two model-invoked references that run *beneath* the other skills, each the single source of truth for its vocabulary. Reach for them directly when the **words**, not the process, are the problem; or let the skills above pull them in. + +- **`/domain-modeling`**: sharpen the project's *domain* language: challenge a fuzzy term, resolve an overloaded word ("account" doing three jobs), record a hard-to-reverse decision as an ADR. It's the active discipline `/grill-with-docs` drives to keep `CONTEXT.md` a clean glossary. +- **`/codebase-design`** is the deep-module vocabulary (module, interface, depth, seam, adapter, leverage, locality) for designing a module's *shape*: a lot of behaviour behind a small interface at a clean seam. `/tdd` and `/improve-codebase-architecture` both speak it. + +## Phase boundaries + +A **phase** is a chunk of work inside a session: the grilling, the implementation, the QA. At the **boundary** between two of them you have five options, and picking between them is the fuzziest decision in this whole map: + +- **Continue**: stay put. Costs nothing, loses nothing. +- **`/clear`**: empty the window, when nothing here matters to what's next. +- **`/handoff`** writes a portable markdown file. Narrow: only for a **new harness**, a **new directory**, a **colleague**, or forking a side task **mid-phase**. What it buys is portability. +- **Subagent**: send a tightly-scoped task to its own window and get a report back. +- **`/compact`** compresses this context and seeds a fresh session with it. The **default**, at the bottom of the tree rather than the first reach. + +Read [PHASE-BOUNDARIES.md](PHASE-BOUNDARIES.md) for the ordered tree: the five questions, the reasoning behind each branch, and why the primary-source cost makes **Continue** the one to rule out first. Make the decision **at** a boundary; mid-phase, continue or split the rest into subagents. + +## Standalone + +Off the main flow entirely. + +- **`/grill-me`**: the same relentless interview as `/grill-with-docs`, but **stateless**: it saves nothing locally and builds no `CONTEXT.md`. Reach for it when you are **not working in a working directory** (sharpening a plan, a design, a piece of writing, anything with no repo under it). If you are in a working directory, use `/grill-with-docs` instead: it runs the same interview and leaves a paper trail, so it is strictly the better one. +- **`/grilling`** is the interview primitive itself: rounds, the frontier, facts are the agent's job and decisions are yours. `/grill-me` and `/grill-with-docs` are the two named ways in, and `/triage`, `/wayfinder` and `/improve-codebase-architecture` all run it internally. Reach for it directly only when you want the interview with no wrapper around it. +- **`/resolving-merge-conflicts`** works an in-progress merge or rebase conflict hunk by hunk, resolving by **intent** traced to each side's primary source rather than by picking lines, then finishes the operation. It never runs `--abort`. Standalone and off every flow: reach for it when you are already mid-conflict. +- **`/prototype`** is a small, throwaway program that answers one design question: does this state model feel right, or what should this UI look like. Throwaway is a constraint on how the code is written, not a promise to destroy it: the answer folds into the real code, and the prototype itself is kept as a **primary source** on a `prototype/` branch out of main, pointed at from the implementation issue. It's the detour in step 2 of the main flow, but reach for it any time a design question is hard to settle on paper. +- **`/research`**: delegate reading legwork to a **background agent**: it investigates a question against **primary sources**, then leaves a cited Markdown file in the repo. Keep working while it reads. The file it produces is something to take *into* the main flow at `/grill-with-docs`, since research feeds the thinking rather than replacing it. +- **`/to-questionnaire`** comes in when the thing blocking you isn't in your head or the codebase but in **someone else's**, and it writes them a questionnaire to fill in. It's the inverse of `/grill-me`: instead of interviewing you about the subject, it interviews you about the **send** (who it's going to, what you need back) and aims the questions at the gap. What comes back is material for `/grill-with-docs` or `/to-spec`. +- **`/wizard`** is for the steps only a **human** can take: provisioning infrastructure, setting up credentials or CI secrets, clicking through an unfamiliar third-party dashboard, running a one-off migration or cutover. It generates an interactive bash script that opens each URL, captures each value, and writes it into `.env` and GitHub secrets, so the procedure stops being something you re-explain to an agent every time. Model-invoked, so the agent reaches for it the moment it hits a wall only you can pass. If the agent could just do it itself, it should; this is for where a human is genuinely in the loop. +- **`/wait-what`** is the corrective for a message that didn't land. Use it mid-conversation, inside any other skill, and the agent re-pitches what it just said with the context you were missing, in plain English, using the `CONTEXT.md` vocabulary. It works after the fact; `/grill-with-docs` is the upfront cure, because a shared language agreed early is what stops the jargon arriving at all. +- **`/teach`**: learn a concept over multiple sessions, using the current directory as a stateful workspace. +- **`/writing-for-agents`** is the reference for writing documents agents consume: skills, AGENTS.md, pointed-at docs. + +## Precondition + +**`/setup-matt-pocock-skills`**: run before your first engineering flow to configure the issue tracker, triage labels, and doc layout the other skills assume. Custom issue trackers also work. diff --git a/.agents/skills/ask-matt/agents/openai.yaml b/.agents/skills/ask-matt/agents/openai.yaml new file mode 100644 index 00000000..5c60d51b --- /dev/null +++ b/.agents/skills/ask-matt/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Ask Matt" + short_description: "Find the right skill or workflow" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/claude-handoff/SKILL.md b/.agents/skills/claude-handoff/SKILL.md new file mode 100644 index 00000000..9ab14e31 --- /dev/null +++ b/.agents/skills/claude-handoff/SKILL.md @@ -0,0 +1,18 @@ +--- +name: claude-handoff +description: Hand the current conversation off to a fresh background agent that picks up the work immediately. +argument-hint: "What will the next session be used for?" +disable-model-invocation: true +--- + +Write a handoff summary of the current conversation so a fresh agent can continue the work. Instead of saving it, launch a background agent seeded with the summary as its prompt: `claude --bg --name "" ""`. It starts in the current working directory and returns immediately; the user manages it with `claude agents`. + +Always pass `-n`/`--name` with a descriptive name (e.g. `--name "Fix login bug"`); it sets the display name shown in the job list, session picker, and terminal title. + +Include a "suggested skills" section in the summary, naming which skills the next agent should call the Skill tool for. + +Do not duplicate content already captured in other artifacts (specs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. + +Redact any sensitive information, such as API keys, passwords, or personally identifiable information, since the summary becomes the agent's prompt. + +If the user passed arguments, treat them as a description of what the next session will focus on and tailor the summary accordingly. diff --git a/.agents/skills/claude-handoff/agents/openai.yaml b/.agents/skills/claude-handoff/agents/openai.yaml new file mode 100644 index 00000000..0a7aa5da --- /dev/null +++ b/.agents/skills/claude-handoff/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Claude Handoff" + short_description: "Hand off to a background agent" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md new file mode 100644 index 00000000..e28d7acb --- /dev/null +++ b/.agents/skills/code-review/SKILL.md @@ -0,0 +1,87 @@ +--- +name: code-review +description: "Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes: Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/spec asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to \"review since X\"." +--- + +Two-axis review of the diff between `HEAD` and a fixed point the user supplies: + +- **Standards**: does the code conform to this repo's documented coding standards? +- **Spec**: does the code faithfully implement the originating issue / spec? + +Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings. + +The issue tracker should have been provided to you. If `docs/agents/issue-tracker.md` is missing, tell the user to run `/setup-matt-pocock-skills`. + +## Process + +### 1. Pin the fixed point + +Whatever the user said is the fixed point (a commit SHA, branch name, tag, `main`, `HEAD~5`, etc.). If they didn't specify one, ask for it. + +Capture the diff command once: `git diff ...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log ..HEAD --oneline`. + +Before going further, confirm the fixed point resolves (`git rev-parse `) and the diff is non-empty. A bad ref or empty diff should fail here, not inside two parallel sub-agents. + +### 2. Identify the spec source + +Look for the originating spec, in this order: + +1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.), fetched via the workflow in `docs/agents/issue-tracker.md`. +2. A path the user passed as an argument. +3. A spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature. +4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available". + +### 3. Identify the standards sources + +Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`. + +On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below: a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it: + +- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell. +- **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation. Like any standard here, skip anything tooling already enforces. + +Each smell reads *what it is* → *how to fix*; match it against the diff: + +- **Mysterious Name**: a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky. +- **Duplicated Code**: the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both. +- **Feature Envy**: a method that reaches into another object's data more than its own. → move the method onto the data it envies. +- **Data Clumps**: the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that. +- **Primitive Obsession**: a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type. +- **Repeated Switches**: the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share. +- **Shotgun Surgery**: one logical change forces scattered edits across many files in the diff. → gather what changes together into one module. +- **Divergent Change**: one file or module is edited for several unrelated reasons. → split so each module changes for one reason. +- **Speculative Generality**: abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows. +- **Message Chains**: long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object. +- **Middle Man**: a class or function that mostly just delegates onward. → cut it, call the real target direct. +- **Refused Bequest**: a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition. + +### 4. Spawn both sub-agents in parallel + +**Standards sub-agent prompt** should include: + +- The full diff command and commit list. +- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full (the sub-agent has no other access to it). +- The brief: "Report, per file/hunk where relevant, (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls: documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words." + +**Spec sub-agent prompt** should include: + +- The diff command and commit list. +- The path or fetched contents of the spec. +- The brief: "Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words." + +If the spec is missing, skip the Spec sub-agent and note this in the final report. + +### 5. Aggregate + +Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings, because the two axes are deliberately separate (see _Why two axes_). + +End with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes: that's the reranking the separation exists to prevent. + +## Why two axes + +A change can pass one axis and fail the other: + +- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.** +- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.** + +Reporting them separately stops one axis from masking the other. diff --git a/.agents/skills/code-review/agents/openai.yaml b/.agents/skills/code-review/agents/openai.yaml new file mode 100644 index 00000000..9076774b --- /dev/null +++ b/.agents/skills/code-review/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Code Review" + short_description: "Review a diff on standards and spec" diff --git a/.agents/skills/codebase-design/DEEPENING.md b/.agents/skills/codebase-design/DEEPENING.md new file mode 100644 index 00000000..cd94075c --- /dev/null +++ b/.agents/skills/codebase-design/DEEPENING.md @@ -0,0 +1,37 @@ +# Deepening + +How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](SKILL.md): **module**, **interface**, **seam**, **adapter**. + +## Dependency categories + +When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. + +### 1. In-process + +Pure computation, in-memory state, no I/O. Always deepenable: merge the modules and test through the new interface directly. No adapter needed. + +### 2. Local-substitutable + +Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface. + +### 3. Remote but owned (Ports & Adapters) + +Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter. + +Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."* + +### 4. True external (Mock) + +Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. + +## Seam discipline + +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection. +- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. + +## Testing strategy: replace, don't layer + +- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist; delete them. +- Write new tests at the deepened module's interface. The **interface is the test surface**. +- Tests assert on observable outcomes through the interface, not internal state. +- Tests should survive internal refactors, since they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface. diff --git a/.agents/skills/codebase-design/DESIGN-IT-TWICE.md b/.agents/skills/codebase-design/DESIGN-IT-TWICE.md new file mode 100644 index 00000000..7edc861a --- /dev/null +++ b/.agents/skills/codebase-design/DESIGN-IT-TWICE.md @@ -0,0 +1,44 @@ +# Design It Twice + +When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout): your first idea is unlikely to be the best. + +Uses the vocabulary in [SKILL.md](SKILL.md): **module**, **interface**, **seam**, **adapter**, **leverage**. + +## Process + +### 1. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md)) +- A rough illustrative code sketch to ground the constraints, not a proposal, just a way to make the constraints concrete + +Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. + +### 2. Spawn sub-agents + +Spawn 3+ sub-agents in parallel. Each must produce a **radically different** interface for the deepened module. + +Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: + +- Agent 1: "Minimize the interface: aim for 1–3 entry points max. Maximise leverage per entry point." +- Agent 2: "Maximise flexibility: support many use cases and extension." +- Agent 3: "Optimise for the most common caller: make the default case trivial." +- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies." + +Include both [SKILL.md](SKILL.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language. + +Each sub-agent outputs: + +1. Interface (types, methods, params, plus invariants, ordering, error modes) +2. Usage example showing how callers use it +3. What the implementation hides behind the seam +4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md)) +5. Trade-offs: where leverage is high, where it's thin + +### 3. Present and compare + +Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. + +After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated: the user wants a strong read, not a menu. diff --git a/.agents/skills/codebase-design/SKILL.md b/.agents/skills/codebase-design/SKILL.md new file mode 100644 index 00000000..3f63c814 --- /dev/null +++ b/.agents/skills/codebase-design/SKILL.md @@ -0,0 +1,114 @@ +--- +name: codebase-design +description: Shared vocabulary for designing deep modules. Use when the user wants to design or improve a module's interface, find deepening opportunities, decide where a seam goes, make code more testable or AI-navigable, or when another skill needs the deep-module vocabulary. +--- + +# Codebase Design + +Design **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone. + +## Glossary + +Use these terms exactly: don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point. + +**Module**: anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service. + +**Interface**: everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow, they refer only to the type-level surface). + +**Implementation**: what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. + +**Depth**: leverage at the interface. The amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation. + +**Seam** _(Michael Feathers)_: a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context). + +**Adapter**: a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside). + +**Leverage**: what callers get from depth. More capability per unit of interface they learn. One implementation pays back across N call sites and M tests. + +**Locality**: what maintainers get from depth. Change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere. + +## Deep vs shallow + +**Deep module** = small interface + lots of implementation: + +``` +┌─────────────────────┐ +│ Small Interface │ ← Few methods, simple params +├─────────────────────┤ +│ │ +│ Deep Implementation│ ← Complex logic hidden +│ │ +└─────────────────────┘ +``` + +**Shallow module** = large interface + little implementation (avoid): + +``` +┌─────────────────────────────────┐ +│ Large Interface │ ← Many methods, complex params +├─────────────────────────────────┤ +│ Thin Implementation │ ← Just passes through +└─────────────────────────────────┘ +``` + +When designing an interface, ask: + +- Can I reduce the number of methods? +- Can I simplify the parameters? +- Can I hide more complexity inside? + +## Principles + +- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts; they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface. +- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. +- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape. +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. + +## Designing for testability + +Good interfaces make testing natural: + +1. **Accept dependencies, don't create them.** + + ```typescript + // Testable + function processOrder(order, paymentGateway) {} + + // Hard to test + function processOrder(order) { + const gateway = new StripeGateway(); + } + ``` + +2. **Return results, don't produce side effects.** + + ```typescript + // Testable + function calculateDiscount(cart): Discount {} + + // Hard to test + function applyDiscount(cart): void { + cart.total -= discount; + } + ``` + +3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup. + +## Relationships + +- A **Module** has exactly one **Interface** (the surface it presents to callers and tests). +- **Depth** is a property of a **Module**, measured against its **Interface**. +- A **Seam** is where a **Module**'s **Interface** lives. +- An **Adapter** sits at a **Seam** and satisfies the **Interface**. +- **Depth** produces **Leverage** for callers and **Locality** for maintainers. + +## Rejected framings + +- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead. +- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow: interface here includes every fact a caller must know. +- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. + +## Going deeper + +- **Deepening a cluster given its dependencies**, see [DEEPENING.md](DEEPENING.md): dependency categories, seam discipline, and replace-don't-layer testing. +- **Exploring alternative interfaces**, see [DESIGN-IT-TWICE.md](DESIGN-IT-TWICE.md): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement. diff --git a/.agents/skills/codebase-design/agents/openai.yaml b/.agents/skills/codebase-design/agents/openai.yaml new file mode 100644 index 00000000..3180715e --- /dev/null +++ b/.agents/skills/codebase-design/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Codebase Design" + short_description: "Vocabulary for deep-module design" diff --git a/.agents/skills/diagnosing-bugs/SKILL.md b/.agents/skills/diagnosing-bugs/SKILL.md new file mode 100644 index 00000000..061c25a5 --- /dev/null +++ b/.agents/skills/diagnosing-bugs/SKILL.md @@ -0,0 +1,138 @@ +--- +name: diagnosing-bugs +description: Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow. +--- + +# Diagnosing Bugs + +A discipline for hard bugs. Skip phases only when explicitly justified. + +When exploring the codebase, read `CONTEXT.md` (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. + +## Redact + +This skill has you show commands, outputs and captured artifacts. **Redact every secret first**: write `` in its place. Build loops against env vars, so the credential stays in the environment rather than in what you show. Captured artifacts carry auth headers: quote only the lines that carry the signal. + +If the redacted output is not enough to diagnose the bug, say so and ask the user. + +## Phase 1: Build a feedback loop + +**This is the skill.** Everything else is mechanical. If you have a **tight** pass/fail signal for the bug (one that goes red on _this_ bug), you will find the cause; bisection, hypothesis-testing, and instrumentation all just consume it. If you don't have one, no amount of staring at code will save you. + +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** + +### Ways to construct one, in roughly this order + +1. **Failing test** at whatever seam reaches the bug: unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) that drives the UI and asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **HITL bash script.** Last resort. If a human must click, drive _them_ with `scripts/hitl-loop.template.sh` so the loop is still structured. Captured output feeds back to you. + +Build the right feedback loop, and the bug is 90% fixed. + +### Tighten the loop + +Treat the loop as a product. Once you have _a_ loop, **tighten** it: + +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) + +A 30-second flaky loop is barely better than no loop; a 2-second deterministic one is tight, a debugging superpower. + +### Non-deterministic bugs + +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not, so keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a redacted captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +### Completion criterion: a tight loop that goes red + +Phase 1 is done when the loop is **tight** and **red-capable**: you can name **one command** (a script path, a test invocation, a curl) that you have **already run at least once** (show the invocation and its output, redacted), and that is: + +- [ ] **Red-capable**: it drives the actual bug code path and asserts the **user's exact symptom**, so it can go red on this bug and green once fixed. Not "runs without erroring"; it must be able to _catch this specific bug_. +- [ ] **Deterministic**: same verdict every run (flaky bugs: a pinned, high reproduction rate, per above). +- [ ] **Fast**: seconds, not minutes. +- [ ] **Agent-runnable**: you can run it unattended; a human in the loop only via `scripts/hitl-loop.template.sh`. + +If you catch yourself reading code to build a theory before this command exists, **stop: jumping straight to a hypothesis is the exact failure this skill prevents.** No red-capable command, no Phase 2. + +## Phase 2: Reproduce + minimise + +Run the loop. Watch it go red as the bug appears. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described, not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +### Minimise + +Once it's red, shrink the repro to the **smallest scenario that still goes red**. Cut inputs, callers, config, data, and steps **one at a time**, re-running the loop after each cut, and keep only what's load-bearing for the failure. + +Why bother: a minimal repro shrinks the hypothesis space in Phase 3 (fewer moving parts left to suspect) and becomes the clean regression test in Phase 5. + +Done when **every remaining element is load-bearing**: removing any one of them makes the loop go green. + +Do not proceed until you have reproduced **and** minimised. + +## Phase 3: Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If is the cause, then will make the bug disappear / will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe: discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it; proceed with your ranking if the user is AFK. + +## Phase 4: Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. + +## Phase 5: Fix + regression test + +Write the regression test **before the fix**, but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6: Cleanup + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message, so the next debugger learns diff --git a/.agents/skills/diagnosing-bugs/agents/openai.yaml b/.agents/skills/diagnosing-bugs/agents/openai.yaml new file mode 100644 index 00000000..a13a755a --- /dev/null +++ b/.agents/skills/diagnosing-bugs/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Diagnosing Bugs" + short_description: "Diagnose hard bugs and regressions" diff --git a/.agents/skills/diagnosing-bugs/scripts/hitl-loop.template.sh b/.agents/skills/diagnosing-bugs/scripts/hitl-loop.template.sh new file mode 100644 index 00000000..24319846 --- /dev/null +++ b/.agents/skills/diagnosing-bugs/scripts/hitl-loop.template.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "" → show instruction, wait for Enter +# capture VAR "" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. +# +# `capture` prints its value back to the terminal, where the agent reads it, +# so capture observations, and leave signing in to the user as a `step`. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/.agents/skills/domain-modeling/ADR-FORMAT.md b/.agents/skills/domain-modeling/ADR-FORMAT.md new file mode 100644 index 00000000..d7e61f30 --- /dev/null +++ b/.agents/skills/domain-modeling/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily: only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why*, not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`): useful when decisions are revisited +- **Considered Options**: only when the rejected alternatives are worth remembering +- **Consequences**: only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse**: the cost of changing your mind later is meaningful +2. **Surprising without context**: a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off**: there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it: you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library: just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it; otherwise someone will suggest GraphQL again in six months. diff --git a/.agents/skills/domain-modeling/CONTEXT-FORMAT.md b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md new file mode 100644 index 00000000..79bbb32f --- /dev/null +++ b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md @@ -0,0 +1,60 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A one or two sentence description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. +- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md): receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md): generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md): manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.agents/skills/domain-modeling/SKILL.md b/.agents/skills/domain-modeling/SKILL.md new file mode 100644 index 00000000..9b97707e --- /dev/null +++ b/.agents/skills/domain-modeling/SKILL.md @@ -0,0 +1,74 @@ +--- +name: domain-modeling +description: Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a CONTEXT.md, or recording or editing an ADR. +--- + +# Domain Modeling + +Actively build and sharpen the project's domain model as you design. This is the *active* discipline: challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill: that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.) + +## File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily: only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y. Which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account': do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible. Which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up: capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse**: the cost of changing your mind later is meaningful +2. **Surprising without context**: a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off**: there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). diff --git a/.agents/skills/domain-modeling/agents/openai.yaml b/.agents/skills/domain-modeling/agents/openai.yaml new file mode 100644 index 00000000..7f1522d2 --- /dev/null +++ b/.agents/skills/domain-modeling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Domain Modeling" + short_description: "Build and sharpen a domain model" diff --git a/.agents/skills/git-guardrails-claude-code/SKILL.md b/.agents/skills/git-guardrails-claude-code/SKILL.md new file mode 100644 index 00000000..58bcdd87 --- /dev/null +++ b/.agents/skills/git-guardrails-claude-code/SKILL.md @@ -0,0 +1,95 @@ +--- +name: git-guardrails-claude-code +description: Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute. Use when user wants to prevent destructive git operations, add git safety hooks, or block git push/reset in Claude Code. +--- + +# Setup Git Guardrails + +Sets up a PreToolUse hook that intercepts and blocks dangerous git commands before Claude executes them. + +## What Gets Blocked + +- `git push` (all variants including `--force`) +- `git reset --hard` +- `git clean -f` / `git clean -fd` +- `git branch -D` +- `git checkout .` / `git restore .` + +When blocked, Claude sees a message telling it that it does not have authority to access these commands. + +## Steps + +### 1. Ask scope + +Ask the user: install for **this project only** (`.claude/settings.json`) or **all projects** (`~/.claude/settings.json`)? + +### 2. Copy the hook script + +The bundled script is at: [scripts/block-dangerous-git.sh](scripts/block-dangerous-git.sh) + +Copy it to the target location based on scope: + +- **Project**: `.claude/hooks/block-dangerous-git.sh` +- **Global**: `~/.claude/hooks/block-dangerous-git.sh` + +Make it executable with `chmod +x`. + +### 3. Add hook to settings + +Add to the appropriate settings file: + +**Project** (`.claude/settings.json`): + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-git.sh" + } + ] + } + ] + } +} +``` + +**Global** (`~/.claude/settings.json`): + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/block-dangerous-git.sh" + } + ] + } + ] + } +} +``` + +If the settings file already exists, merge the hook into the existing `hooks.PreToolUse` array. Don't overwrite other settings. + +### 4. Ask about customization + +Ask if user wants to add or remove any patterns from the blocked list. Edit the copied script accordingly. + +### 5. Verify + +Run a quick test: + +```bash +echo '{"tool_input":{"command":"git push origin main"}}' | +``` + +Should exit with code 2 and print a BLOCKED message to stderr. diff --git a/.agents/skills/git-guardrails-claude-code/agents/openai.yaml b/.agents/skills/git-guardrails-claude-code/agents/openai.yaml new file mode 100644 index 00000000..3f5d756f --- /dev/null +++ b/.agents/skills/git-guardrails-claude-code/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Git Guardrails for Claude Code" + short_description: "Block dangerous git commands" diff --git a/.agents/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh b/.agents/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh new file mode 100755 index 00000000..c40b59cb --- /dev/null +++ b/.agents/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +INPUT=$(cat) +COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command') + +DANGEROUS_PATTERNS=( + "git push" + "git reset --hard" + "git clean -fd" + "git clean -f" + "git branch -D" + "git checkout \." + "git restore \." + "push --force" + "reset --hard" +) + +for pattern in "${DANGEROUS_PATTERNS[@]}"; do + if echo "$COMMAND" | grep -qE "$pattern"; then + echo "BLOCKED: '$COMMAND' matches dangerous pattern '$pattern'. The user has prevented you from doing this." >&2 + exit 2 + fi +done + +exit 0 diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md new file mode 100644 index 00000000..3947ff9c --- /dev/null +++ b/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-me +description: A relentless interview to sharpen a plan or design. +disable-model-invocation: true +--- + +Call the Skill tool with "grilling". diff --git a/.agents/skills/grill-me/agents/openai.yaml b/.agents/skills/grill-me/agents/openai.yaml new file mode 100644 index 00000000..4d6fb0c7 --- /dev/null +++ b/.agents/skills/grill-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill Me" + short_description: "Sharpen a plan through interview" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/grill-with-docs/SKILL.md b/.agents/skills/grill-with-docs/SKILL.md new file mode 100644 index 00000000..62b9efb6 --- /dev/null +++ b/.agents/skills/grill-with-docs/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-with-docs +description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go. +disable-model-invocation: true +--- + +Call the Skill tool twice, for "grilling" and "domain-modeling". diff --git a/.agents/skills/grill-with-docs/agents/openai.yaml b/.agents/skills/grill-with-docs/agents/openai.yaml new file mode 100644 index 00000000..5dbe2780 --- /dev/null +++ b/.agents/skills/grill-with-docs/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill with Docs" + short_description: "Grill a design and write its docs" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/grilling/SKILL.md b/.agents/skills/grilling/SKILL.md new file mode 100644 index 00000000..1c2bb7bf --- /dev/null +++ b/.agents/skills/grilling/SKILL.md @@ -0,0 +1,22 @@ +--- +name: grilling +description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. +--- + +Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it. + +Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round. + +Each question should be formatted like so: + +``` +❓ **Q1** - ****: + +➡️ +``` + +Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one. + +Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait. + +The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding. diff --git a/.agents/skills/grilling/agents/openai.yaml b/.agents/skills/grilling/agents/openai.yaml new file mode 100644 index 00000000..ddbdb961 --- /dev/null +++ b/.agents/skills/grilling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Grilling" + short_description: "Stress-test thinking a round of questions at a time" diff --git a/.agents/skills/handoff/SKILL.md b/.agents/skills/handoff/SKILL.md new file mode 100644 index 00000000..2eb98a51 --- /dev/null +++ b/.agents/skills/handoff/SKILL.md @@ -0,0 +1,16 @@ +--- +name: handoff +description: Compact the current conversation into a handoff document for another agent to pick up. +argument-hint: "What will the next session be used for?" +disable-model-invocation: true +--- + +Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS - not the current workspace. + +Include a "suggested skills" section in the document, naming which skills the next agent should call the Skill tool for. + +Do not duplicate content already captured in other artifacts (specs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. + +Redact any sensitive information, such as API keys, passwords, or personally identifiable information. + +If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly. diff --git a/.agents/skills/handoff/agents/openai.yaml b/.agents/skills/handoff/agents/openai.yaml new file mode 100644 index 00000000..6e1d8da1 --- /dev/null +++ b/.agents/skills/handoff/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Handoff" + short_description: "Compact a conversation into a handoff" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/implement/SKILL.md b/.agents/skills/implement/SKILL.md new file mode 100644 index 00000000..7a0b11f5 --- /dev/null +++ b/.agents/skills/implement/SKILL.md @@ -0,0 +1,15 @@ +--- +name: implement +description: "Implement a piece of work based on a spec or set of tickets." +disable-model-invocation: true +--- + +Implement the work described by the user in the spec or tickets. + +Use /tdd where possible, at pre-agreed seams. + +Run typechecking regularly, single test files regularly, and the full test suite once at the end. + +Once done, use /code-review to review the work. + +Commit your work to the current branch. diff --git a/.agents/skills/implement/agents/openai.yaml b/.agents/skills/implement/agents/openai.yaml new file mode 100644 index 00000000..f8794dc1 --- /dev/null +++ b/.agents/skills/implement/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Implement" + short_description: "Build work from a spec or tickets" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/improve-codebase-architecture/HTML-REPORT.md b/.agents/skills/improve-codebase-architecture/HTML-REPORT.md new file mode 100644 index 00000000..e39e8255 --- /dev/null +++ b/.agents/skills/improve-codebase-architecture/HTML-REPORT.md @@ -0,0 +1,123 @@ +# HTML Report Format + +The architectural review is rendered as a single self-contained HTML file in the OS temp directory. Tailwind and Mermaid both come from CDNs. Mermaid handles graph-shaped diagrams reliably; hand-built divs and inline SVG handle the more editorial visuals (mass diagrams, cross-sections). Mix the two: don't lean on Mermaid for everything, it'll start to look generic. + +## Scaffold + +```html + + + + + Architecture review for {{repo name}} + + + + + +
+
...
+
...
+
...
+
+ + +``` + +## Header + +Repo name, date, and a compact legend: solid box = module, dashed line = seam, red arrow = leakage, thick dark box = deep module. No introduction paragraph. Straight into the candidates. + +## Candidate card + +The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `/codebase-design` skill) without ceremony. + +Each candidate is one `
`: + +- **Title**: short, names the deepening (e.g. "Collapse the Order intake pipeline"). +- **Badge row**: recommendation strength (`Strong` = emerald, `Worth exploring` = amber, `Speculative` = slate), plus a tag for the dependency category (`in-process`, `local-substitutable`, `ports & adapters`, `mock`). +- **Files**: monospaced list, `font-mono text-sm`. +- **Before / After diagram**: the centrepiece. Two columns, side by side. See patterns below. +- **Problem**: one sentence. What hurts. +- **Solution**: one sentence. What changes. +- **Wins**: bullets, ≤6 words each. e.g. "Tests hit one interface", "Pricing logic stops leaking", "Delete 4 shallow wrappers". +- **ADR callout** (if applicable): one line in an amber-tinted box. + +No paragraphs of explanation. If the diagram needs a paragraph to be understood, redraw the diagram. + +## Diagram patterns + +Pick the pattern that fits the candidate. Mix them. Don't make every diagram look the same. Variety is part of the point. + +### Mermaid graph (the workhorse for dependencies / call flow) + +Use a Mermaid `flowchart` or `graph` when the point is "X calls Y calls Z, and look at the mess." Wrap it in a Tailwind-styled card so it doesn't feel parachuted in. Style with classDef to colour leakage edges red and the deep module dark. Sequence diagrams work well for "before: 6 round-trips; after: 1." + +```html +
+
+    flowchart LR
+      A[OrderHandler] --> B[OrderValidator]
+      B --> C[OrderRepo]
+      C -.leak.-> D[PricingClient]
+      classDef leak stroke:#dc2626,stroke-width:2px;
+      class C,D leak
+  
+
+``` + +### Hand-built boxes-and-arrows (when Mermaid's layout fights you) + +Modules as `
`s with borders and labels. Arrows as inline SVG `` or `` elements positioned absolutely over a relative container. Reach for this when you want the "after" diagram to feel like one thick-bordered deep module with greyed-out internals, since Mermaid won't render that with the right weight. + +### Cross-section (good for layered shallowness) + +Stack horizontal bands (`h-12 border-l-4`) to show layers a call passes through. Before: 6 thin layers each doing nothing. After: 1 thick band labelled with the consolidated responsibility. + +### Mass diagram (good for "interface as wide as implementation") + +Two rectangles per module: one for interface surface area, one for implementation. Before: interface rectangle is nearly as tall as the implementation rectangle (shallow). After: interface rectangle is short, implementation rectangle is tall (deep). + +### Call-graph collapse + +Before: a tree of function calls rendered as nested boxes. After: the same tree collapsed into one box, with the now-internal calls shown faded inside it. + +## Style guidance + +- Lean editorial, not corporate-dashboard. Generous whitespace. Serif optional for headings (`font-serif` works well with stone/slate). +- Colour sparingly: one accent (emerald or indigo) plus red for leakage and amber for warnings. +- Keep diagrams ~320px tall so before/after sits comfortably side by side without scrolling. +- Use `text-xs uppercase tracking-wider` for module labels inside diagrams, so they read as schematic, not as UI. +- The only scripts are the Tailwind CDN and the Mermaid ESM import. The report is otherwise static: no app code, no interactivity beyond Mermaid's own rendering. + +## Top recommendation section + +One larger card. Candidate name, one sentence on why, anchor link to its card. That's it. + +## Tone + +Plain English, concise, but the architectural nouns and verbs come straight from the `/codebase-design` skill. Concision is not an excuse to drift. + +**Use exactly:** module, interface, implementation, depth, deep, shallow, seam, adapter, leverage, locality. + +**Never substitute:** component, service, unit (for module) · API, signature (for interface) · boundary (for seam) · layer, wrapper (for module, when you mean module). + +**Phrasings that fit the style:** + +- "Order intake module is shallow: interface nearly matches the implementation." +- "Pricing leaks across the seam." +- "Deepen: one interface, one place to test." +- "Two adapters justify the seam: HTTP in prod, in-memory in tests." + +**Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"*, because those terms aren't in the glossary and don't earn their place. + +No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `/codebase-design` glossary, reach for one that is before inventing a new one. diff --git a/.agents/skills/improve-codebase-architecture/SKILL.md b/.agents/skills/improve-codebase-architecture/SKILL.md new file mode 100644 index 00000000..a578dd0a --- /dev/null +++ b/.agents/skills/improve-codebase-architecture/SKILL.md @@ -0,0 +1,71 @@ +--- +name: improve-codebase-architecture +description: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick. +disable-model-invocation: true +--- + +# Improve Codebase Architecture + +Surface architectural friction and propose **deepening opportunities**: refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability. + +This command is _informed_ by the project's domain model and built on a shared design vocabulary: + +- Call the Skill tool with "codebase-design" for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use these terms exactly in every suggestion, and don't drift into "component," "service," "API," or "boundary." +- The domain language in `CONTEXT.md` gives names to good seams; ADRs in `docs/adr/` record decisions this command should not re-litigate. + +## Process + +### 1. Explore + +**Scope before you scan: YAGNI.** Deepening a module pays off by making future changes to it easier, so put extra weight on the parts of the codebase that have recently changed. Decide *where* to look before you look: + +- If the user named a direction (a module, a subsystem, a pain point), take it, and skip the inference below. +- Otherwise, walk back a good stretch of the commit history (`git log --oneline`) to find the codebase's hot spots, the files and areas that keep coming up, and let those paths pull your attention first. If the changes are scattered with no clear hot spot, widen the net. + +Read the project's domain glossary (`CONTEXT.md`) and any ADRs in the area you're touching first. + +Then spawn a sub-agent to walk the codebase. Don't follow rigid heuristics; explore organically and note where you experience friction: + +- Where does understanding one concept require bouncing between many small modules? +- Where are modules **shallow**, with an interface nearly as complex as the implementation? +- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)? +- Where do tightly-coupled modules leak across their seams? +- Which parts of the codebase are untested, or hard to test through their current interface? + +Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want. + +### 2. Present candidates as an HTML report + +Write a self-contained HTML file to the OS temp directory so nothing lands in the repo. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `/architecture-review-.html` so each run gets a fresh file. Open it for the user (`xdg-open ` on Linux, `open ` on macOS, `start ` on Windows) and tell them the absolute path. + +The report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals: use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual. + +For each candidate, render a card with: + +- **Files**: which files/modules are involved +- **Problem**: why the current architecture is causing friction +- **Solution**: plain English description of what would change +- **Benefits**: explained in terms of locality and leverage, and how tests would improve +- **Before / After diagram**: side-by-side, custom-drawn, illustrating the shallowness and the deepening +- **Recommendation strength**: one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge + +End the report with a **Top recommendation** section: which candidate you'd tackle first and why. + +**Use CONTEXT.md vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module," not "the FooBarHandler," and not "the Order service." + +**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: _"contradicts ADR-0007, but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids. + +See [HTML-REPORT.md](HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance. + +Do NOT propose interfaces yet. After the file is written, ask the user: "Which of these would you like to explore?" + +### 3. Grilling loop + +Once the user picks a candidate, call the Skill tool with "grilling" to walk the decision tree with them: constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive. + +Side effects happen inline as decisions crystallize; call the Skill tool with "domain-modeling" to keep the domain model current as you go: + +- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md`. Create the file lazily if it doesn't exist. +- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there. +- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing; skip ephemeral reasons ("not worth it right now") and self-evident ones. +- **Want to explore alternative interfaces for the deepened module?** Call the Skill tool with "codebase-design" and use its design-it-twice parallel sub-agent pattern. diff --git a/.agents/skills/improve-codebase-architecture/agents/openai.yaml b/.agents/skills/improve-codebase-architecture/agents/openai.yaml new file mode 100644 index 00000000..706fdca0 --- /dev/null +++ b/.agents/skills/improve-codebase-architecture/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Improve Codebase Architecture" + short_description: "Find and grill architecture improvements" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/loop-me/SKILL.md b/.agents/skills/loop-me/SKILL.md new file mode 100644 index 00000000..e58a474c --- /dev/null +++ b/.agents/skills/loop-me/SKILL.md @@ -0,0 +1,32 @@ +--- +name: loop-me +description: Grill me about specs for the workflows I want to build, within this workspace. +disable-model-invocation: true +argument-hint: "A workflow to design, or nothing to go find one" +--- + +Run a stateful `/grilling` session whose only output is **workflow** specs. Use the grilling discipline (relentless, a round of questions at a time, a recommended answer attached to each) aimed at the vocabulary and goal below. Create, edit, and delete specs as the grilling resolves things. + +## The loop lens + +A **loop** is a recurring pattern in the user's life: their career, their week, their morning, a single repeated activity. Picturing a life as loops within loops reveals how predictable its activities really are, which is what makes them worth **delegating**. Use the lens to find loops worth specifying, and propose ones the user hasn't noticed. + +A **workflow** is the spec of one loop, made real. You run a workflow on a loop: the loop is its running instantiation. Workflows live in `workflows/*.md` and are the source of truth. + +## Vocabulary + +A shared language, reached for only when a workflow calls for it: never a checklist. **Mandate nothing structural**: a workflow needs no AI, no checkpoint, and no schedule unless the grilling shows it does. + +- **Trigger**: what fires each run, an **event** (a new email, a new issue) or a **schedule** (every morning). Event-triggering is usually the more efficient. +- **Checkpoint**: a human-in-the-loop point where the user is asked to verify or decide. Some workflows have none and run autonomously; some use no AI at all. +- **Push right**: defer the checkpoint as far as it will go. Do maximal work before involving the human, so they are asked once, late, with everything prepared. +- **Brief**: what a checkpoint presents, a tight, decision-ready summary (what was produced, why, and a link down to the asset itself), never the raw output. The user reads a brief, not a draft. Speed of review is imperative. + +## Definition of done + +A workflow spec is done when an implementer agent could build it without asking a single question. Grill until then; nothing is done while a question remains. + +## The workspace + +- `workflows/*.md`: one spec per workflow. +- `NOTES.md`: raw notes on the user's world, the tools they use, the channels they process, and their own terminology for both. When it is empty or thin, interview them about their world before specifying anything. Sharpen fuzzy terms into canonical ones as they surface, and record them here. diff --git a/.agents/skills/loop-me/agents/openai.yaml b/.agents/skills/loop-me/agents/openai.yaml new file mode 100644 index 00000000..1a4f4111 --- /dev/null +++ b/.agents/skills/loop-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Loop Me" + short_description: "Spec the workflows you want to build" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/migrate-to-shoehorn/SKILL.md b/.agents/skills/migrate-to-shoehorn/SKILL.md new file mode 100644 index 00000000..ae4f965e --- /dev/null +++ b/.agents/skills/migrate-to-shoehorn/SKILL.md @@ -0,0 +1,118 @@ +--- +name: migrate-to-shoehorn +description: Migrate test files from `as` type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace `as` in tests, or needs partial test data. +--- + +# Migrate to Shoehorn + +## Why shoehorn? + +`shoehorn` lets you pass partial data in tests while keeping TypeScript happy. It replaces `as` assertions with type-safe alternatives. + +**Test code only.** Never use shoehorn in production code. + +Problems with `as` in tests: + +- Trained not to use it +- Must manually specify target type +- Double-as (`as unknown as Type`) for intentionally wrong data + +## Install + +```bash +npm i @total-typescript/shoehorn +``` + +## Migration patterns + +### Large objects with few needed properties + +Before: + +```ts +type Request = { + body: { id: string }; + headers: Record; + cookies: Record; + // ...20 more properties +}; + +it("gets user by id", () => { + // Only care about body.id but must fake entire Request + getUser({ + body: { id: "123" }, + headers: {}, + cookies: {}, + // ...fake all 20 properties + }); +}); +``` + +After: + +```ts +import { fromPartial } from "@total-typescript/shoehorn"; + +it("gets user by id", () => { + getUser( + fromPartial({ + body: { id: "123" }, + }), + ); +}); +``` + +### `as Type` → `fromPartial()` + +Before: + +```ts +getUser({ body: { id: "123" } } as Request); +``` + +After: + +```ts +import { fromPartial } from "@total-typescript/shoehorn"; + +getUser(fromPartial({ body: { id: "123" } })); +``` + +### `as unknown as Type` → `fromAny()` + +Before: + +```ts +getUser({ body: { id: 123 } } as unknown as Request); // wrong type on purpose +``` + +After: + +```ts +import { fromAny } from "@total-typescript/shoehorn"; + +getUser(fromAny({ body: { id: 123 } })); +``` + +## When to use each + +| Function | Use case | +| --------------- | -------------------------------------------------- | +| `fromPartial()` | Pass partial data that still type-checks | +| `fromAny()` | Pass intentionally wrong data (keeps autocomplete) | +| `fromExact()` | Force full object (swap with fromPartial later) | + +## Workflow + +1. **Gather requirements** - ask user: + - What test files have `as` assertions causing problems? + - Are they dealing with large objects where only some properties matter? + - Do they need to pass intentionally wrong data for error testing? + +2. **Install and migrate**: + - [ ] Install: `npm i @total-typescript/shoehorn` + - [ ] Find test files with `as` assertions: `grep -r " as [A-Z]" --include="*.test.ts" --include="*.spec.ts"` + - [ ] Replace `as Type` with `fromPartial()` + - [ ] Replace `as unknown as Type` with `fromAny()` + - [ ] Add imports from `@total-typescript/shoehorn` + - [ ] Run type check to verify diff --git a/.agents/skills/migrate-to-shoehorn/agents/openai.yaml b/.agents/skills/migrate-to-shoehorn/agents/openai.yaml new file mode 100644 index 00000000..3bd79ee2 --- /dev/null +++ b/.agents/skills/migrate-to-shoehorn/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Migrate to Shoehorn" + short_description: "Replace test assertions with shoehorn" diff --git a/.agents/skills/prototype/LOGIC.md b/.agents/skills/prototype/LOGIC.md new file mode 100644 index 00000000..32be86a0 --- /dev/null +++ b/.agents/skills/prototype/LOGIC.md @@ -0,0 +1,67 @@ +# Logic Prototype + +A single, self-contained HTML file (a **shareable demo**) that lets anyone drive a state model by clicking buttons. Use this when the question is about **business logic, state transitions, or data shape**: the kind of thing that looks reasonable on paper but only feels wrong once you push it through real cases. + +Because it's one file with nothing to install, you can hand it to a non-developer (a designer, a PM, a domain expert) and let them feel the model for themselves. So it speaks their language, not the code's. + +## When this is the right shape + +- "I'm not sure if this state machine handles the edge case where X then Y." +- "Does this data model actually let me represent the case where..." +- "I want to feel out what the API should look like before writing it." +- Anything where someone wants to **press buttons and watch state change**. + +If the question is "what should this look like," this is the wrong branch. Use [UI.md](UI.md). + +## Process + +### 1. State the question + +Before writing code, write down what state model and what question you're prototyping. One paragraph, at the top of the demo (in a visible intro, not just a comment). A logic prototype that answers the wrong question is pure waste, so make the question explicit so it can be checked later, whether the user is watching now or returning to it AFK. + +### 2. Isolate the logic in a portable module + +Put the actual logic (the bit that's answering the question) in a single `