one directory per feature: schema.ts and queries.ts together

src/databases/officer_db/src/<feature>/{schema.ts,queries.ts}, replacing the
parallel schema/ and queries/ trees. 24 feature directories, 46 files moved with
git mv so history follows.

The parallel trees had drifted, which is what the restructure is really fixing:

  four features were named differently on each side — app-store/sidecar-installs,
  email/email-accounts, server/server-config

  operations had a schema and NO query file: its task_logs is reached directly
  from src/servers/api/task-logger.ts, bypassing this package's own boundary

  integrations had queries and NO schema, because it spans two features'
  tables — server_integrations and user_integrations

Both lopsided cases survive as directories holding one file, which states the
problem instead of hiding it across two trees.

Nothing outside the package changed how it imports. `officerdb`, `officerdb/types`
and `officerdb/db` resolve exactly as before; index.ts absorbed the path changes.
Added `"./*": "./src/*"` so the new layout is reachable — `officerdb/soulseek/schema`
— which one script needed, because soulseek is a plugin and therefore commented
out of the aggregator.

schema/index.ts became src/schema.ts, keeping the core/plugin split from earlier
tonight. drizzle.config.ts and the package's "./schema" export follow it.

Verified rather than assumed: all 52 files in the package parse, every relative
import resolves against the new layout (checked by walking each specifier to a
real file, since parsing does not check paths), and everything in the tree
importing officerdb still parses. Not typechecked — empty node_modules, frozen
installs.

One rewrite bug worth recording: the rule mapping a query module's sibling import
also matched the './schema' this pass had just written, turning it into
'../schema/queries' in 22 files. Caught by the resolver check, not by parsing —
both spellings parse fine.

Also corrects every path reference the move invalidated: src/databases/CLAUDE.md's
layout diagram, the root CLAUDE.md data section, three docs, and seven sidecar
comments naming queries/<x>.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 01:34:04 +00:00
co-authored by Claude Opus 5
parent 4cebae1c85
commit 68f2c55ecf
63 changed files with 161 additions and 142 deletions
+3 -2
View File
@@ -123,8 +123,9 @@ imported by their package name (`officerdev`, `hooks`, `state`, `types`, `helper
Two stores, and the split matters: Two stores, and the split matters:
**Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards, **Postgres** (`src/databases/officer_db`) holds the account, passkeys, settings, dashboards,
email accounts, queue and pipeline jobs. Schema in `src/schema/`, hand-written queries in email accounts, queue and pipeline jobs. One directory per feature holding `schema.ts` and
`src/queries/`, types inferred from the schema in `src/types.ts`. `queries.ts` beside each other; `src/schema.ts` is what `db:push` reads, and it lists the core tables
with the plugin ones commented out. Types inferred from the schema in `src/types.ts`.
**The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`) **The filesystem** holds everything the agent authors. `OFFICER_ITEMS_DIR` (`$OFFICER_ROOT/capabilities`)
contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no contains one directory per item under `skills/`, `tools/`, `tasks/`, `processes/`, `extensions/` — no
+1 -1
View File
@@ -327,4 +327,4 @@ service verbs exist (`listApiKeys`, `revokeApiKey`) if that changes.
bearer string into a caller. All four doors call it: `userMiddleware`, `originScopeMiddleware`, the bearer string into a caller. All four doors call it: `userMiddleware`, `originScopeMiddleware`, the
WebSocket upgrade in `server.tsx`, and the vault socket. WebSocket upgrade in `server.tsx`, and the vault socket.
- `src/servers/api/api-keys/router.ts` — the three endpoints. - `src/servers/api/api-keys/router.ts` — the three endpoints.
- `src/databases/officer_db/src/schema/api-keys.ts` — the table, and why it stores what it stores. - `src/databases/officer_db/src/api-keys/schema.ts` — the table, and why it stores what it stores.
+1 -1
View File
@@ -5,7 +5,7 @@ does and does not protect against.
Authoritative for the crypto design. The code is `src/servers/sidecar/wallet/keys.ts` (sealing, Authoritative for the crypto design. The code is `src/servers/sidecar/wallet/keys.ts` (sealing,
derivation, unlock sessions), `src/databases/officer_db/src/crypto.ts` (storage encryption) and derivation, unlock sessions), `src/databases/officer_db/src/crypto.ts` (storage encryption) and
`src/databases/officer_db/src/queries/wallet.ts` (where the two meet). `src/databases/officer_db/src/wallet/queries.ts` (where the two meet).
## The requirement ## The requirement
+3 -3
View File
@@ -135,7 +135,7 @@ and the rename sequence leaves `workspaces` with no zombie.
- [x] **`ws-terminals-{id}: null` on a live dashboard is a 500.** Same file, `:61-66` — the - [x] **`ws-terminals-{id}: null` on a live dashboard is a 500.** Same file, `:61-66` — the
`ws-layout-*` branch has a `value === null``deleteDashboard` case (`:42`); the terminals `ws-layout-*` branch has a `value === null``deleteDashboard` case (`:42`); the terminals
branches do not. A null falls to the UPDATE branch and sets a `NOT NULL` column branches do not. A null falls to the UPDATE branch and sets a `NOT NULL` column
(`databases/officer_db/src/queries/dashboards.ts:70`) → 23502. (`databases/officer_db/src/dashboards/queries.ts:70`) → 23502.
**Resolved.** A null on either terminals branch is now a no-op: it means "forget this key", and it **Resolved.** A null on either terminals branch is now a no-op: it means "forget this key", and it
only ever arrives paired with `ws-layout-{id}: null` on a rename, by which point the row is gone. only ever arrives paired with `ws-layout-{id}: null` on a rename, by which point the row is gone.
@@ -202,7 +202,7 @@ these.
> which uuid ids would not. > which uuid ids would not.
- [ ] **`dashboards.id` is a global primary key but ids are `slugify(name)`.** - [ ] **`dashboards.id` is a global primary key but ids are `slugify(name)`.**
`databases/officer_db/src/schema/dashboards.ts` declares `id: text('id').primaryKey()`. Live: `databases/officer_db/src/dashboards/schema.ts` declares `id: text('id').primaryKey()`. Live:
`"dashboards_pkey" PRIMARY KEY, btree (id)` plus a redundant `"dashboards_pkey" PRIMARY KEY, btree (id)` plus a redundant
`"uq_dashboards_user_id" UNIQUE, btree (user_id, id)` — evidence per-user ids were intended and `"uq_dashboards_user_id" UNIQUE, btree (user_id, id)` — evidence per-user ids were intended and
half-built. Ids come from `DashboardPreview.tsx:300` (`slugify(trimmed) || generateSlug()`) and the half-built. Ids come from `DashboardPreview.tsx:300` (`slugify(trimmed) || generateSlug()`) and the
@@ -213,7 +213,7 @@ these.
(see `databases/CLAUDE.md` → "Composite keys") — harmless churn, but read the plan. (see `databases/CLAUDE.md` → "Composite keys") — harmless churn, but read the plan.
- [x] **`upsertDashboard`'s UPDATE has no `userId` predicate.** - [x] **`upsertDashboard`'s UPDATE has no `userId` predicate.**
`databases/officer_db/src/queries/dashboards.ts:73` — `databases/officer_db/src/dashboards/queries.ts:73` —
`db.update(dashboards).set(set).where(eq(dashboards.id, id))`. The `existing` lookup above it _is_ `db.update(dashboards).set(set).where(eq(dashboards.id, id))`. The `existing` lookup above it _is_
scoped, so it cannot reach another user's row today, but it is a non-transactional read-then-write. scoped, so it cannot reach another user's row today, but it is a non-transactional read-then-write.
**It becomes a live cross-user overwrite the moment the PK above is made composite.** **It becomes a live cross-user overwrite the moment the PK above is made composite.**
+3 -1
View File
@@ -10,7 +10,9 @@
import type { BrowsedFile } from 'officerdb'; import type { BrowsedFile } from 'officerdb';
import { eq, asc } from 'drizzle-orm'; import { eq, asc } from 'drizzle-orm';
import { db, finishSoulseekBrowse } from 'officerdb'; import { db, finishSoulseekBrowse } from 'officerdb';
import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/schema'; // soulseek is a plugin, so its tables are commented out of officerdb's schema aggregator —
// import them from the feature directly.
import { soulseekBrowseSnapshots, soulseekBrowseDirs } from 'officerdb/soulseek/schema';
import { buildTree } from '../src/servers/sidecar/slskd/browse'; import { buildTree } from '../src/servers/sidecar/slskd/browse';
const snapshots = await db const snapshots = await db
+23 -10
View File
@@ -10,17 +10,30 @@ describing a different codebase.
src/databases/officer_db/ src/databases/officer_db/
├── src/ ├── src/
│ ├── db.ts # the connection │ ├── db.ts # the connection
│ ├── index.ts # public surface: re-exports queries, schema and drizzle helpers │ ├── index.ts # public surface: re-exports every feature's queries
│ ├── schema.ts # what db:push creates — see below
│ ├── types.ts # every type export (Select / Insert / extended) │ ├── types.ts # every type export (Select / Insert / extended)
── schema/ ── crypto.ts # at-rest encryption, one key per purpose
├── index.ts # re-exports all schema files ├── secret-store.ts # the key store itself (SQLite, outside Postgres)
└── *.ts # table definitions, grouped by domain └── <feature>/
└── package.json # exports "." and "./types" │ ├── schema.ts # its tables
│ └── queries.ts # everything that reads or writes them
└── package.json # exports ".", "./types", "./db", "./schema", "./secret-store", "./*"
``` ```
Schema files are grouped by domain, not by table: `auth`, `chat-events`, `dashboards`, `email`, **One directory per feature, holding both halves.** Restructured 2026-08-13 from parallel `schema/` and
`headscale`, `music`, `operations`, `pipeline-jobs`, `server`, `soulseek`, `user-data`, `vault`, `queries/` trees, where the two sides had drifted: four features were named differently on each side
`wallet`. (`app-store`/`sidecar-installs`, `email`/`email-accounts`, `server`/`server-config`), `operations` had no
query file at all, and `integrations` had no schema file.
Two directories are still lopsided and say so by their contents: `operations/` has only a schema (its
`task_logs` is reached directly from `src/servers/`, bypassing this package), and `integrations/` has only
queries, because it spans `server` and `user-data`.
**`src/schema.ts` is drizzle-kit's view, not the runtime's.** `drizzle.config.ts` points at it, so a
commented line there removes a table from the DATABASE without removing a line of code — every query
imports its tables from `./schema` inside its own feature directory. That is what lets a fresh install
create only the core tables, with the plugin ones commented out until their plugin is installed.
## Schema changes use `push`, not migrations ## Schema changes use `push`, not migrations
@@ -114,8 +127,8 @@ Organise `types.ts` by domain with section comments, mirroring the schema files.
## Queries ## Queries
Hand-written, one file per domain under `src/queries/`, importing tables from `../schema` and types from Hand-written, one `queries.ts` per feature directory, importing tables from `./schema` beside it and
`../types`: types from `../types`:
```ts ```ts
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
+1 -1
View File
@@ -15,7 +15,7 @@ try {
} catch {} } catch {}
export default defineConfig({ export default defineConfig({
schema: './src/schema/index.ts', schema: './src/schema.ts',
out: './migrations', out: './migrations',
dialect: 'postgresql', dialect: 'postgresql',
dbCredentials: { dbCredentials: {
+3 -2
View File
@@ -7,8 +7,9 @@
".": "./src/index.ts", ".": "./src/index.ts",
"./types": "./src/types.ts", "./types": "./src/types.ts",
"./db": "./src/db.ts", "./db": "./src/db.ts",
"./schema": "./src/schema/index.ts", "./schema": "./src/schema.ts",
"./secret-store": "./src/secret-store.ts" "./secret-store": "./src/secret-store.ts",
"./*": "./src/*"
}, },
"scripts": { "scripts": {
"generate": "drizzle-kit generate --config=drizzle.config.ts", "generate": "drizzle-kit generate --config=drizzle.config.ts",
@@ -1,8 +1,8 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { and, asc, eq } from 'drizzle-orm'; import { and, asc, eq } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { agentPanels } from '../schema'; import { agentPanels } from './schema';
import type { AgentPanelRow } from '../schema/agent-panels'; import type { AgentPanelRow } from './schema';
export type AgentPanel = AgentPanelRow; export type AgentPanel = AgentPanelRow;
@@ -1,5 +1,5 @@
import { pgTable, serial, text, integer, timestamp, uniqueIndex, index } from 'drizzle-orm/pg-core'; import { pgTable, serial, text, integer, timestamp, uniqueIndex, index } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
/** /**
* One named agent living in one dashboard panel the address book that lets two chat panels on the * One named agent living in one dashboard panel the address book that lets two chat panels on the
@@ -1,6 +1,7 @@
import { eq, and, isNull, sql } from 'drizzle-orm'; import { eq, and, isNull, sql } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { apiKeys, users } from '../schema'; import { apiKeys } from './schema';
import { users } from '../auth/schema';
import type { ApiKeySelect } from '../types'; import type { ApiKeySelect } from '../types';
// Every read here is scoped by userId except `findLiveApiKeyByHash`, which cannot be: authentication is // Every read here is scoped by userId except `findLiveApiKeyByHash`, which cannot be: authentication is
@@ -1,5 +1,5 @@
import { pgTable, serial, text, integer, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, text, integer, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
// Long-lived credentials a user mints for themselves, so a native app can hold one instead of a password. // Long-lived credentials a user mints for themselves, so a native app can hold one instead of a password.
// //
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { sidecarInstalls } from '../schema'; import { sidecarInstalls } from './schema';
// What the owner has installed from the app store. See ../schema/app-store.ts for why there is no // What the owner has installed from the app store. See ../schema/app-store.ts for why there is no
// userId and why `installed` and `enabled` are separate. // userId and why `installed` and `enabled` are separate.
@@ -1,6 +1,6 @@
import { eq, and, lt, sql } from 'drizzle-orm'; import { eq, and, lt, sql } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from '../schema'; import { users, passkeys, passkeyChallenges, tokenBlacklist, OWNER_USER_ID } from './schema';
import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types'; import type { UserSelect, UserInsert, PasskeySelect, PasskeyInsert } from '../types';
// ── Users ── // ── Users ──
@@ -1,8 +1,8 @@
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { roleCapabilities } from '../schema'; import { roleCapabilities } from './schema';
import type { UserRole } from '../schema/auth'; import type { UserRole } from '../auth/schema';
import type { CapabilityLevelValue } from '../schema/capabilities'; import type { CapabilityLevelValue } from './schema';
// Grants, keyed on role. Absence denies — see the table comment. // Grants, keyed on role. Absence denies — see the table comment.
@@ -1,6 +1,6 @@
import { pgTable, serial, text, timestamp, uniqueIndex, check } from 'drizzle-orm/pg-core'; import { pgTable, serial, text, timestamp, uniqueIndex, check } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { USER_ROLES } from './auth'; import { USER_ROLES } from '../auth/schema';
// What a ROLE may reach. The subject of a grant is a role, never a user. // What a ROLE may reach. The subject of a grant is a role, never a user.
// //
@@ -1,6 +1,6 @@
import { eq, and, gt, asc, desc, lt } from 'drizzle-orm'; import { eq, and, gt, asc, desc, lt } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { chatSessionEvents } from '../schema'; import { chatSessionEvents } from './schema';
/** Append one outbound event to a session's durable log; returns its global cursor id. */ /** Append one outbound event to a session's durable log; returns its global cursor id. */
export async function appendChatEvent(sessionId: string, event: unknown): Promise<number> { export async function appendChatEvent(sessionId: string, event: unknown): Promise<number> {
@@ -1,6 +1,6 @@
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { dashboards, screens, dashboardDefaults } from '../schema'; import { dashboards, screens, dashboardDefaults } from './schema';
// ── Full state read ── // ── Full state read ──
@@ -1,7 +1,7 @@
import type { AnyPgColumn } from 'drizzle-orm/pg-core'; import type { AnyPgColumn } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex, check } from 'drizzle-orm/pg-core'; import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex, check } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
/** /**
* A `LayoutNode` is an object, and the whole write path to these columns is `unknown` the PATCH body is * A `LayoutNode` is an object, and the whole write path to these columns is `unknown` the PATCH body is
@@ -2,7 +2,7 @@ import { and, desc, eq, isNull } from 'drizzle-orm';
import argon2 from 'argon2'; import argon2 from 'argon2';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { db } from '../db'; import { db } from '../db';
import { davAppPasswords } from '../schema/dav'; import { davAppPasswords } from './schema';
export type DavAppPassword = typeof davAppPasswords.$inferSelect; export type DavAppPassword = typeof davAppPasswords.$inferSelect;
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, timestamp, index } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, timestamp, index } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
// Per-device credentials for CalDAV / CardDAV clients — DAVx5, iOS, macOS, Thunderbird. // Per-device credentials for CalDAV / CardDAV clients — DAVx5, iOS, macOS, Thunderbird.
// //
@@ -1,6 +1,6 @@
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { emailAccounts } from '../schema/email'; import { emailAccounts } from './schema';
import type { EmailAccountInsert, EmailAccountSelect } from '../types'; import type { EmailAccountInsert, EmailAccountSelect } from '../types';
export async function getEmailAccounts(userId: number): Promise<EmailAccountSelect[]> { export async function getEmailAccounts(userId: number): Promise<EmailAccountSelect[]> {
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
export const emailAccounts = pgTable( export const emailAccounts = pgTable(
'email_accounts', 'email_accounts',
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm'; import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { headscaleServers } from '../schema'; import { headscaleServers } from './schema';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// Headscale server registry access for the officer-headscale sidecar. Callers deal in PLAINTEXT — // Headscale server registry access for the officer-headscale sidecar. Callers deal in PLAINTEXT —
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { users } from './auth'; import { users } from '../auth/schema';
// The Headscale servers the owner manages, for the officer-headscale sidecar. Officer targets no single // The Headscale servers the owner manages, for the officer-headscale sidecar. Officer targets no single
// Headscale: the owner registers one or more servers (URL + an admin API key generated on that server) and // Headscale: the owner registers one or more servers (URL + an admin API key generated on that server) and
+40 -40
View File
@@ -18,7 +18,7 @@ export {
blacklistToken, blacklistToken,
isTokenBlacklisted, isTokenBlacklisted,
cleanupExpiredTokens, cleanupExpiredTokens,
} from './queries/auth'; } from './auth/queries';
export { export {
findLiveApiKeyByHash, findLiveApiKeyByHash,
@@ -27,9 +27,9 @@ export {
revokeApiKey, revokeApiKey,
touchApiKey, touchApiKey,
type ApiKeyIdentity, type ApiKeyIdentity,
} from './queries/api-keys'; } from './api-keys/queries';
export { readServerSettings, writeServerSettings, readConfigValue, writeConfigValue } from './queries/server-config'; export { readServerSettings, writeServerSettings, readConfigValue, writeConfigValue } from './server/queries';
export { export {
getUserSettings, getUserSettings,
@@ -38,7 +38,7 @@ export {
patchUserState, patchUserState,
getDockPaths, getDockPaths,
setDockPaths, setDockPaths,
} from './queries/user-data'; } from './user-data/queries';
export { export {
getServerIntegrations, getServerIntegrations,
@@ -51,7 +51,7 @@ export {
upsertUserIntegration, upsertUserIntegration,
deleteUserIntegration, deleteUserIntegration,
findUserByIntegrationConfig, findUserByIntegrationConfig,
} from './queries/integrations'; } from './integrations/queries';
export { export {
getEmailAccounts, getEmailAccounts,
@@ -61,7 +61,7 @@ export {
updateEmailAccountStatus, updateEmailAccountStatus,
updateEmailAccountSyncMeta, updateEmailAccountSyncMeta,
getAllSyncedAccounts, getAllSyncedAccounts,
} from './queries/email-accounts'; } from './email/queries';
export { export {
getAllDashboardState, getAllDashboardState,
@@ -73,7 +73,7 @@ export {
deleteScreen, deleteScreen,
upsertDefaults, upsertDefaults,
setDefaultsPanelState, setDefaultsPanelState,
} from './queries/dashboards'; } from './dashboards/queries';
export { export {
createPipelineJob, createPipelineJob,
@@ -86,14 +86,14 @@ export {
deletePipelineJob, deletePipelineJob,
deleteTerminalJobsForUser, deleteTerminalJobsForUser,
markInterruptedJobs, markInterruptedJobs,
} from './queries/pipeline-jobs'; } from './pipeline-jobs/queries';
export { export {
appendChatEvent, appendChatEvent,
getChatEventsSince, getChatEventsSince,
getLastChatEventSeq, getLastChatEventSeq,
pruneChatEventsOlderThan, pruneChatEventsOlderThan,
} from './queries/chat-events'; } from './chat-events/queries';
export { export {
listAgentPanels, listAgentPanels,
@@ -105,8 +105,8 @@ export {
markAgentPanelIntroduced, markAgentPanelIntroduced,
deleteAgentPanel, deleteAgentPanel,
toAgentPanelView, toAgentPanelView,
} from './queries/agent-panels'; } from './agent-panels/queries';
export type { AgentPanel, AgentPanelView, CreateAgentPanelInput, UpdateAgentPanelInput } from './queries/agent-panels'; export type { AgentPanel, AgentPanelView, CreateAgentPanelInput, UpdateAgentPanelInput } from './agent-panels/queries';
export { export {
getMusicFavorites, getMusicFavorites,
@@ -122,7 +122,7 @@ export {
deletePlaylist, deletePlaylist,
addPlaylistItems, addPlaylistItems,
setPlaylistItems, setPlaylistItems,
} from './queries/music'; } from './music/queries';
export type { export type {
FavoriteKind, FavoriteKind,
GroupedFavorites, GroupedFavorites,
@@ -130,8 +130,8 @@ export type {
NowPlayingInput, NowPlayingInput,
PlaylistSummary, PlaylistSummary,
Playlist, Playlist,
} from './queries/music'; } from './music/queries';
export { getSoulseekFavorites, addSoulseekFavorite, removeSoulseekFavorite } from './queries/soulseek'; export { getSoulseekFavorites, addSoulseekFavorite, removeSoulseekFavorite } from './soulseek/queries';
export { export {
getSoulseekBrowseSnapshots, getSoulseekBrowseSnapshots,
getSoulseekBrowseSnapshot, getSoulseekBrowseSnapshot,
@@ -144,7 +144,7 @@ export {
getSoulseekBrowseDirFiles, getSoulseekBrowseDirFiles,
getSoulseekBrowseDownload, getSoulseekBrowseDownload,
deleteSoulseekBrowse, deleteSoulseekBrowse,
} from './queries/soulseek'; } from './soulseek/queries';
export type { export type {
BrowseDownloadFile, BrowseDownloadFile,
BrowsedFile, BrowsedFile,
@@ -154,7 +154,7 @@ export type {
BrowseLevel, BrowseLevel,
BrowseTreeSearch, BrowseTreeSearch,
SoulseekBrowseSnapshot, SoulseekBrowseSnapshot,
} from './queries/soulseek'; } from './soulseek/queries';
export { export {
listHeadscaleServers, listHeadscaleServers,
getActiveHeadscaleCredentials, getActiveHeadscaleCredentials,
@@ -164,8 +164,8 @@ export {
setActiveHeadscaleServer, setActiveHeadscaleServer,
deleteHeadscaleServer, deleteHeadscaleServer,
recordHeadscaleProbe, recordHeadscaleProbe,
} from './queries/headscale'; } from './headscale/queries';
export type { HeadscaleServer, HeadscaleServerCredentials } from './queries/headscale'; export type { HeadscaleServer, HeadscaleServerCredentials } from './headscale/queries';
export { export {
listInvoiceshelfAccounts, listInvoiceshelfAccounts,
getActiveInvoiceshelfCredentials, getActiveInvoiceshelfCredentials,
@@ -175,8 +175,8 @@ export {
setActiveInvoiceshelfAccount, setActiveInvoiceshelfAccount,
deleteInvoiceshelfAccount, deleteInvoiceshelfAccount,
recordInvoiceshelfProbe, recordInvoiceshelfProbe,
} from './queries/invoiceshelf'; } from './invoiceshelf/queries';
export type { InvoiceshelfAccount, InvoiceshelfCredentials } from './queries/invoiceshelf'; export type { InvoiceshelfAccount, InvoiceshelfCredentials } from './invoiceshelf/queries';
export { export {
listJellyfinServers, listJellyfinServers,
getActiveJellyfinCredentials, getActiveJellyfinCredentials,
@@ -186,8 +186,8 @@ export {
setActiveJellyfinServer, setActiveJellyfinServer,
deleteJellyfinServer, deleteJellyfinServer,
recordJellyfinProbe, recordJellyfinProbe,
} from './queries/jellyfin'; } from './jellyfin/queries';
export type { JellyfinServer, JellyfinCredentials } from './queries/jellyfin'; export type { JellyfinServer, JellyfinCredentials } from './jellyfin/queries';
export { export {
listPhotosAccounts, listPhotosAccounts,
getActivePhotosCredentials, getActivePhotosCredentials,
@@ -197,16 +197,16 @@ export {
setActivePhotosAccount, setActivePhotosAccount,
deletePhotosAccount, deletePhotosAccount,
recordPhotosProbe, recordPhotosProbe,
} from './queries/photos'; } from './photos/queries';
export type { PhotosAccount, PhotosCredentials } from './queries/photos'; export type { PhotosAccount, PhotosCredentials } from './photos/queries';
export { export {
listDavAppPasswords, listDavAppPasswords,
createDavAppPassword, createDavAppPassword,
revokeDavAppPassword, revokeDavAppPassword,
deleteDavAppPassword, deleteDavAppPassword,
verifyDavAppPassword, verifyDavAppPassword,
} from './queries/dav'; } from './dav/queries';
export type { DavAppPassword, DavAppPasswordView } from './queries/dav'; export type { DavAppPassword, DavAppPasswordView } from './dav/queries';
export { export {
getServiceConnection, getServiceConnection,
getServiceCredentials, getServiceCredentials,
@@ -215,17 +215,17 @@ export {
recordServiceProbe, recordServiceProbe,
getServiceInstanceUrl, getServiceInstanceUrl,
getResolvedServiceCredentials, getResolvedServiceCredentials,
} from './queries/service-connections'; } from './service-connections/queries';
export type { ServiceName, ServiceConnection, ServiceCredentials } from './queries/service-connections'; export type { ServiceName, ServiceConnection, ServiceCredentials } from './service-connections/queries';
export { export {
getAllRoleGrants, getAllRoleGrants,
getRoleGrants, getRoleGrants,
setRoleGrant, setRoleGrant,
revokeRoleGrant, revokeRoleGrant,
replaceRoleGrants, replaceRoleGrants,
} from './queries/capabilities'; } from './capabilities/queries';
export type { RoleGrant } from './queries/capabilities'; export type { RoleGrant } from './capabilities/queries';
export type { CapabilityLevelValue } from './schema/capabilities'; export type { CapabilityLevelValue } from './capabilities/schema';
export { export {
getVaultTokens, getVaultTokens,
setVaultTokens, setVaultTokens,
@@ -234,8 +234,8 @@ export {
getVaultUnlockKey, getVaultUnlockKey,
setVaultUnlockKey, setVaultUnlockKey,
clearVaultUnlockKey, clearVaultUnlockKey,
} from './queries/vault'; } from './vault/queries';
export type { VaultTokenSet } from './queries/vault'; export type { VaultTokenSet } from './vault/queries';
export { export {
listWallets, listWallets,
getWallet, getWallet,
@@ -254,7 +254,7 @@ export {
getWalletChainCache, getWalletChainCache,
saveWalletChainCache, saveWalletChainCache,
recordWalletChainError, recordWalletChainError,
} from './queries/wallet'; } from './wallet/queries';
export type { export type {
WalletKind, WalletKind,
WalletSummary, WalletSummary,
@@ -262,13 +262,13 @@ export type {
WalletLabel, WalletLabel,
CreateWalletParams, CreateWalletParams,
WalletChainCache, WalletChainCache,
} from './queries/wallet'; } from './wallet/queries';
export type { WalletChainSnapshot } from './schema/wallet'; export type { WalletChainSnapshot } from './wallet/schema';
// Exported as a value, not just a type: the API and the UI need to enumerate the roles, and the // Exported as a value, not just a type: the API and the UI need to enumerate the roles, and the
// column definition is the only place that list should exist. // column definition is the only place that list should exist.
export { USER_ROLES, OWNER_USER_ID } from './schema/auth'; export { USER_ROLES, OWNER_USER_ID } from './auth/schema';
export type { UserRole } from './schema/auth'; export type { UserRole } from './auth/schema';
export { db } from './db'; export { db } from './db';
export * as schema from './schema'; export * as schema from './schema';
@@ -279,7 +279,7 @@ export {
deletePushDevice, deletePushDevice,
recordPushFailure, recordPushFailure,
markPushDeviceSeen, markPushDeviceSeen,
} from './queries/notify'; } from './notify/queries';
export type { PushDeviceSelect, PushDeviceInsert } from './types'; export type { PushDeviceSelect, PushDeviceInsert } from './types';
// App store — what the owner has installed, and whether it should be running. // App store — what the owner has installed, and whether it should be running.
@@ -294,4 +294,4 @@ export {
setEnabled, setEnabled,
removeInstall, removeInstall,
type SidecarInstall, type SidecarInstall,
} from './queries/sidecar-installs'; } from './app-store/queries';
@@ -1,7 +1,8 @@
import { eq, and, sql } from 'drizzle-orm'; import { eq, and, sql } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { serverIntegrations, userIntegrations } from '../schema'; import { serverIntegrations } from '../server/schema';
import { users } from '../schema/auth'; import { userIntegrations } from '../user-data/schema';
import { users } from '../auth/schema';
import type { ServerIntegrationSelect, UserIntegrationSelect } from '../types'; import type { ServerIntegrationSelect, UserIntegrationSelect } from '../types';
// ── Server Integrations ── // ── Server Integrations ──
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm'; import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { invoiceshelfAccounts } from '../schema/invoiceshelf'; import { invoiceshelfAccounts } from './schema';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// InvoiceShelf account registry for the officer-invoiceshelf sidecar. Callers deal in PLAINTEXT — encryption // InvoiceShelf account registry for the officer-invoiceshelf sidecar. Callers deal in PLAINTEXT — encryption
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { users } from './auth'; import { users } from '../auth/schema';
// The InvoiceShelf accounts behind /invoices, for the officer-invoiceshelf sidecar. // The InvoiceShelf accounts behind /invoices, for the officer-invoiceshelf sidecar.
// //
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm'; import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { jellyfinServers } from '../schema/jellyfin'; import { jellyfinServers } from './schema';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// Jellyfin server registry for the officer-jellyfin sidecar. Callers deal in PLAINTEXT — encryption to and // Jellyfin server registry for the officer-jellyfin sidecar. Callers deal in PLAINTEXT — encryption to and
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { users } from './auth'; import { users } from '../auth/schema';
// The Jellyfin servers behind /jellyfin, for the officer-jellyfin sidecar. // The Jellyfin servers behind /jellyfin, for the officer-jellyfin sidecar.
// //
@@ -1,6 +1,6 @@
import { eq, and, desc, asc, sql } from 'drizzle-orm'; import { eq, and, desc, asc, sql } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { musicFavorites, musicNowPlaying, musicPlaylists, musicPlaylistItems } from '../schema/music'; import { musicFavorites, musicNowPlaying, musicPlaylists, musicPlaylistItems } from './schema';
export type FavoriteKind = 'track' | 'album' | 'artist'; export type FavoriteKind = 'track' | 'album' | 'artist';
export type GroupedFavorites = { tracks: string[]; albums: string[]; artists: string[] }; export type GroupedFavorites = { tracks: string[]; albums: string[]; artists: string[] };
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, real, timestamp, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, real, timestamp, index, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
// Per-user music favorites. `key` is an opaque path the app supplies and the server never interprets: // Per-user music favorites. `key` is an opaque path the app supplies and the server never interprets:
// track → homePath "Music/<rel>/<file>" (also the /stream path + RNTP queue id) // track → homePath "Music/<rel>/<file>" (also the /stream path + RNTP queue id)
@@ -1,6 +1,6 @@
import { eq, and, sql } from 'drizzle-orm'; import { eq, and, sql } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { pushDevices } from '../schema/notify'; import { pushDevices } from './schema';
import type { PushDeviceSelect, PushDeviceInsert } from '../types'; import type { PushDeviceSelect, PushDeviceInsert } from '../types';
// The push device registry. Only the officer-notify sidecar uses these. // The push device registry. Only the officer-notify sidecar uses these.
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, timestamp, index, check, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, timestamp, index, check, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { users } from './auth'; import { users } from '../auth/schema';
// Devices that can receive a push, for the officer-notify sidecar. // Devices that can receive a push, for the officer-notify sidecar.
// //
@@ -1,5 +1,5 @@
import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core'; import { pgTable, serial, text, integer, boolean, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
export const taskLogs = pgTable('task_logs', { export const taskLogs = pgTable('task_logs', {
id: serial('id').primaryKey(), id: serial('id').primaryKey(),
@@ -1,6 +1,6 @@
import { eq, and, desc } from 'drizzle-orm'; import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { photosConfig } from '../schema/photos'; import { photosConfig } from './schema';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// Immich account registry for the officer-photos sidecar. Callers deal in PLAINTEXT — encryption to and from // Immich account registry for the officer-photos sidecar. Callers deal in PLAINTEXT — encryption to and from
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { users } from './auth'; import { users } from '../auth/schema';
// The Immich accounts behind /photos, for the officer-photos sidecar. // The Immich accounts behind /photos, for the officer-photos sidecar.
// //
@@ -2,7 +2,7 @@ import { eq, and, inArray, asc, desc } from 'drizzle-orm';
const TERMINAL_STATUSES = ['completed', 'failed', 'stopped', 'interrupted'] as const; const TERMINAL_STATUSES = ['completed', 'failed', 'stopped', 'interrupted'] as const;
import { db } from '../db'; import { db } from '../db';
import { pipelineJobs } from '../schema/pipeline-jobs'; import { pipelineJobs } from './schema';
import type { PipelineJobInsert } from '../types'; import type { PipelineJobInsert } from '../types';
export async function createPipelineJob(data: PipelineJobInsert) { export async function createPipelineJob(data: PipelineJobInsert) {
@@ -1,5 +1,5 @@
import { pgTable, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core'; import { pgTable, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
export const pipelineJobs = pgTable( export const pipelineJobs = pgTable(
'pipeline_jobs', 'pipeline_jobs',
@@ -21,35 +21,35 @@
// ── Core ───────────────────────────────────────────────────────────────────────────────────────── // ── Core ─────────────────────────────────────────────────────────────────────────────────────────
export * from './auth'; // users, passkeys, passkey_challenges, token_blacklist export * from './auth/schema'; // users, passkeys, passkey_challenges, token_blacklist
export * from './capabilities'; // role_capabilities — what each ROLE may reach export * from './capabilities/schema'; // role_capabilities — what each ROLE may reach
export * from './api-keys'; // api_keys export * from './api-keys/schema'; // api_keys
export * from './user-data'; // user_settings, user_state, user_integrations, dock_configs export * from './user-data/schema'; // user_settings, user_state, user_integrations, dock_configs
export * from './dashboards'; // dashboards, screens, dashboard_defaults export * from './dashboards/schema'; // dashboards, screens, dashboard_defaults
export * from './server'; // server_config (SMTP lives here), server_integrations export * from './server/schema'; // server_config (SMTP lives here), server_integrations
export * from './operations'; // task_logs, queue_jobs, terminal_containers export * from './operations/schema'; // task_logs, queue_jobs, terminal_containers
export * from './pipeline-jobs'; // pipeline_jobs export * from './pipeline-jobs/schema'; // pipeline_jobs
export * from './chat-events'; // chat_session_events export * from './chat-events/schema'; // chat_session_events
export * from './agent-panels'; // agent_panels export * from './agent-panels/schema'; // agent_panels
// Core because the tailnet is the perimeter — a security model resting on it cannot treat administering // Core because the tailnet is the perimeter — a security model resting on it cannot treat administering
// it as an optional extra. The secret store bootstraps a `headscale` key on this basis. // it as an optional extra. The secret store bootstraps a `headscale` key on this basis.
export * from './headscale'; // headscale_servers export * from './headscale/schema'; // headscale_servers
// The app store itself, and the credentials it stores for what it installs. `app-store/effects.ts` // The app store itself, and the credentials it stores for what it installs. `app-store/effects.ts`
// reads service_connections, so this is core however few plugins are installed. // reads service_connections, so this is core however few plugins are installed.
export * from './app-store'; // sidecar_installs export * from './app-store/schema'; // sidecar_installs
export * from './service-connections'; // service_connections export * from './service-connections/schema'; // service_connections
// ── Plugins — uncomment when the plugin is installed ───────────────────────────────────────────── // ── Plugins — uncomment when the plugin is installed ─────────────────────────────────────────────
// export * from './email'; // email_accounts officer-email // export * from './email/schema'; // email_accounts officer-email
// export * from './music'; // music_favorites, _playlists, _playlist_items, _now_playing // export * from './music/schema'; // music_favorites, _playlists, _playlist_items, _now_playing
// export * from './notify'; // push_devices officer-notify // export * from './notify/schema'; // push_devices officer-notify
// export * from './dav'; // dav_app_passwords officer-caldav // export * from './dav/schema'; // dav_app_passwords officer-caldav
// export * from './photos'; // photos_config officer-photos // export * from './photos/schema'; // photos_config officer-photos
// export * from './jellyfin'; // jellyfin_servers officer-jellyfin // export * from './jellyfin/schema'; // jellyfin_servers officer-jellyfin
// export * from './invoiceshelf'; // invoiceshelf_accounts officer-invoiceshelf // export * from './invoiceshelf/schema'; // invoiceshelf_accounts officer-invoiceshelf
// export * from './soulseek'; // soulseek_favorites, _browse_snapshots, _browse_dirs // export * from './soulseek/schema'; // soulseek_favorites, _browse_snapshots, _browse_dirs
// export * from './vault'; // vault_tokens, vault_unlock_keys officer-vault // export * from './vault/schema'; // vault_tokens, vault_unlock_keys officer-vault
// export * from './wallet'; // wallet_wallets, _labels, _frozen_utxos, _chain_cache // export * from './wallet/schema'; // wallet_wallets, _labels, _frozen_utxos, _chain_cache
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { serverConfig } from '../schema'; import { serverConfig } from './schema';
const SETTINGS_KEY = 'server-settings'; const SETTINGS_KEY = 'server-settings';
@@ -1,7 +1,7 @@
import { eq, and } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { serviceConnections } from '../schema'; import { serviceConnections } from './schema';
import { getOwnerUser } from './auth'; import { getOwnerUser } from '../auth/queries';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// Single-connection services (transmission, slskd) for their sidecars. Callers deal in PLAINTEXT — // Single-connection services (transmission, slskd) for their sidecars. Callers deal in PLAINTEXT —
@@ -1,5 +1,5 @@
import { pgTable, serial, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
// Where a self-hosted service lives, and what it takes to talk to it — for services the owner has exactly // Where a self-hosted service lives, and what it takes to talk to it — for services the owner has exactly
// ONE of. Transmission and slskd today. // ONE of. Transmission and slskd today.
@@ -1,6 +1,6 @@
import { eq, and, asc, isNull, inArray, sql } from 'drizzle-orm'; import { eq, and, asc, isNull, inArray, sql } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { soulseekFavorites, soulseekBrowseSnapshots, soulseekBrowseDirs } from '../schema/soulseek'; import { soulseekFavorites, soulseekBrowseSnapshots, soulseekBrowseDirs } from './schema';
/** A user's favourited Soulseek peers, alphabetical (the order the UI lists them in). */ /** A user's favourited Soulseek peers, alphabetical (the order the UI lists them in). */
export async function getSoulseekFavorites(userId: number): Promise<string[]> { export async function getSoulseekFavorites(userId: number): Promise<string[]> {
@@ -10,7 +10,7 @@ import {
foreignKey, foreignKey,
uniqueIndex, uniqueIndex,
} from 'drizzle-orm/pg-core'; } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
// Soulseek state that Officer owns because slskd has none. slskd exposes no favourites/buddy-list API // Soulseek state that Officer owns because slskd has none. slskd exposes no favourites/buddy-list API
// (verified against 0.26.0's UsersController: only endpoint/browse/directory/info/status), so the peers // (verified against 0.26.0's UsersController: only endpoint/browse/directory/info/status), so the peers
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { dockConfigs, userSettings, userState } from '../schema'; import { dockConfigs, userSettings, userState } from './schema';
// ── User Settings ── // ── User Settings ──
@@ -1,7 +1,7 @@
import { pgTable, serial, integer, text, timestamp, jsonb, check, uniqueIndex, foreignKey } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, timestamp, jsonb, check, uniqueIndex, foreignKey } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { serverIntegrations } from './server'; import { serverIntegrations } from '../server/schema';
export const userSettings = pgTable('user_settings', { export const userSettings = pgTable('user_settings', {
userId: integer('user_id') userId: integer('user_id')
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { vaultTokens, vaultUnlockKeys } from '../schema/vault'; import { vaultTokens, vaultUnlockKeys } from './schema';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// Vault store access. Callers deal in PLAINTEXT — encryption to/from at-rest ciphertext happens here, so // Vault store access. Callers deal in PLAINTEXT — encryption to/from at-rest ciphertext happens here, so
@@ -1,5 +1,5 @@
import { pgTable, integer, text, timestamp } from 'drizzle-orm/pg-core'; import { pgTable, integer, text, timestamp } from 'drizzle-orm/pg-core';
import { users } from './auth'; import { users } from '../auth/schema';
// Officer Vault server-side state (see VAULT_AUTH_SPEC.md). The device never holds a Vaultwarden token; // Officer Vault server-side state (see VAULT_AUTH_SPEC.md). The device never holds a Vaultwarden token;
// the platform brokers it, stores it here tied to the owner account, and injects it on proxied /api/vault // the platform brokers it, stores it here tied to the owner account, and injects it on proxied /api/vault
@@ -1,7 +1,7 @@
import type { WalletChainSnapshot } from '../schema/wallet'; import type { WalletChainSnapshot } from './schema';
import { eq, and, desc } from 'drizzle-orm'; import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db'; import { db } from '../db';
import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from '../schema/wallet'; import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from './schema';
import { encryptSecret, decryptSecret } from '../crypto'; import { encryptSecret, decryptSecret } from '../crypto';
// Wallet access for the officer-wallet sidecar. Callers deal in PLAINTEXT — the at-rest layer ('wallet' // Wallet access for the officer-wallet sidecar. Callers deal in PLAINTEXT — the at-rest layer ('wallet'
@@ -1,6 +1,6 @@
import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core'; import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { users } from './auth'; import { users } from '../auth/schema';
// Bitcoin wallets for the officer-wallet sidecar. The owner registers one or more wallets — either a // Bitcoin wallets for the officer-wallet sidecar. The owner registers one or more wallets — either a
// self-custodial on-chain wallet whose seed lives here, or a connection to a node (LND / Core Lightning / // self-custodial on-chain wallet whose seed lives here, or a connection to a node (LND / Core Lightning /
+1 -1
View File
@@ -5,7 +5,7 @@
// auth+forward proxy and holds NO InvoiceShelf credentials. // auth+forward proxy and holds NO InvoiceShelf credentials.
// //
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `invoiceshelf_accounts` (see // The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `invoiceshelf_accounts` (see
// databases/officer_db/src/queries/invoiceshelf.ts). It is deliberately no longer read from the environment: // databases/officer_db/src/invoiceshelf/queries.ts). It is deliberately no longer read from the environment:
// Bun auto-loads `.env` into every process started in the platform directory, so an `INVOICESHELF_TOKEN` // Bun auto-loads `.env` into every process started in the platform directory, so an `INVOICESHELF_TOKEN`
// there was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no // there was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no
// code to use it and the largest attack surface in the system. Nothing in this file reads process.env. // code to use it and the largest attack surface in the system. Nothing in this file reads process.env.
+1 -1
View File
@@ -5,7 +5,7 @@
// thin auth+forward proxy and holds NO Jellyfin credentials. // thin auth+forward proxy and holds NO Jellyfin credentials.
// //
// The server is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `jellyfin_servers` (see // The server is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `jellyfin_servers` (see
// databases/officer_db/src/queries/jellyfin.ts). Nothing in this file reads process.env — Bun auto-loads // databases/officer_db/src/jellyfin/queries.ts). Nothing in this file reads process.env — Bun auto-loads
// `.env` into every process started in the platform directory, so a token there would also be sitting in // `.env` into every process started in the platform directory, so a token there would also be sitting in
// `officer`'s own environment: a credential held by the one process that has no code to use it. // `officer`'s own environment: a credential held by the one process that has no code to use it.
// //
+1 -1
View File
@@ -4,7 +4,7 @@
// auth+forward proxy and holds NO Immich credentials. // auth+forward proxy and holds NO Immich credentials.
// //
// The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `photos_config` (see // The instance is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `photos_config` (see
// databases/officer_db/src/queries/photos.ts). It is deliberately no longer read from the environment: // databases/officer_db/src/photos/queries.ts). It is deliberately no longer read from the environment:
// Bun auto-loads `.env` into every process started in the platform directory, so an `IMMICH_API_KEY` there // Bun auto-loads `.env` into every process started in the platform directory, so an `IMMICH_API_KEY` there
// was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no code // was also sitting in `officer`'s own `process.env` — a credential held by the one process that has no code
// to use it and the largest attack surface in the system. Nothing in this file reads process.env. // to use it and the largest attack surface in the system. Nothing in this file reads process.env.
+1 -1
View File
@@ -6,7 +6,7 @@ import { getServiceCredentials } from 'officerdb';
// auth+forward proxy and holds NO slskd credentials. // auth+forward proxy and holds NO slskd credentials.
// //
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `service_connections` (see // The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored encrypted in `service_connections` (see
// databases/officer_db/src/queries/service-connections.ts). It is deliberately no longer read from the // databases/officer_db/src/service-connections/queries.ts). It is deliberately no longer read from the
// environment: Bun auto-loads `.env` into every process started in the platform directory, so an // environment: Bun auto-loads `.env` into every process started in the platform directory, so an
// `SLSKD_API_KEY` there was also sitting in `officer`'s own process.env — a credential that drives the whole // `SLSKD_API_KEY` there was also sitting in `officer`'s own process.env — a credential that drives the whole
// Soulseek daemon, held by the one process with no code to use it. Nothing in this file reads process.env. // Soulseek daemon, held by the one process with no code to use it. Nothing in this file reads process.env.
+1 -1
View File
@@ -6,7 +6,7 @@ import { getServiceCredentials } from 'officerdb';
// a thin auth+forward proxy and holds NO Transmission credentials. // a thin auth+forward proxy and holds NO Transmission credentials.
// //
// The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored in `service_connections` (see // The daemon is CONFIGURED BY THE OWNER FROM THE UI and stored in `service_connections` (see
// databases/officer_db/src/queries/service-connections.ts), no longer read from the environment: Bun // databases/officer_db/src/service-connections/queries.ts), no longer read from the environment: Bun
// auto-loads `.env` into every process started in the platform directory, so TRANSMISSION_* was also // auto-loads `.env` into every process started in the platform directory, so TRANSMISSION_* was also
// sitting in `officer`'s own process.env, and pointing Officer at a daemon meant editing a file on the // sitting in `officer`'s own process.env, and pointing Officer at a daemon meant editing a file on the
// server. Nothing in this file reads process.env. // server. Nothing in this file reads process.env.
+1 -1
View File
@@ -4,7 +4,7 @@ import { getKey } from 'officerdb/secret-store';
// The ONLY reader of WALLET_* env in the tree. Everything else — node URLs, macaroons, runes, LNDHub // The ONLY reader of WALLET_* env in the tree. Everything else — node URLs, macaroons, runes, LNDHub
// credentials, NWC URIs — is per-wallet configuration the owner enters at runtime and lives encrypted in // credentials, NWC URIs — is per-wallet configuration the owner enters at runtime and lives encrypted in
// Postgres (databases/officer_db/src/schema/wallet.ts), not here. Env holds only what is genuinely // Postgres (databases/officer_db/src/wallet/schema.ts), not here. Env holds only what is genuinely
// deployment-wide: which chain we're on. // deployment-wide: which chain we're on.
// //
// WHERE CHAIN DATA COMES FROM IS NOT ENV. It used to be WALLET_ESPLORA_URL, which meant the one setting // WHERE CHAIN DATA COMES FROM IS NOT ENV. It used to be WALLET_ESPLORA_URL, which meant the one setting
@@ -1,7 +1,7 @@
// Shared types/constants for the /wallet workspace panels. // Shared types/constants for the /wallet workspace panels.
// //
// The wire shapes mirror src/servers/sidecar/wallet/types.ts and the WalletSummary projection in // The wire shapes mirror src/servers/sidecar/wallet/types.ts and the WalletSummary projection in
// src/databases/officer_db/src/queries/wallet.ts. They are restated here rather than imported because the // src/databases/officer_db/src/wallet/queries.ts. They are restated here rather than imported because the
// officerdev workspace has no path into src/servers — pulling the sidecar's module graph into the browser // officerdev workspace has no path into src/servers — pulling the sidecar's module graph into the browser
// bundle would drag bitcoinjs-lib and the key handling along with it, which is exactly what must never // bundle would drag bitcoinjs-lib and the key handling along with it, which is exactly what must never
// reach the client. Keep this file in step with those two by hand; the field names are identical on // reach the client. Keep this file in step with those two by hand; the field names are identical on