@corgelabs/corge 0.0.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/index.d.ts +3 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +3 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/schema.d.ts +83 -0
- package/dist/ai/schema.d.ts.map +1 -0
- package/dist/ai/schema.js +19 -0
- package/dist/ai/schema.js.map +1 -0
- package/dist/ai/service.d.ts +14 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/service.js +266 -0
- package/dist/ai/service.js.map +1 -0
- package/dist/ai/utils.d.ts +10 -0
- package/dist/ai/utils.d.ts.map +1 -0
- package/dist/ai/utils.js +27 -0
- package/dist/ai/utils.js.map +1 -0
- package/dist/attachments/index.d.ts +4 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +4 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/routes.d.ts +75 -0
- package/dist/attachments/routes.d.ts.map +1 -0
- package/dist/attachments/routes.js +18 -0
- package/dist/attachments/routes.js.map +1 -0
- package/dist/attachments/schema.d.ts +61 -0
- package/dist/attachments/schema.d.ts.map +1 -0
- package/dist/attachments/schema.js +30 -0
- package/dist/attachments/schema.js.map +1 -0
- package/dist/attachments/service.d.ts +27 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +224 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/utils.d.ts +9 -0
- package/dist/attachments/utils.d.ts.map +1 -0
- package/dist/attachments/utils.js +80 -0
- package/dist/attachments/utils.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oidc.d.ts +25 -0
- package/dist/auth/oidc.d.ts.map +1 -0
- package/dist/auth/oidc.js +298 -0
- package/dist/auth/oidc.js.map +1 -0
- package/dist/auth/recovery.d.ts +41 -0
- package/dist/auth/recovery.d.ts.map +1 -0
- package/dist/auth/recovery.js +250 -0
- package/dist/auth/recovery.js.map +1 -0
- package/dist/auth/routes.d.ts +226 -0
- package/dist/auth/routes.d.ts.map +1 -0
- package/dist/auth/routes.js +96 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/schema.d.ts +158 -0
- package/dist/auth/schema.d.ts.map +1 -0
- package/dist/auth/schema.js +82 -0
- package/dist/auth/schema.js.map +1 -0
- package/dist/auth/service.d.ts +96 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +129 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/session.d.ts +51 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +344 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/utils.d.ts +17 -0
- package/dist/auth/utils.d.ts.map +1 -0
- package/dist/auth/utils.js +96 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/bin/corge.js +542 -0
- package/dist/chat/context.d.ts +10 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +221 -0
- package/dist/chat/context.js.map +1 -0
- package/dist/chat/index.d.ts +4 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +4 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/routes.d.ts +160 -0
- package/dist/chat/routes.d.ts.map +1 -0
- package/dist/chat/routes.js +210 -0
- package/dist/chat/routes.js.map +1 -0
- package/dist/chat/schema.d.ts +285 -0
- package/dist/chat/schema.d.ts.map +1 -0
- package/dist/chat/schema.js +74 -0
- package/dist/chat/schema.js.map +1 -0
- package/dist/chat/service.d.ts +64 -0
- package/dist/chat/service.d.ts.map +1 -0
- package/dist/chat/service.js +279 -0
- package/dist/chat/service.js.map +1 -0
- package/dist/chat/stream.d.ts +53 -0
- package/dist/chat/stream.d.ts.map +1 -0
- package/dist/chat/stream.js +339 -0
- package/dist/chat/stream.js.map +1 -0
- package/dist/chat/utils.d.ts +61 -0
- package/dist/chat/utils.d.ts.map +1 -0
- package/dist/chat/utils.js +285 -0
- package/dist/chat/utils.js.map +1 -0
- package/dist/cli/builder.d.ts +6 -0
- package/dist/cli/builder.d.ts.map +1 -0
- package/dist/cli/builder.js +91 -0
- package/dist/cli/builder.js.map +1 -0
- package/dist/cli/deployer.d.ts +2 -0
- package/dist/cli/deployer.d.ts.map +1 -0
- package/dist/cli/deployer.js +152 -0
- package/dist/cli/deployer.js.map +1 -0
- package/dist/cli/dev-server.d.ts +2 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +70 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/env.d.ts +4 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +97 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config.d.ts +262 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +212 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +410 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +294 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/service.d.ts +42 -0
- package/dist/db/service.d.ts.map +1 -0
- package/dist/db/service.js +372 -0
- package/dist/db/service.js.map +1 -0
- package/dist/db/utils.d.ts +25 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +188 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/emails/index.d.ts +4 -0
- package/dist/emails/index.d.ts.map +1 -0
- package/dist/emails/index.js +4 -0
- package/dist/emails/index.js.map +1 -0
- package/dist/emails/routes.d.ts +191 -0
- package/dist/emails/routes.d.ts.map +1 -0
- package/dist/emails/routes.js +39 -0
- package/dist/emails/routes.js.map +1 -0
- package/dist/emails/schema.d.ts +237 -0
- package/dist/emails/schema.d.ts.map +1 -0
- package/dist/emails/schema.js +98 -0
- package/dist/emails/schema.js.map +1 -0
- package/dist/emails/service.d.ts +22 -0
- package/dist/emails/service.d.ts.map +1 -0
- package/dist/emails/service.js +337 -0
- package/dist/emails/service.js.map +1 -0
- package/dist/emails/utils.d.ts +49 -0
- package/dist/emails/utils.d.ts.map +1 -0
- package/dist/emails/utils.js +341 -0
- package/dist/emails/utils.js.map +1 -0
- package/dist/files/index.d.ts +4 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +4 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/routes.d.ts +72 -0
- package/dist/files/routes.d.ts.map +1 -0
- package/dist/files/routes.js +38 -0
- package/dist/files/routes.js.map +1 -0
- package/dist/files/schema.d.ts +57 -0
- package/dist/files/schema.d.ts.map +1 -0
- package/dist/files/schema.js +39 -0
- package/dist/files/schema.js.map +1 -0
- package/dist/files/service.d.ts +27 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +175 -0
- package/dist/files/service.js.map +1 -0
- package/dist/files/utils.d.ts +3 -0
- package/dist/files/utils.d.ts.map +1 -0
- package/dist/files/utils.js +63 -0
- package/dist/files/utils.js.map +1 -0
- package/dist/index.d.ts +875 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9919 -0
- package/dist/index.js.map +7 -0
- package/dist/payments/billing.d.ts +29 -0
- package/dist/payments/billing.d.ts.map +1 -0
- package/dist/payments/billing.js +378 -0
- package/dist/payments/billing.js.map +1 -0
- package/dist/payments/index.d.ts +4 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +4 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/routes.d.ts +340 -0
- package/dist/payments/routes.d.ts.map +1 -0
- package/dist/payments/routes.js +56 -0
- package/dist/payments/routes.js.map +1 -0
- package/dist/payments/schema.d.ts +412 -0
- package/dist/payments/schema.d.ts.map +1 -0
- package/dist/payments/schema.js +260 -0
- package/dist/payments/schema.js.map +1 -0
- package/dist/payments/service.d.ts +31 -0
- package/dist/payments/service.d.ts.map +1 -0
- package/dist/payments/service.js +322 -0
- package/dist/payments/service.js.map +1 -0
- package/dist/payments/utils.d.ts +61 -0
- package/dist/payments/utils.d.ts.map +1 -0
- package/dist/payments/utils.js +369 -0
- package/dist/payments/utils.js.map +1 -0
- package/dist/router/guard.d.ts +19 -0
- package/dist/router/guard.d.ts.map +1 -0
- package/dist/router/guard.js +243 -0
- package/dist/router/guard.js.map +1 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/schema.d.ts +103 -0
- package/dist/router/schema.d.ts.map +1 -0
- package/dist/router/schema.js +282 -0
- package/dist/router/schema.js.map +1 -0
- package/dist/router/service.d.ts +25 -0
- package/dist/router/service.d.ts.map +1 -0
- package/dist/router/service.js +339 -0
- package/dist/router/service.js.map +1 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/routes.d.ts +54 -0
- package/dist/tasks/routes.d.ts.map +1 -0
- package/dist/tasks/routes.js +195 -0
- package/dist/tasks/routes.js.map +1 -0
- package/dist/tasks/schema.d.ts +261 -0
- package/dist/tasks/schema.d.ts.map +1 -0
- package/dist/tasks/schema.js +35 -0
- package/dist/tasks/schema.js.map +1 -0
- package/dist/tasks/service.d.ts +32 -0
- package/dist/tasks/service.d.ts.map +1 -0
- package/dist/tasks/service.js +201 -0
- package/dist/tasks/service.js.map +1 -0
- package/dist/tasks/steps.d.ts +18 -0
- package/dist/tasks/steps.d.ts.map +1 -0
- package/dist/tasks/steps.js +365 -0
- package/dist/tasks/steps.js.map +1 -0
- package/dist/tasks/utils.d.ts +75 -0
- package/dist/tasks/utils.d.ts.map +1 -0
- package/dist/tasks/utils.js +311 -0
- package/dist/tasks/utils.js.map +1 -0
- package/dist/tasks/watcher.d.ts +13 -0
- package/dist/tasks/watcher.d.ts.map +1 -0
- package/dist/tasks/watcher.js +255 -0
- package/dist/tasks/watcher.js.map +1 -0
- package/dist/tasks/worker.d.ts +28 -0
- package/dist/tasks/worker.d.ts.map +1 -0
- package/dist/tasks/worker.js +326 -0
- package/dist/tasks/worker.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/schema.d.ts +79 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +41 -0
- package/dist/utils/schema.js.map +1 -0
- package/dist/utils/service.d.ts +32 -0
- package/dist/utils/service.d.ts.map +1 -0
- package/dist/utils/service.js +384 -0
- package/dist/utils/service.js.map +1 -0
- package/docs/SDK_SPEC.md +1645 -0
- package/infra/firestore.base.indexes.json +138 -0
- package/infra/firestore.base.rules +152 -0
- package/infra/storage.base.rules +8 -0
- package/package.json +58 -13
- package/src/ai/index.ts +2 -0
- package/src/ai/schema.ts +64 -0
- package/src/ai/service.ts +317 -0
- package/src/ai/utils.ts +38 -0
- package/src/attachments/index.ts +3 -0
- package/src/attachments/routes.ts +28 -0
- package/src/attachments/schema.ts +45 -0
- package/src/attachments/service.ts +315 -0
- package/src/attachments/utils.ts +115 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oidc.ts +419 -0
- package/src/auth/recovery.ts +365 -0
- package/src/auth/routes.ts +142 -0
- package/src/auth/schema.ts +181 -0
- package/src/auth/service.ts +183 -0
- package/src/auth/session.ts +414 -0
- package/src/auth/utils.ts +106 -0
- package/src/chat/context.ts +310 -0
- package/src/chat/index.ts +3 -0
- package/src/chat/routes.ts +264 -0
- package/src/chat/schema.ts +295 -0
- package/src/chat/service.ts +360 -0
- package/src/chat/stream.ts +481 -0
- package/src/chat/utils.ts +370 -0
- package/src/cli/builder.ts +93 -0
- package/src/cli/deployer.ts +184 -0
- package/src/cli/dev-server.ts +77 -0
- package/src/cli/env.ts +89 -0
- package/src/cli/init.ts +121 -0
- package/src/config.ts +447 -0
- package/src/db/index.ts +2 -0
- package/src/db/schema.ts +409 -0
- package/src/db/service.ts +477 -0
- package/src/db/utils.ts +250 -0
- package/src/emails/index.ts +3 -0
- package/src/emails/routes.ts +64 -0
- package/src/emails/schema.ts +239 -0
- package/src/emails/service.ts +488 -0
- package/src/emails/utils.ts +449 -0
- package/src/files/index.ts +3 -0
- package/src/files/routes.ts +44 -0
- package/src/files/schema.ts +76 -0
- package/src/files/service.ts +238 -0
- package/src/files/utils.ts +77 -0
- package/src/index.ts +425 -0
- package/src/payments/billing.ts +488 -0
- package/src/payments/index.ts +3 -0
- package/src/payments/routes.ts +92 -0
- package/src/payments/schema.ts +401 -0
- package/src/payments/service.ts +445 -0
- package/src/payments/utils.ts +472 -0
- package/src/router/guard.ts +319 -0
- package/src/router/index.ts +2 -0
- package/src/router/schema.ts +458 -0
- package/src/router/service.ts +453 -0
- package/src/tasks/index.ts +11 -0
- package/src/tasks/routes.ts +230 -0
- package/src/tasks/schema.ts +310 -0
- package/src/tasks/service.ts +254 -0
- package/src/tasks/steps.ts +472 -0
- package/src/tasks/utils.ts +445 -0
- package/src/tasks/watcher.ts +307 -0
- package/src/tasks/worker.ts +461 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/schema.ts +69 -0
- package/src/utils/service.ts +461 -0
- package/tests/e2e.test.ts +2531 -0
- package/tsconfig.base.json +12 -0
- package/README.md +0 -2
- package/index.d.ts +0 -1
- package/index.js +0 -1
package/docs/SDK_SPEC.md
ADDED
|
@@ -0,0 +1,1645 @@
|
|
|
1
|
+
# Core SDK v2 (`@core`) — Complete Architecture, Methods Map & Per-Function Specification (`SDK_SPEC.md`)
|
|
2
|
+
|
|
3
|
+
> **Version**: 2.3 (Flat Domain Barrel & `<= 500 LOC` Class-Composed Architecture, Methods Map & Per-Function Specification)
|
|
4
|
+
> **Runtime Stack**: Node.js 24 LTS (`nodejs24` GA on Cloud Functions 2nd Gen) · Hono · Cloud Tasks · Cloud Firestore Enterprise · Firebase Storage · Vertex AI (`claude-haiku-5-5`, `claude-sonnet-4-5`, `text-embedding-005`) · Mistral OCR (`mistral-ocr-latest`) · TypeSafe Jev (`jev-latest`) · Jina Reader (`r.jina.ai`)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. [Core Design Philosophy & 4-Tier Visibility Model](#1-core-design-philosophy--4-tier-visibility-model)
|
|
11
|
+
2. [Project & Module Topology (`src/core/` Flat Domain Barrel Map)](#2-project--module-topology-srccore-flat-domain-barrel-map)
|
|
12
|
+
3. [Master `CoreClient`, HTTP Route & Runtime State Lookup Matrices](#3-master-coreclient-http-route--runtime-state-lookup-matrices)
|
|
13
|
+
- [3.1 Master `CoreClient` Public Surface Map (`core.*` & `useCore()`)](#31-master-coreclient-public-surface-map-core--usecore)
|
|
14
|
+
- [3.2 Master `core.internals.*` Engine Hooks Map](#32-master-coreinternals-engine-hooks-map)
|
|
15
|
+
- [3.3 Master Built-In HTTP Routes (`CORE_ROUTE_DEFAULTS` — 51 Endpoints)](#33-master-built-in-http-routes-core_route_defaults--51-endpoints)
|
|
16
|
+
- [3.4 Master In-Memory Registries, Caches & `AsyncLocalStorage` Stores](#34-master-in-memory-registries-caches--asynclocalstorage-stores)
|
|
17
|
+
4. [Subsystem Architecture & Call-Graph Diagrams (Mermaid)](#4-subsystem-architecture--call-graph-diagrams-mermaid)
|
|
18
|
+
- [4.1 Request & Route Execution Pipeline (`rest` vs `workflow`)](#41-request--route-execution-pipeline-rest-vs-workflow)
|
|
19
|
+
- [4.2 Durable Workflow Engine, Epoch Fencing & Saga Compensation](#42-durable-workflow-engine-epoch-fencing--saga-compensation)
|
|
20
|
+
- [4.3 Attachment Scanning & Sharded Fan-Out Pipeline](#43-attachment-scanning--sharded-fan-out-pipeline)
|
|
21
|
+
- [4.4 Declarative AI Agent, Hybrid RAG Context & Streaming Pipeline](#44-declarative-ai-agent-hybrid-rag-context--streaming-pipeline)
|
|
22
|
+
- [4.5 Authentication, OIDC Social Login, Token Cache & Revocation Sync](#45-authentication-oidc-social-login-token-cache--revocation-sync)
|
|
23
|
+
5. [Entrypoint, Core Class & Config Module (`index.ts`, `config.ts`) — Per-Function Spec](#5-entrypoint-core-class--config-module-indexts-configts--per-function-spec)
|
|
24
|
+
6. [Database & RBAC Module (`src/core/db/*`) — Per-Function Spec](#6-database--rbac-module-srccoredb--per-function-spec)
|
|
25
|
+
7. [Files & Content-Addressed Storage Module (`src/core/files/*`) — Per-Function Spec](#7-files--content-addressed-storage-module-srccorefiles--per-function-spec)
|
|
26
|
+
8. [AI Primitives Module (`src/core/ai/*`) — Per-Function Spec](#8-ai-primitives-module-srccoreai--per-function-spec)
|
|
27
|
+
9. [Attachments Scanning & Batch Fan-Out Module (`src/core/attachments/*`) — Per-Function Spec](#9-attachments-scanning--batch-fan-out-module-srccoreattachments--per-function-spec)
|
|
28
|
+
10. [Authentication, OIDC Social & Account Recovery Module (`src/core/auth/*`) — Per-Function Spec](#10-authentication-oidc-social--account-recovery-module-srccoreauth--per-function-spec)
|
|
29
|
+
11. [Chat, Agents, InfoBase Context, MCP & OpenAI Module (`src/core/chat/*`) — Per-Function Spec](#11-chat-agents-infobase-context-mcp--openai-module-srccorechat--per-function-spec)
|
|
30
|
+
12. [Durable Workflows, StepRunner, Cloud Tasks & Watcher Module (`src/core/tasks/*`) — Per-Function Spec](#12-durable-workflows-steprunner-cloud-tasks--watcher-module-srccoretasks--per-function-spec)
|
|
31
|
+
13. [Router, Security Guards & Cloud Functions Module (`src/core/router/*`) — Per-Function Spec](#13-router-security-guards--cloud-functions-module-srccorerouter--per-function-spec)
|
|
32
|
+
14. [Resilience, Rate Limiting, Circuit Breaker & Error Utilities (`src/core/utils/*`) + End-to-End DX Examples](#14-resilience-rate-limiting-circuit-breaker--error-utilities-srccoreutils--end-to-end-dx-examples)
|
|
33
|
+
15. [Transactional Emails & Payments Modules (`src/core/emails/*`, `src/core/payments/*`) — Per-Function Spec](#15-transactional-emails--payments-modules-srccoreemails-srccorepayments--per-function-spec)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Core Design Philosophy & 4-Tier Visibility Model
|
|
38
|
+
|
|
39
|
+
### 1.1 Single `@core` Barrel + Ambient Context Propagation (`core.*` & `useCore()`)
|
|
40
|
+
|
|
41
|
+
1. **Single Public Entrypoint (`import { Core, core, useCore } from '@core'`)**:
|
|
42
|
+
- `src/core/index.ts` is the sole public entrypoint defining `Core`, `core`, `useCore`, and the `core.types.*` namespace, composed directly from domain `*Service` classes (`ErrorsService`, `ResilienceService`, `DbService`, `FilesService`, `AiService`, `AttachmentsService`, `AuthService`, `ChatService`, `TasksService`, `RouterService`).
|
|
43
|
+
- Application code (`src/config/*`, `src/routes/*`, `src/services/*`, `src/utils/*`, `src/index.ts`) imports exclusively from `@core` and never reaches into internal sub-paths (`@core/db/*`, `@core/tasks/*`, etc.).
|
|
44
|
+
2. **Ambient Execution State via `AsyncLocalStorage` + Class Composition (`this.#core`)**:
|
|
45
|
+
- Every HTTP route, MCP tool invocation, AI agent tool call, and Cloud Tasks workflow worker executes inside `runWithCore({ env, userId, role, step }, fn)`.
|
|
46
|
+
- Calling `core.<ns>.<method>()` on the singleton `core` (or `const coreClient = useCore()`) dynamically resolves `#state` from `coreStorage.getStore()` → `tasks.getActiveWorkflowStore()` → fallback dev/prod admin context with zero prop-drilling.
|
|
47
|
+
- Each domain `*Service` class receives `coreClient: Core` in its constructor (`readonly #core: Core`), calling sibling services and reading `this.#core.env`, `this.#core.userId`, `this.#core.role`, `this.#core.caller`, and `this.#core.step` directly without circular factory injection.
|
|
48
|
+
|
|
49
|
+
### 1.2 The 4-Tier Function Visibility Taxonomy
|
|
50
|
+
|
|
51
|
+
Every function, method, class, and closure across the flat domain barrel files in `src/core/` belongs to one of four strict architectural tiers:
|
|
52
|
+
|
|
53
|
+
| Tier | Badge | Access Path | Architectural Role |
|
|
54
|
+
| :--- | :--- | :--- | :--- |
|
|
55
|
+
| **Tier 1** | `Public (core.*)` | `core.<ns>.<fn>` / `useCore().<ns>.<fn>` | Primary SDK surface for application developers (`createApp`, `define.*`, `db.*`, `files.*`, `ai.*`, `attachments.*`, `tasks.*`, `auth.*`, `chat.*`, `step.*`, `schemas.*`, `errors.*`, `types.*`). |
|
|
56
|
+
| **Tier 2** | `Internal Hook (core.internals.*)` | `core.internals.<group>.<fn>` | Non-enumerable getter on `Core.prototype` exposing low-level resilience (`singleflight`, `circuitBreaker`, `acquireLimit`), context runners, registries, workflow engine primitives, and auth token/revocation internals for advanced orchestration and test harnesses. |
|
|
57
|
+
| **Tier 3** | `Domain Barrel Export (<domain>/index.ts)` | `export class *Service` / `export * from './schema'` | Domain `<domain>/index.ts` barrels imported exclusively by `src/core/index.ts` and cross-domain callers (`'../<domain>'`), re-exporting `*Service`, public `schema.ts` symbols, and `core*Routes`. |
|
|
58
|
+
| **Tier 4** | `Private (#method / Flat Sibling)` | `#method` on `*Service` or flat sibling sub-engine | Encapsulated `#private` class methods and domain-internal flat sibling files (`tasks/{steps,watcher,worker}.ts`, `auth/{session,oidc,recovery}.ts`, `chat/{context,stream}.ts`, `router/guard.ts`, `<domain>/utils.ts`). |
|
|
59
|
+
|
|
60
|
+
### 1.3 Unified Capabilities (`defineRoute`: `'rest'` vs `'workflow'`)
|
|
61
|
+
|
|
62
|
+
Every route declared with `core.define.route()` / `core.define.routes()` is mounted under `/api/${routeKey}` regardless of `mode: 'rest' | 'workflow'`:
|
|
63
|
+
- **`mode: 'rest'` (default)**: Executes inline within the HTTP request using an in-memory `StepRunner` (`createStepRunner('rest')`) providing in-memory memoization, step retries/timeouts, and reverse-order Saga compensation on failure. When `idempotencyKey` or the `Idempotency-Key` HTTP header is supplied, concurrent identical calls coalesce via `singleflight` and completed JSON outcomes are persisted in `_idempotency/{keyHash}` (24h TTL).
|
|
64
|
+
- **`mode: 'workflow'`**: Executes durably over sharded Cloud Tasks (`0..19`) + Firestore `_tasks/{taskId}` with monotonic epoch fencing (`epoch`, `activeRunId`, `lastHeartbeatAt`), automatic `> 200 KiB` (or `> 150 KiB` cumulative) GCS Claim-Check offloading, `step.map`, `step.sleep`, `step.waitForSignal`, `step.continueAsNew`, 75% time-budget continuation yielding, and synchronous HTTP watching (`watchTask`) that gracefully detaches to `202 Accepted` when `Prefer: respond-async`, `?wait=false`, or `maxWaitMs` (`60s`) is reached.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. Project & Module Topology (`src/core/` Flat Domain Barrel Map)
|
|
69
|
+
|
|
70
|
+
Every domain in `src/core/<domain>/` is a flat directory (`0` nested subdirectories), with every `.ts` file strictly `<= 500 LOC`, every method `<= 100 LOC`, pure `schema.ts` files (zero runtime helper functions), and a `<domain>/index.ts` public domain barrel so all cross-domain imports resolve exclusively via `'../<domain>'` or `'./<domain>'`:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
src/core/
|
|
74
|
+
├── index.ts # [Entrypoint & Client] Core class composed from *Service classes, ALS state (coreStorage), core singleton, core.types namespace, useCore()
|
|
75
|
+
├── config.ts # [Config] CORE_CONFIG, defineConfig(), SECRET_NAMES, CoreConfig/AppBindings/RbacConfig types
|
|
76
|
+
├── ai/
|
|
77
|
+
│ ├── index.ts # [AI Barrel] Re-exports schema, AiService
|
|
78
|
+
│ ├── schema.ts # [AI] Pure types & schemas (JevQuestion, DecideResult, VisionOptions, VisionResult, AiGenerateOptions, AiGenerateResult)
|
|
79
|
+
│ ├── utils.ts # [AI] Pure prompt builders (buildVisionFallbackPrompt)
|
|
80
|
+
│ └── service.ts # [AI] AiService (decide, vision, generate, embed) + Mistral OCR, TypeSafe Jev & Vertex Anthropic clients
|
|
81
|
+
├── attachments/
|
|
82
|
+
│ ├── index.ts # [Attachments Barrel] Re-exports schema, AttachmentsService, coreAttachmentsRoutes
|
|
83
|
+
│ ├── schema.ts # [Attachments] Pure Zod schemas, types & AttachmentTypeSpec registry
|
|
84
|
+
│ ├── utils.ts # [Attachments] schemaFor(), parseFields(), extractAttachmentTasks(), DEFECT_QUESTIONS
|
|
85
|
+
│ ├── service.ts # [Attachments] AttachmentsService (scan, process, classify, scanShortCircuit)
|
|
86
|
+
│ └── routes.ts # [Attachments] coreAttachmentsRoutes ('attachments.scan', 'attachments.process')
|
|
87
|
+
├── auth/
|
|
88
|
+
│ ├── index.ts # [Auth Barrel] Re-exports schema, AuthService, coreAuthRoutes
|
|
89
|
+
│ ├── schema.ts # [Auth] Pure Zod schemas & static types (AuthUser, AuthTokenDoc, LinkedProvider)
|
|
90
|
+
│ ├── utils.ts # [Auth] resolveUserRole(), hashToken(), mapUserRecord(), parseJwtSegment()
|
|
91
|
+
│ ├── session.ts # [Auth] AuthSessionEngine (LRU tokenCache, prod/dev revocation watermarks, session cookies, custom tokens)
|
|
92
|
+
│ ├── oidc.ts # [Auth] AuthOidcEngine (strict RS256 JWK verification, fail-closed claim check, account linking)
|
|
93
|
+
│ ├── recovery.ts # [Auth] AuthRecoveryEngine (OTP/token issuance, email verification, password reset, account recovery)
|
|
94
|
+
│ ├── service.ts # [Auth] AuthService facade + bounded GDPR eraseUser (runChunked 25)
|
|
95
|
+
│ └── routes.ts # [Auth] coreAuthRoutes (18 built-in authentication & user management route handlers)
|
|
96
|
+
├── chat/
|
|
97
|
+
│ ├── index.ts # [Chat Barrel] Re-exports schema, ChatService, coreChatRoutes, mcpRoute
|
|
98
|
+
│ ├── schema.ts # [Chat] Pure Zod schemas & static types (AgentConfig, UserContext, ChatMetadataDoc, ChatMessageDoc)
|
|
99
|
+
│ ├── utils.ts # [Chat] SSRF guard (isSafePublicUrl), message/attachment parsers, dynamic context & UI stream builders
|
|
100
|
+
│ ├── context.ts # [Chat] ChatContextEngine (InfoBase Jev selector, vector recall, sliding window, userContext)
|
|
101
|
+
│ ├── stream.ts # [Chat] ChatStreamEngine (SSE turn execution, attachment staging, activeGenerations lock, cancel)
|
|
102
|
+
│ ├── service.ts # [Chat] ChatService facade (stream, ask, retrieveContext, getContext, saveChat, messages, cancel)
|
|
103
|
+
│ └── routes.ts # [Chat] coreChatRoutes + MCP JSON-RPC handler (mcpRoute) + OpenAI-compatible endpoints
|
|
104
|
+
├── db/
|
|
105
|
+
│ ├── index.ts # [DB Barrel] Re-exports schema, DbService
|
|
106
|
+
│ ├── schema.ts # [DB] Pure Zod schemas, coreCollections (9 collections), collectionRegistry, defineCollection(s)
|
|
107
|
+
│ ├── utils.ts # [DB] Pure RBAC scope resolvers, Firestore normalizers, cursor pagination & runChunked helpers
|
|
108
|
+
│ └── service.ts # [DB] DbService (prod/dev Firestore & Bucket singletons, getGcpAccessToken, RBAC CRUD, GDPR cascade)
|
|
109
|
+
├── emails/
|
|
110
|
+
│ ├── index.ts # [Emails Barrel] Re-exports schema, EmailsService, coreEmailRoutes
|
|
111
|
+
│ ├── schema.ts # [Emails] Pure Zod schemas, EmailTemplateDefinition, defineEmailTemplates, emailTemplateRegistry
|
|
112
|
+
│ ├── utils.ts # [Emails] Stateless Resend/SendGrid/Firebase OOB callers, Svix/Twilio HMAC-SHA256 webhook verifiers, template builders
|
|
113
|
+
│ ├── service.ts # [Emails] EmailsService (send, sendBatch, sendAuthEmail, getLog, listLogs, isSuppressed, webhook processor)
|
|
114
|
+
│ └── routes.ts # [Emails] coreEmailRoutes ('emails.send', 'emails.batch', 'emails.logs', 'emails.webhook')
|
|
115
|
+
├── files/
|
|
116
|
+
│ ├── index.ts # [Files Barrel] Re-exports schema, FilesService, coreFilesRoutes
|
|
117
|
+
│ ├── schema.ts # [Files] Content-addressed FileRef (<userId>_<sha256_24>), magic-byte refined FileBody, SignedUploadInput
|
|
118
|
+
│ ├── utils.ts # [Files] canAccessFile() O(1) prefix check & hasValidMagicBytes() signature validator
|
|
119
|
+
│ ├── service.ts # [Files] FilesService (SDK-level #assertAccess RBAC, upload with Saga rollback, V4 signed URLs, CRUD)
|
|
120
|
+
│ └── routes.ts # [Files] coreFilesRoutes ('files.list', 'files.upload', 'files.get', 'files.delete')
|
|
121
|
+
├── payments/
|
|
122
|
+
│ ├── index.ts # [Payments Barrel] Re-exports schema, PaymentsService, corePaymentRoutes
|
|
123
|
+
│ ├── schema.ts # [Payments] Pure Zod schemas, PlanDefinition, ProductDefinition, definePlans, defineProducts & registries
|
|
124
|
+
│ ├── utils.ts # [Payments] Stateless Stripe/Dodo REST helpers, encodeStripeForm, HMAC-SHA256 webhook verifiers & normalizers
|
|
125
|
+
│ ├── billing.ts # [Payments] PaymentsBillingEngine (ensureCustomer, applyProductGrants, createCheckout, createPaymentLink, createInvoice, charge, subscribe, changeSubscriptionTier, cancelSubscription)
|
|
126
|
+
│ ├── service.ts # [Payments] PaymentsService (billing facade, getSubscription, getUsage, checkQuota, recordUsage, meter ingestion & webhook processor)
|
|
127
|
+
│ └── routes.ts # [Payments] corePaymentRoutes (9 built-in payment, subscription, status & webhook routes)
|
|
128
|
+
├── router/
|
|
129
|
+
│ ├── index.ts # [Router Barrel] Re-exports schema, RouterService
|
|
130
|
+
│ ├── schema.ts # [Router] RouteConfig, RouteExecutionContext, ResourceGuardConfig, defineRoute(s), CORE_ROUTE_DEFAULTS (51 routes)
|
|
131
|
+
│ ├── guard.ts # [Router] RouterGuardEngine (rate limiting, authenticateRequest, enforceGuard, withHttpIdempotency)
|
|
132
|
+
│ └── service.ts # [Router] RouterService (route invoker, REST + workflow dispatch, Hono apps, createCloudFunctions)
|
|
133
|
+
├── tasks/
|
|
134
|
+
│ ├── index.ts # [Tasks Barrel] Re-exports schema, TasksService, task/health/introspection route builders
|
|
135
|
+
│ ├── schema.ts # [Tasks] Pure types & signal classes (StepRunner, TaskStatusRecord, StaleWorkerEpochError, TaskYieldSignal)
|
|
136
|
+
│ ├── utils.ts # [Tasks] Pure predicates, Cloud Tasks queue helpers, retry runner & Firestore epoch transactions
|
|
137
|
+
│ ├── steps.ts # [Tasks] TaskStepsEngine (createStepRunner, GCS Claim-Check offload/hydrate, fenced step persistence)
|
|
138
|
+
│ ├── watcher.ts # [Tasks] TaskWatcherEngine (sharded Cloud Tasks dispatcher, multiplexed onSnapshot + poll + hedge watcher)
|
|
139
|
+
│ ├── worker.ts # [Tasks] TaskWorkerEngine (dispatch, execute, monotonic epoch fencing, 20s heartbeat, yield & compensation)
|
|
140
|
+
│ ├── service.ts # [Tasks] TasksService facade (status, signal, cancel, step/workflow internals)
|
|
141
|
+
│ └── routes.ts # [Tasks] Health check, /api/routes JSON+Markdown introspection generator, task status/signal/cancel routes
|
|
142
|
+
└── utils/
|
|
143
|
+
├── index.ts # [Utils Barrel] Re-exports schema, ErrorsService, ResilienceService
|
|
144
|
+
├── schema.ts # [Utils] BreakerName, CircuitState, CircuitBreakerDoc, SingleflightScope, SingleflightOptions, errorBodySchema
|
|
145
|
+
└── service.ts # [Utils] ErrorsService & ResilienceService (singleflight, #private RateLimiter reading CORE_CONFIG.limits.buckets, circuitBreaker)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 3. Master `CoreClient`, HTTP Route & Runtime State Lookup Matrices
|
|
151
|
+
|
|
152
|
+
### 3.1 Master `CoreClient` Public Surface Map (`core.*` & `useCore()`)
|
|
153
|
+
|
|
154
|
+
| Public SDK Path | Source File | Backing Implementation | Signature Summary | Purpose |
|
|
155
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
156
|
+
| **`Core`** | `src/core/index.ts` | `class Core` | `new Core(state?: CoreRuntimeState)` | Context-aware SDK client class composed of domain `*Service` instances. |
|
|
157
|
+
| **`core`** | `src/core/index.ts` | `new Core()` | `const core: Core` | Default ambient singleton + merged `core.types.*` namespace. |
|
|
158
|
+
| **`useCore()`** | `src/core/index.ts` | `useCore` | `() => Core` | Returns `new Core(active)` if inside ALS store, else `core` singleton. |
|
|
159
|
+
| **`core.env`** | `src/core/index.ts` | `Core.prototype.env` | `get env(): AppBindings` | Active secret bindings & `isDev` flag. |
|
|
160
|
+
| **`core.userId`** | `src/core/index.ts` | `Core.prototype.userId` | `get userId(): EntryId` | Authenticated caller UID (or `'admin'` / `''`). |
|
|
161
|
+
| **`core.role`** | `src/core/index.ts` | `Core.prototype.role` | `get role(): DbRole` | Caller RBAC role (`'admin' \| 'user' \| 'anon'`). |
|
|
162
|
+
| **`core.caller`** | `src/core/index.ts` | `Core.prototype.caller` | `get caller(): { userId: EntryId; role: DbRole }` | Tuple object passed to task/chat ownership checks. |
|
|
163
|
+
| **`core.step`** | `src/core/index.ts` | `Core.prototype.step` | `get step(): StepRunner` | Active `StepRunner` from `#state.step` or `TasksService.useStep()`. |
|
|
164
|
+
| **`core.config`** | `src/core/config.ts` | `CORE_CONFIG` | `readonly config: CoreConfig` | Live mutable platform configuration singleton. |
|
|
165
|
+
| **`core.createApp`** | `src/core/router/service.ts` | `RouterService.createApp` | `(opts?: CoreAppInit) => CoreAppReturn` | Compiles collections, agents, context, routes, Hono apps, and Cloud Functions. |
|
|
166
|
+
| **`core.define.config`** | `src/core/config.ts` | `defineConfig` | `(overrides?: DeepPartial<CoreConfig>) => CoreConfig` | Deep-merges config overrides and recomputes GCP URIs/buckets if `gcpProject` changes. |
|
|
167
|
+
| **`core.define.route`** | `src/core/router/schema.ts` | `defineRoute` | `<C extends RouteConfig>(config: C) => C` | Declares a typed HTTP/Workflow/Agent/MCP route (defaults `mcp: false`). |
|
|
168
|
+
| **`core.define.routes`** | `src/core/router/schema.ts` | `defineRoutes` | `<T extends Record<string, RouteConfig>>(routes: T) => T` | Declares a map of domain routes. |
|
|
169
|
+
| **`core.define.routeOverrides`** | `src/core/router/schema.ts` | `defineRouteOverrides` | `<T extends CoreRouteOverrides>(overrides: T) => T` | Customizes `auth`, `rateLimit`, `guard`, `mcp`, `tool`, `timeout`, `retry` on built-in routes. |
|
|
170
|
+
| **`core.define.collection`** | `src/core/db/schema.ts` | `defineCollection` | `<S extends z.ZodObject<any>>(def: CollectionDefinition<S>) => CollectionDefinition<S>` | Merges `BaseEntryFields` (`id`, `deleted`, `createdAt`, `updatedAt`) and normalizes `userRelation`. |
|
|
171
|
+
| **`core.define.collections`** | `src/core/db/schema.ts` | `defineCollections` | `<T extends Record<string, CollectionDefinition<any>>>(defs: T) => T` | Declares a map of typed Firestore collections. |
|
|
172
|
+
| **`core.define.agent`** | `src/core/chat/schema.ts` | `defineAgent` | `<C extends AgentConfig>(config: C) => C` | Declares a single AI agent configuration. |
|
|
173
|
+
| **`core.define.agents`** | `src/core/chat/schema.ts` | `defineAgents` | `<T extends Record<string, AgentConfig>>(agents: T) => T` | Declares a map of AI agents. |
|
|
174
|
+
| **`core.define.context`** | `src/core/chat/schema.ts` | `defineContext` | `<const T extends readonly ContextItemSpec[]>(items: T) => T` | Declares the Agent InfoBase `.md` documentation catalog. |
|
|
175
|
+
| **`core.define.attachmentTypes`** | `src/core/attachments/schema.ts` | `defineAttachmentTypes` | `<T extends AttachmentTypesMap>(types: T) => T` | Declares document classification & extraction Zod schemas. |
|
|
176
|
+
| **`core.define.emailTemplates`** | `src/core/emails/schema.ts` | `defineEmailTemplates` | `<T extends EmailTemplatesMap>(templates: T) => T` | Declares typed transactional email templates with Zod `variables`, `onBeforeSend`, and provider specs. |
|
|
177
|
+
| **`core.define.plans`** | `src/core/payments/schema.ts` | `definePlans` | `<T extends PlansMap>(plans: T) => T` | Declares recurring/free subscription tiers with provider price/product IDs, AI token limits, and overage specs. |
|
|
178
|
+
| **`core.define.products`** | `src/core/payments/schema.ts` | `defineProducts` | `<T extends ProductsMap>(products: T) => T` | Declares one-time purchasable products & token/credit packs with automatic `grants`. |
|
|
179
|
+
| **`core.db.create`** | `src/core/db/service.ts` | `DbService.create` | `<N>(name: N, data: InsertData<N>, ctx?: DbCallContext) => Promise<SelectModel<N>>` | Validates schema, checks RBAC `'create'` permission, and writes new Firestore doc. |
|
|
180
|
+
| **`core.db.read`** | `src/core/db/service.ts` | `DbService.read` | `<N>(name: N, id: EntryId, includeDeleted?: boolean, ctx?: DbCallContext) => Promise<SelectModel<N> \| undefined>` | Reads doc by ID, normalizes Timestamps, and enforces RBAC `'read'` scope. |
|
|
181
|
+
| **`core.db.list`** | `src/core/db/service.ts` | `DbService.list` | `<N>(name: N, options?: DbListOptions<SelectModel<N>>, ctx?: DbCallContext) => Promise<PaginatedList<SelectModel<N>>>` | Queries/batches docs with RBAC auto-scoping, index-fallback sorting, and `nextCursor`. |
|
|
182
|
+
| **`core.db.listPage`** | `src/core/db/service.ts` | `DbService.listPage` | `<N>(name: N, options?: DbListOptions<SelectModel<N>>, ctx?: DbCallContext) => Promise<DbListPage<SelectModel<N>>>` | Wraps `list` returning explicit `{ items, nextCursor }`. |
|
|
183
|
+
| **`core.db.update`** | `src/core/db/service.ts` | `DbService.update` | `<N>(name: N, id: EntryId, patch: UpdateData<N>, ctx?: DbCallContext) => Promise<{ id: EntryId; updated: true }>` | Strips immutable/owner fields, validates partial schema, enforces RBAC `'write'`, updates doc. |
|
|
184
|
+
| **`core.db.delete`** | `src/core/db/service.ts` | `DbService.delete` | `<N>(name: N, id: EntryId, ctx?: DbCallContext) => Promise<{ id: EntryId; deleted: true }>` | Soft-deletes doc (`deleted: true`) after RBAC `'delete'` check. |
|
|
185
|
+
| **`core.db.hardDelete`** | `src/core/db/service.ts` | `DbService.hardDelete` | `<N>(name: N, id: EntryId, ctx?: DbCallContext) => Promise<{ id: EntryId; deleted: true; hard: true }>` | Permanently deletes doc, GCS file/chat subcollections, and runs `onHardDelete` hook. |
|
|
186
|
+
| **`core.db.canAccess`** | `src/core/db/service.ts` | `DbService.canAccess` | `<N>(name: N, doc: SelectModel<N>, userId?: EntryId, action?: DbAction, role?: DbRole) => Promise<boolean>` | Evaluates RBAC matrix (`any` vs `own` + `#isEntryOwner`) for `doc`. |
|
|
187
|
+
| **`core.db.firestore`** | `src/core/db/service.ts` | `DbService.firestore` | `(isDev?: boolean) => Firestore` | Returns raw `firebase-admin/firestore` instance for prod or dev DB. |
|
|
188
|
+
| **`core.files.upload`** | `src/core/files/service.ts` | `FilesService.upload` | `(input: FileBody, userId?: EntryId) => Promise<{ ref: FileRef }>` | Content-addressed upload (`<userId>_<sha256_24>`) to GCS + `file` collection with Saga rollback. |
|
|
189
|
+
| **`core.files.getSignedUploadUrl`** | `src/core/files/service.ts` | `FilesService.getSignedUploadUrl` | `(input: SignedUploadInput, userId?: EntryId, expiresInMs?: number) => Promise<SignedUploadUrlResult>` | Generates V4 GCS signed `PUT` URL with automatic SHA-256 deduplication check. |
|
|
190
|
+
| **`core.files.getSignedDownloadUrl`** | `src/core/files/service.ts` | `FilesService.getSignedDownloadUrl` | `(ref: FileRef, expiresInMs?: number) => Promise<SignedDownloadUrlResult>` | Verifies `file` metadata exists and generates V4 GCS signed `GET` URL. |
|
|
191
|
+
| **`core.files.get`** | `src/core/files/service.ts` | `FilesService.get` | `(ref: FileRef) => Promise<FileBody>` | Downloads file bytes + metadata in parallel and validates `FileBody`. |
|
|
192
|
+
| **`core.files.list`** | `src/core/files/service.ts` | `FilesService.list` | `(options?: DbListOptions<SelectModel<'file'>>) => Promise<PaginatedList<SelectModel<'file'>>>` | Lists `file` metadata records (auto-scoped to `core.userId` for non-admins). |
|
|
193
|
+
| **`core.files.download`** | `src/core/files/service.ts` | `FilesService.download` | `(ref: FileRef) => Promise<Buffer>` | Downloads raw `Buffer` from Firebase Storage bucket. |
|
|
194
|
+
| **`core.files.delete`** | `src/core/files/service.ts` | `FilesService.delete` | `(ref: FileRef) => Promise<{ ref: FileRef; deleted: true }>` | Soft-deletes `file` metadata record in Firestore. |
|
|
195
|
+
| **`core.files.hardDelete`** | `src/core/files/service.ts` | `FilesService.hardDelete` | `(ref: FileRef) => Promise<{ ref: FileRef }>` | Permanently deletes file from GCS and Firestore inside a reversible `step` with rollback. |
|
|
196
|
+
| **`core.files.canAccess`** | `src/core/files/service.ts` | `FilesService.canAccess` | `(ref: string, userId?: EntryId) => boolean` | $O(1)$ prefix check verifying `ref.slice(0, -25) === userId` (or admin). |
|
|
197
|
+
| **`core.files.bucket`** | `src/core/files/service.ts` | `FilesService.bucket` | `(isDev?: boolean) => Bucket` | Returns raw Firebase Storage `Bucket` instance for prod or dev. |
|
|
198
|
+
| **`core.ai.vision`** | `src/core/ai/service.ts` | `AiService.vision` | `(documentUrl: string, opts?: VisionOptions) => Promise<VisionResult>` | Mistral OCR + JSON Schema annotation + automatic Vertex Vision fallback under circuit breaker. |
|
|
199
|
+
| **`core.ai.decide`** | `src/core/ai/service.ts` | `AiService.decide` | `(state: unknown, questions: Record<string, JevQuestion>) => Promise<DecideResult>` | TypeSafe Jev `systemOne` probabilistic reasoning (`noul`, `choice`, `score`) under circuit breaker. |
|
|
200
|
+
| **`core.ai.generate`** | `src/core/ai/service.ts` | `AiService.generate` | `<O = string>(promptOrMessages: string \| ModelMessage[], opts?: AiGenerateOptions<O>) => Promise<AiGenerateResult<O>>` | Vertex Anthropic text or structured Zod output generation under circuit breaker. |
|
|
201
|
+
| **`core.ai.embed`** | `src/core/ai/service.ts` | `AiService.embed` | `(text: string) => Promise<number[] \| null>` | Generates 768-dim embedding vector via Vertex `text-embedding-005` (null if `< 15` chars). |
|
|
202
|
+
| **`core.attachments.scan`** | `src/core/attachments/service.ts` | `AttachmentsService.scan` | `(raw: ScanAttachmentInput, opts?: { types?: AttachmentTypesMap; step?: StepRunner }) => Promise<FileScan>` | Singleflight + cross-user SHA-256 cache + multi-step OCR → classify → extract → persist pipeline. |
|
|
203
|
+
| **`core.attachments.process`** | `src/core/attachments/service.ts` | `AttachmentsService.process` | `(raw: ProcessAttachmentsInput, opts?: { types?: AttachmentTypesMap; step?: StepRunner }) => Promise<ProcessAttachmentsOutput>` | Batch preloads `file` docs and fans out `#scanAttachmentTask` via `step.map` with `shortCircuit`. |
|
|
204
|
+
| **`core.attachments.classify`** | `src/core/attachments/service.ts` | `AttachmentsService.classify` | `(markdown: string, types?: AttachmentTypesMap) => Promise<{ attachmentType: string; confidence: number }>` | Classifies document markdown into a registered `AttachmentTypesMap` key via Jev `choice`. |
|
|
205
|
+
| **`core.tasks.status`** | `src/core/tasks/service.ts` | `TasksService.status` | `<O = unknown>(taskId: string) => Promise<TaskStatusRecord<O>>` | Reads `_tasks/{taskId}`, enforces task ownership, and hydrates GCS Claim-Check response. |
|
|
206
|
+
| **`core.tasks.signal`** | `src/core/tasks/service.ts` | `TasksService.signal` | `(taskId: string, signalName: string, payload?: unknown) => Promise<{ taskId: string; signaled: true }>` | Stores signal in `_tasks/{taskId}.signals`, increments `epoch` if waiting, and wakes workflow. |
|
|
207
|
+
| **`core.tasks.cancel`** | `src/core/tasks/service.ts` | `TasksService.cancel` | `(taskId: string) => Promise<{ taskId: string; status: 'cancelled' }>` | Bumps `epoch` to `'compensating'`, aborts local worker or runs replay-only Saga compensation, marks `'cancelled'`. |
|
|
208
|
+
| **`core.auth.getUser`** | `src/core/auth/service.ts` | `AuthService.getUser` | `(uid: EntryId) => Promise<AuthUser \| undefined>` | Fetches Firebase `UserRecord` + `user_auth_meta` doc in parallel and merges into `AuthUser`. |
|
|
209
|
+
| **`core.auth.listUsers`** | `src/core/auth/service.ts` | `AuthService.listUsers` | `(includeDeleted?: boolean) => Promise<AuthUser[]>` | Lists up to 1,000 Firebase Auth users mapped to `AuthUser`. |
|
|
210
|
+
| **`core.auth.setClaims`** | `src/core/auth/service.ts` | `AuthService.setClaims` | `(uid: EntryId, claims: Record<string, unknown> \| null) => Promise<void>` | Sets Firebase custom claims, evicts local token cache, and records global revocation watermark. |
|
|
211
|
+
| **`core.auth.blockUser`** | `src/core/auth/service.ts` | `AuthService.blockUser` | `(userId?: EntryId, blocked?: boolean) => Promise<{ userId: EntryId; blocked: boolean; deleted: boolean }>` | Disables/enables Firebase Auth user, revokes refresh tokens, and updates `_system/auth_revocations`. |
|
|
212
|
+
| **`core.auth.eraseUser`** | `src/core/auth/service.ts` | `AuthService.eraseUser` | `(raw: EraseUserInput, s?: StepRunner) => Promise<EraseUserOutput>` | Durable GDPR erasure: optional GCS Firestore export, cascade-deletes files/chats/collections/tasks/auth. |
|
|
213
|
+
| **`core.auth.signInSocial`** | `src/core/auth/service.ts` | `AuthService.signInSocial` | `(input: SocialSignInInput) => Promise<{ user: AuthUser; sessionCookie: string }>` | Verifies Google/Apple OIDC ID token (with IdentityToolkit fallback), links account, and mints session. |
|
|
214
|
+
| **`core.auth.sendVerificationEmail`** | `src/core/auth/service.ts` | `AuthService.sendVerificationEmail` | `(userId?: EntryId) => Promise<{ sent: true; expiresInMinutes: number }>` | Issues 6-digit OTP + 32-byte URL token in `auth_tokens` and dispatches verification email. |
|
|
215
|
+
| **`core.auth.verifyEmail`** | `src/core/auth/service.ts` | `AuthService.verifyEmail` | `(input: VerifyEmailInput) => Promise<{ verified: true; userId: string }>` | Consumes `verify_email` token in transaction, sets `emailVerified: true`, and evicts token cache. |
|
|
216
|
+
| **`core.auth.forgotPassword`** | `src/core/auth/service.ts` | `AuthService.forgotPassword` | `(input: ForgotPasswordInput) => Promise<{ sent: true }>` | Enumeration-safe password reset OTP/token issuer (`reset_password`). |
|
|
217
|
+
| **`core.auth.resetPassword`** | `src/core/auth/service.ts` | `AuthService.resetPassword` | `(input: ResetPasswordInput) => Promise<{ reset: true }>` | Consumes `reset_password` token, updates password, and revokes all user sessions/tokens. |
|
|
218
|
+
| **`core.auth.setRecoveryEmail`** | `src/core/auth/service.ts` | `AuthService.setRecoveryEmail` | `(input: SetRecoveryEmailInput, userId?: EntryId) => Promise<{ recoveryEmail: string; recoveryEmailVerified: false; sent: true }>` | Stores unverified `recoveryEmail` in `user_auth_meta` and sends `verify_recovery_email` OTP. |
|
|
219
|
+
| **`core.auth.verifyRecoveryEmail`** | `src/core/auth/service.ts` | `AuthService.verifyRecoveryEmail` | `(input: VerifyRecoveryEmailInput, userId?: EntryId) => Promise<{ recoveryEmail: string; recoveryEmailVerified: true; verified: true }>` | Consumes `verify_recovery_email` token and marks `recoveryEmailVerified: true`. |
|
|
220
|
+
| **`core.auth.initiateAccountRecovery`** | `src/core/auth/service.ts` | `AuthService.initiateAccountRecovery` | `(input: InitiateAccountRecoveryInput) => Promise<{ initiated: true }>` | Enumeration-safe recovery OTP dispatch to verified secondary recovery email (`account_recovery`). |
|
|
221
|
+
| **`core.auth.completeAccountRecovery`** | `src/core/auth/service.ts` | `AuthService.completeAccountRecovery` | `(input: CompleteAccountRecoveryInput) => Promise<{ user: AuthUser; sessionCookie: string }>` | Consumes `account_recovery` token, updates primary email (+ optional password), revokes old sessions, mints new session. |
|
|
222
|
+
| **`core.chat.ask`** | `src/core/chat/service.ts` | `ChatService.ask` | `(agentOrId: string \| AgentConfig, prompt: string \| ModelMessage[], options?: AskAgentOptions) => Promise<{ text: string; messages: ModelMessage[] }>` | Runs hybrid context retrieval + multi-step `stream` turn and resolves final `{ text, messages }`. |
|
|
223
|
+
| **`core.chat.stream`** | `src/core/chat/service.ts` | `ChatService.stream` | `(agent: AgentConfig, messages: ModelMessage[], userId?: EntryId, opts?: RunAgentOptions) => Promise<StreamTextResult<any, any>>` | Executes `streamText` with prompt caching, dynamic context block, `toolPresets` activeTools, and `maxSteps`. |
|
|
224
|
+
| **`core.chat.messages`** | `src/core/chat/service.ts` | `ChatService.messages` | `(chatId: EntryId, opts?: ListChatMessagesOptions) => Promise<ChatMessagePublic[]>` | Enforces chat ownership and lists paginated messages from `chat/{chatId}/messages` in chronological order. |
|
|
225
|
+
| **`core.chat.cancel`** | `src/core/chat/service.ts` | `ChatService.cancel` | `(chatId: EntryId) => Promise<{ chatId: string; cancelled: true }>` | Clears `activeGenerationId` in Firestore (triggering remote `onSnapshot` abort) and aborts local controller. |
|
|
226
|
+
| **`core.chat.retrieveContext`** | `src/core/chat/service.ts` | `ChatService.retrieveContext` | `(opts: Omit<HybridRetrievalOptions, 'userId'> & { userId?: EntryId }) => Promise<HybridRetrievalResult>` | Runs sliding window, in-chat + cross-chat vector recall, `resolveUserContext`, and Jev InfoBase selection in parallel. |
|
|
227
|
+
| **`core.chat.getContext`** | `src/core/chat/service.ts` | `ChatService.getContext` | `(prompt: string, catalog?: readonly ContextItemSpec[], options?: GetContextOptions) => Promise<ContextEntry[]>` | Filters InfoBase catalog by `audiences`, scores relevance via Jev `noul`, and loads matched `.md` files. |
|
|
228
|
+
| **`core.emails.send`** | `src/core/emails/service.ts` | `EmailsService.send` | `<T>(input: SendEmailInput<T>, opts?: EmailCallOptions) => Promise<SendEmailResult>` | Sends a transactional email via SendGrid, Resend, or Firebase/dev with idempotency, suppression check, and `email_log` audit entry. |
|
|
229
|
+
| **`core.emails.sendBatch`** | `src/core/emails/service.ts` | `EmailsService.sendBatch` | `(inputs: SendEmailInput[], opts?: EmailCallOptions) => Promise<SendBatchEmailResult>` | Sends up to 100 transactional emails in a single batch (native Resend `/emails/batch` or parallel dispatch). |
|
|
230
|
+
| **`core.emails.sendAuthEmail`** | `src/core/emails/service.ts` | `EmailsService.sendAuthEmail` | `(purpose: AuthEmailPurpose, to: string, vars?: Partial<AuthEmailVariables>, opts?: EmailCallOptions) => Promise<SendEmailResult>` | Dispatches a transactional auth email (`verify_email`, `reset_password`, `verify_recovery_email`, `account_recovery`, `welcome`, `security_alert`). |
|
|
231
|
+
| **`core.emails.getLog`** | `src/core/emails/service.ts` | `EmailsService.getLog` | `(emailId: EntryId) => Promise<SelectModel<'email_log'> \| undefined>` | Reads an `email_log` delivery audit document by ID. |
|
|
232
|
+
| **`core.emails.listLogs`** | `src/core/emails/service.ts` | `EmailsService.listLogs` | `(options?: DbListOptions<SelectModel<'email_log'>>) => Promise<PaginatedList<SelectModel<'email_log'>>>` | Lists `email_log` records with filtering and pagination. |
|
|
233
|
+
| **`core.emails.isSuppressed`** | `src/core/emails/service.ts` | `EmailsService.isSuppressed` | `(email: string) => Promise<boolean>` | Checks whether a recipient address is on the `email_suppression` bounce/complaint list. |
|
|
234
|
+
| **`core.payments.createCheckout`** | `src/core/payments/service.ts` | `PaymentsService.createCheckout` | `(input: CreateCheckoutInput, opts?: PaymentCallOptions) => Promise<CheckoutSessionResult>` | Creates a hosted Stripe/Dodo checkout session for a predefined plan, product, or custom ad-hoc amount. |
|
|
235
|
+
| **`core.payments.createPaymentLink`** | `src/core/payments/service.ts` | `PaymentsService.createPaymentLink` | `(input: CreatePaymentLinkInput, opts?: PaymentCallOptions) => Promise<PaymentLinkResult>` | Generates a shareable Stripe/Dodo payment link for a predefined plan, product, or custom ad-hoc amount. |
|
|
236
|
+
| **`core.payments.createInvoice`** | `src/core/payments/service.ts` | `PaymentsService.createInvoice` | `(input: CreateInvoiceInput, opts?: PaymentCallOptions) => Promise<InvoiceResult>` | Creates and finalizes a Stripe/Dodo invoice with hosted payment URL and PDF link. |
|
|
237
|
+
| **`core.payments.charge`** | `src/core/payments/service.ts` | `PaymentsService.charge` | `(input: DirectChargeInput, opts?: PaymentCallOptions) => Promise<ChargeResult>` | Executes a direct one-time charge and automatically applies product `grants` on `status === 'succeeded'`. |
|
|
238
|
+
| **`core.payments.subscribe`** | `src/core/payments/service.ts` | `PaymentsService.subscribe` | `(input: SubscribeInput, opts?: PaymentCallOptions) => Promise<SubscribeResult>` | Subscribes the caller to a defined free or recurring plan tier (`month` / `year` / `free`). |
|
|
239
|
+
| **`core.payments.changeSubscriptionTier`** | `src/core/payments/service.ts` | `PaymentsService.changeSubscriptionTier` | `(input: ChangeTierInput, opts?: PaymentCallOptions) => Promise<SubscriptionRecord>` | Upgrades or downgrades the caller's active subscription tier with proration (`prorated_immediately`, `next_cycle`, `none`). |
|
|
240
|
+
| **`core.payments.cancelSubscription`** | `src/core/payments/service.ts` | `PaymentsService.cancelSubscription` | `(input?: CancelSubscriptionInput, opts?: PaymentCallOptions) => Promise<SubscriptionRecord>` | Cancels the caller's active subscription immediately or at the end of the current billing period. |
|
|
241
|
+
| **`core.payments.getSubscription`** | `src/core/payments/service.ts` | `PaymentsService.getSubscription` | `(userId?: EntryId) => Promise<CustomerBillingSummary>` | Returns the caller's current plan tier, subscription status, and bonus token/credit balances. |
|
|
242
|
+
| **`core.payments.getUsage`** | `src/core/payments/service.ts` | `PaymentsService.getUsage` | `(userId?: EntryId) => Promise<UsageStatusResult>` | Returns the caller's current billing period AI token and custom metric usage vs plan limits. |
|
|
243
|
+
| **`core.payments.checkQuota`** | `src/core/payments/service.ts` | `PaymentsService.checkQuota` | `(metric?: string, requiredAmount?: number, userId?: EntryId, throwOnExceeded?: boolean) => Promise<QuotaCheckResult>` | Evaluates plan quota + bonus balance + overage allowance (throws `402 QUOTA_EXCEEDED` when `throwOnExceeded = true`). |
|
|
244
|
+
| **`core.payments.recordUsage`** | `src/core/payments/service.ts` | `PaymentsService.recordUsage` | `(input: RecordUsageInput, opts?: PaymentCallOptions) => Promise<UsageStatusResult>` | Atomically increments `payment_usage`, deducts bonus balances, and ingests overage meter events to Stripe/Dodo. |
|
|
245
|
+
| **`core.schemas.*`** | Multiple `schema.ts` | 41 Zod Schemas | `{ entryId, fileRef, fileScan, fileScanError, fileBody, fileType, signedUploadInput, processedAttachment, scanAttachmentInput, scanAttachmentOutput, processAttachmentsInput, processAttachmentsOutput, streamChatBody, openaiCompletionsBody, errorBodySchema, emailLogStatusSchema, emailAttachmentInputSchema, sendEmailInputSchema, sendBatchEmailInputSchema, emailLogsQuerySchema, emailWebhookParamsSchema, sendEmailResultSchema, sendBatchEmailResultSchema, createCheckoutInput, createPaymentLinkInput, createInvoiceInput, directChargeInput, subscribeInput, changeTierInput, cancelSubscriptionInput, recordUsageInput, paymentWebhookParamsSchema, checkoutSessionResultSchema, paymentLinkResultSchema, invoiceResultSchema, chargeResultSchema, subscribeResultSchema, subscriptionRecordSchema, customerBillingSummarySchema, usageStatusResultSchema, usageSummarySchema, quotaCheckResultSchema }` | Reusable Zod schemas for route/collection definitions. |
|
|
246
|
+
| **`core.errors.*`** | `src/core/utils/service.ts` | `ErrorsService` | `{ httpError, nonRetryableError, NonRetryableError, isNonRetryableError, CircuitBreakerOpenError, StaleWorkerEpochError, isStaleWorkerEpochError, parseDuration }` | Structured HTTP errors, non-retryable workflow signals, epoch fence errors, and duration parser. |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### 3.2 Master `core.internals.*` Engine Hooks Map
|
|
251
|
+
|
|
252
|
+
Accessed via the non-enumerable `core.internals` prototype getter (`src/core/index.ts`):
|
|
253
|
+
|
|
254
|
+
| Group | Property Path | Source File | Backing Implementation | Purpose |
|
|
255
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
256
|
+
| **`resilience`** | `core.internals.resilience.singleflight` | `src/core/utils/service.ts` | `ResilienceService.singleflight` | Coalesces concurrent in-flight promises by `tenant:<userId>:<key>` or `global:<key>`. |
|
|
257
|
+
| **`resilience`** | `core.internals.resilience.circuitBreaker` | `src/core/utils/service.ts` | `ResilienceService.circuitBreaker` | Wraps upstream calls in a 3-state (`CLOSED`/`OPEN`/`HALF_OPEN`) Firestore-synced circuit breaker. |
|
|
258
|
+
| **`resilience`** | `core.internals.resilience.getCircuitBreakerState` | `src/core/utils/service.ts` | `ResilienceService.getCircuitBreakerState` | Reads `_system/breaker_${key}` state (with 3s in-memory cache + global singleflight). |
|
|
259
|
+
| **`resilience`** | `core.internals.resilience.resetCircuitBreaker` | `src/core/utils/service.ts` | `ResilienceService.resetCircuitBreaker` | Resets breaker state to `CLOSED` with `failureCount: 0` in memory and Firestore. |
|
|
260
|
+
| **`resilience`** | `core.internals.resilience.acquireLimit` | `src/core/utils/service.ts` | `ResilienceService.acquireLimit` | Sliding-window rate limiter for `mistral`, `jev`, `ai`, `workflows`, `firestore`, `storage` buckets. |
|
|
261
|
+
| **`resilience`** | `core.internals.resilience.acquireHttpLimit` | `src/core/utils/service.ts` | `ResilienceService.acquireHttpLimit` | Fixed-window in-memory counter rate limiter for HTTP user/IP keys (throws `429` with `Retry-After`). |
|
|
262
|
+
| **`context`** | `core.internals.context.run` | `src/core/index.ts` | `runWithCore` | Runs an async callback inside `coreStorage` (`AsyncLocalStorage<CoreRuntimeState>`). |
|
|
263
|
+
| **`context`** | `core.internals.context.getState` | `src/core/index.ts` | `getActiveCoreState` | Returns the current `CoreRuntimeState \| undefined` from `coreStorage`. |
|
|
264
|
+
| **`context`** | `core.internals.context.routeDefaults` | `src/core/router/schema.ts` | `CORE_ROUTE_DEFAULTS` | Map of all 42 built-in `CoreRouteId` base route configurations. |
|
|
265
|
+
| **`context`** | `core.internals.context.getMcpTools` | `src/core/chat/service.ts` | `ChatService.getMcpTools` | Returns the filtered `Map<string, WorkflowToolSpec>` of tools with `mcp === true`. |
|
|
266
|
+
| **`context`** | `core.internals.context.startChatStreamTurn` | `src/core/chat/stream.ts` | `ChatStreamEngine.startChatStreamTurn` | Shared turn starter that stages attachments, retrieves hybrid RAG context, and starts `core.chat.stream`. |
|
|
267
|
+
| **`registry`** | `core.internals.registry.registerCollections` | `src/core/db/schema.ts` | `registerCollections` | Registers custom collections into `collectionRegistry` (preserving the 6 core collections on reset). |
|
|
268
|
+
| **`registry`** | `core.internals.registry.unregisterCollections` | `src/core/db/schema.ts` | `unregisterCollections` | Removes custom collections from `collectionRegistry` (refuses to delete core collections). |
|
|
269
|
+
| **`registry`** | `core.internals.registry.cascadeEraseUserCollections` | `src/core/db/service.ts` | `DbService.cascadeEraseUserCollections` | Step-checkpointed GDPR cascade erasure across `file`, `chat`, and all user-related custom collections. |
|
|
270
|
+
| **`registry`** | `core.internals.registry.getAttachmentTypes` | `src/core/attachments/schema.ts` | `getRegisteredAttachmentTypes` | Returns the live `registeredAttachmentTypes` map. |
|
|
271
|
+
| **`registry`** | `core.internals.registry.registerEmailTemplates` | `src/core/emails/schema.ts` | `registerEmailTemplates` | Registers transactional `EmailTemplatesMap` definitions into `emailTemplateRegistry`. |
|
|
272
|
+
| **`registry`** | `core.internals.registry.clearEmailTemplates` | `src/core/emails/schema.ts` | `clearEmailTemplates` | Clears `emailTemplateRegistry`. |
|
|
273
|
+
| **`registry`** | `core.internals.registry.getEmailTemplate` | `src/core/emails/schema.ts` | `getEmailTemplate` | Looks up a registered `EmailTemplateDefinition` by template ID. |
|
|
274
|
+
| **`registry`** | `core.internals.registry.getRegisteredEmailTemplates` | `src/core/emails/schema.ts` | `getRegisteredEmailTemplates` | Returns all registered `EmailTemplatesMap` definitions. |
|
|
275
|
+
| **`registry`** | `core.internals.registry.registerPlans` | `src/core/payments/schema.ts` | `registerPlans` | Registers `PlansMap` definitions into `registeredPlans` and enables `CORE_CONFIG.payments.enabled`. |
|
|
276
|
+
| **`registry`** | `core.internals.registry.registerProducts` | `src/core/payments/schema.ts` | `registerProducts` | Registers `ProductsMap` definitions into `registeredProducts` and enables `CORE_CONFIG.payments.enabled`. |
|
|
277
|
+
| **`registry`** | `core.internals.registry.clearPaymentDefinitions` | `src/core/payments/schema.ts` | `clearPaymentDefinitions` | Clears `registeredPlans` and `registeredProducts` and resets `CORE_CONFIG.payments.enabled = false`. |
|
|
278
|
+
| **`registry`** | `core.internals.registry.getPlanDefinition` | `src/core/payments/schema.ts` | `getPlanDefinition` | Looks up a registered `PlanDefinition` by plan ID. |
|
|
279
|
+
| **`registry`** | `core.internals.registry.getProductDefinition` | `src/core/payments/schema.ts` | `getProductDefinition` | Looks up a registered `ProductDefinition` by product ID. |
|
|
280
|
+
| **`registry`** | `core.internals.registry.getRegisteredPlans` | `src/core/payments/schema.ts` | `getRegisteredPlans` | Returns all registered `PlanDefinition` entries. |
|
|
281
|
+
| **`registry`** | `core.internals.registry.getRegisteredProducts` | `src/core/payments/schema.ts` | `getRegisteredProducts` | Returns all registered `ProductDefinition` entries. |
|
|
282
|
+
| **`workflows`** | `core.internals.workflows.createStepRunner` | `src/core/tasks/steps.ts` | `TaskStepsEngine.createStepRunner` | Factory for durable (`'workflow'`) or in-memory (`'rest'`) `StepRunner` instances. |
|
|
283
|
+
| **`workflows`** | `core.internals.workflows.allWithRollback` | `src/core/tasks/steps.ts` | `TaskStepsEngine.allWithRollback` | Runs parallel `RollbackLeg<T>[]` and compensates fulfilled legs if any leg rejects. |
|
|
284
|
+
| **`workflows`** | `core.internals.workflows.getContext` | `src/core/tasks/worker.ts` | `TaskWorkerEngine.getContext` | Returns active `WorkflowContext` from `workflowStorage` (or an `'inline'` default). |
|
|
285
|
+
| **`workflows`** | `core.internals.workflows.useStep` | `src/core/tasks/service.ts` | `TasksService.#useStep` | Returns active `StepRunner` from `workflowStorage` (or a fresh `'workflow'` runner). |
|
|
286
|
+
| **`workflows`** | `core.internals.workflows.dispatch` | `src/core/tasks/worker.ts` | `TaskWorkerEngine.dispatch` | Dispatches a workflow task (inline or `_tasks` + Cloud Tasks) with deduplication and `watchTask`. |
|
|
287
|
+
| **`workflows`** | `core.internals.workflows.execute` | `src/core/tasks/worker.ts` | `TaskWorkerEngine.execute` | Worker entrypoint: claims monotonic `epoch`, runs 20s heartbeat, handles yields & Saga compensation. |
|
|
288
|
+
| **`workflows`** | `core.internals.workflows.registerRuntime` | `src/core/tasks/worker.ts` | `TaskWorkerEngine.registerRuntime` | Binds `workflowRegistry` for worker and child-workflow execution. |
|
|
289
|
+
| **`workflows`** | `core.internals.workflows.getRuntime` | `src/core/tasks/worker.ts` | `TaskWorkerEngine.getRuntime` | Returns `{ registry }`. |
|
|
290
|
+
| **`workflows`** | `core.internals.workflows.watchTask` | `src/core/tasks/watcher.ts` | `TaskWatcherEngine.watchTask` | Watches `_tasks/{taskId}` via `onSnapshot` + 5s polling + 65s hedging + `maxWaitMs` detach. |
|
|
291
|
+
| **`workflows`** | `core.internals.workflows.getActiveWatcherCount` | `src/core/tasks/watcher.ts` | `TaskWatcherEngine.getActiveWatcherCount` | Returns current count of active `watchTask` / `watchers` listeners (for leak verification). |
|
|
292
|
+
| **`auth`** | `core.internals.auth.verifyToken` | `src/core/auth/session.ts` | `AuthSessionEngine.verifyToken` | Verifies session cookie or Bearer ID token with LRU caching, revocation sync, and singleflight. |
|
|
293
|
+
| **`auth`** | `core.internals.auth.hashToken` | `src/core/auth/utils.ts` | `hashToken` | Computes SHA-256 hex digest of an OTP code or magic token. |
|
|
294
|
+
| **`auth`** | `core.internals.auth.resolveUserRole` | `src/core/auth/utils.ts` | `resolveUserRole` | Resolves `'admin' \| 'user' \| 'anon'` from `userId` and optional `explicitRole`. |
|
|
295
|
+
| **`auth`** | `core.internals.auth.tokenCache` | `src/core/auth/session.ts` | `tokenCache` | In-memory `Map<string, CachedTokenEntry>` (max 2,000 entries, 5m TTL). |
|
|
296
|
+
| **`auth`** | `core.internals.auth.setTokenCache` | `src/core/auth/session.ts` | `AuthSessionEngine.setTokenCache` | Inserts/updates a verified token in `tokenCache` and starts the 2s background revocation sync timer. |
|
|
297
|
+
| **`auth`** | `core.internals.auth.recordRevocation` | `src/core/auth/session.ts` | `AuthSessionEngine.recordRevocation` | Evicts user tokens locally and writes revocation timestamp to `_system/auth_revocations`. |
|
|
298
|
+
| **`auth`** | `core.internals.auth.syncRevocations` | `src/core/auth/session.ts` | `AuthSessionEngine.syncRevocations` | Pulls `_system/auth_revocations` (coalesced via global `singleflight`) and evicts revoked tokens. |
|
|
299
|
+
| **`auth`** | `core.internals.auth.getLastDevEmail` | `src/core/auth/recovery.ts` | `AuthRecoveryEngine.getLastDevEmail` | Retrieves the latest `TransactionalEmailPayload` sent to `(to, purpose)` from `devEmailOutbox`. |
|
|
300
|
+
| **`auth`** | `core.internals.auth.registerTestOidcPublicKey` | `src/core/auth/oidc.ts` | `AuthOidcEngine.registerTestOidcPublicKey` | Registers an RSA JWK public key for offline OIDC ID token verification in dev/test mode. |
|
|
301
|
+
| **`auth`** | `core.internals.auth.sessionCookie` | `src/core/config.ts` | `CORE_CONFIG.auth.sessionCookie` | Cookie name constant (`'session'`). |
|
|
302
|
+
| **`emails`** | `core.internals.emails.dispatchAuthEmail` | `src/core/emails/service.ts` | `EmailsService.#dispatchAuthEmail` | Dispatches an auth flow email with `onAuthEmail`/`onSend` hooks, template resolution, and dev outbox capture. |
|
|
303
|
+
| **`emails`** | `core.internals.emails.verifyAndProcessWebhook` | `src/core/emails/service.ts` | `EmailsService.#verifyAndProcessWebhook` | Verifies Resend (Svix) or SendGrid HMAC-SHA256 webhooks, updates `email_log`, and populates `email_suppression`. |
|
|
304
|
+
| **`emails`** | `core.internals.emails.getLastDevEmail` | `src/core/emails/service.ts` | `EmailsService.#getLastDevEmail` | Retrieves the latest `TransactionalEmailPayload` captured in `#devEmailOutbox`. |
|
|
305
|
+
| **`payments`** | `core.internals.payments.enforceAiQuota` | `src/core/payments/service.ts` | `PaymentsService.#enforceAiQuota` | Pre-flight AI token quota check invoked automatically by `AiService` and `ChatService.stream` (throws `402 QUOTA_EXCEEDED` on hard cap). |
|
|
306
|
+
| **`payments`** | `core.internals.payments.trackAiUsage` | `src/core/payments/service.ts` | `PaymentsService.#trackAiUsage` | Post-flight AI token consumption tracker invoked automatically by `AiService` and `ChatService.stream`. |
|
|
307
|
+
| **`payments`** | `core.internals.payments.verifyAndProcessWebhook` | `src/core/payments/service.ts` | `PaymentsService.#verifyAndProcessWebhook` | Verifies Stripe or Dodo HMAC-SHA256 webhooks, persists `payment_transaction`, reconciles `payment_customer`, and applies product `grants`. |
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
### 3.3 Master Built-In HTTP Routes (`CORE_ROUTE_DEFAULTS` — 51 Endpoints)
|
|
312
|
+
|
|
313
|
+
All built-in routes are defined in [`src/core/router/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/router/schema.ts) and mounted under `/api` in [`src/core/router/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/router/service.ts) (plus `ALL /api/mcp` for the MCP server):
|
|
314
|
+
|
|
315
|
+
| # | `CoreRouteId` | HTTP Method & Path | Mode | Default `auth` | Default `guard` | `mcp` | `tool` | Backing Handler & File |
|
|
316
|
+
| :-: | :--- | :--- | :---: | :---: | :--- | :---: | :---: | :--- |
|
|
317
|
+
| 1 | `'health'` | `GET /api/health` | `'rest'` | `'public'` | — | `false` | `true` | `buildHealthRoute()` (`tasks/routes.ts`) |
|
|
318
|
+
| 2 | `'auth.custom-token'` | `POST /api/auth/custom-token` | `'rest'` | `'admin'` | — | `false` | `true` | `core.auth.mintCustomToken()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
319
|
+
| 3 | `'auth.session.create'` | `POST /api/auth/session` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.createSession()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
320
|
+
| 4 | `'auth.session.get'` | `GET /api/auth/session` | `'rest'` | `'strict'` | — | `false` | `true` | Inline `({ userId }) => ({ userId })` (`auth/routes.ts`) |
|
|
321
|
+
| 5 | `'auth.sign-up.email'` | `POST /api/auth/sign-up/email` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.signUpEmail()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
322
|
+
| 6 | `'auth.sign-in.email'` | `POST /api/auth/sign-in/email` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.signInEmail()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
323
|
+
| 7 | `'auth.sign-in.social'` | `POST /api/auth/sign-in/social` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.signInSocial()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
324
|
+
| 8 | `'auth.email.send-verification'` | `POST /api/auth/email/send-verification` | `'rest'` | `'user'` | — | `false` | `true` | `core.auth.sendVerificationEmail()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
325
|
+
| 9 | `'auth.email.verify'` | `POST /api/auth/email/verify` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.verifyEmail()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
326
|
+
| 10 | `'auth.password.forgot'` | `POST /api/auth/password/forgot` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.forgotPassword()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
327
|
+
| 11 | `'auth.password.reset'` | `POST /api/auth/password/reset` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.resetPassword()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
328
|
+
| 12 | `'auth.recovery-email.set'` | `POST /api/auth/recovery-email/set` | `'rest'` | `'strict'` | — | `false` | `true` | `core.auth.setRecoveryEmail()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
329
|
+
| 13 | `'auth.recovery-email.verify'` | `POST /api/auth/recovery-email/verify` | `'rest'` | `'strict'` | — | `false` | `true` | `core.auth.verifyRecoveryEmail()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
330
|
+
| 14 | `'auth.account-recovery.initiate'` | `POST /api/auth/account-recovery/initiate` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.initiateAccountRecovery()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
331
|
+
| 15 | `'auth.account-recovery.complete'` | `POST /api/auth/account-recovery/complete` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.completeAccountRecovery()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
332
|
+
| 16 | `'auth.users.list'` | `GET /api/auth/users` | `'rest'` | `'admin'` | — | `false` | `true` | `core.auth.listUsers()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
333
|
+
| 17 | `'auth.users.block'` | `POST /api/auth/users/block` | `'rest'` | `'strict'` | `{ checkUserId: true }` | `false` | `true` | `core.auth.blockUser()` (`auth/routes.ts` → `auth/service.ts`) |
|
|
334
|
+
| 18 | `'auth.users.delete'` | `POST /api/erase-user` | `'workflow'` | `'strict'` | `{ checkUserId: true }` | `false` | `true` | `core.auth.eraseUser()` (`auth/routes.ts` → `auth/service.ts`, `workflowName: 'erase-user'`) |
|
|
335
|
+
| 19 | `'auth.sign-out'` | `POST /api/auth/sign-out` | `'rest'` | `'public'` | — | `false` | `true` | `core.auth.revokeSession()` + `deleteCookie` (`auth/routes.ts`) |
|
|
336
|
+
| 20 | `'files.upload'` | `POST /api/files` | `'rest'` | `'user'` | — | `false` | `true` | `core.files.upload()` (`files/routes.ts` → `files/service.ts`) |
|
|
337
|
+
| 21 | `'files.list'` | `GET /api/files` | `'rest'` | `'user'` | — | `false` | `true` | `core.db.list('file', { userId })` (`files/routes.ts`) |
|
|
338
|
+
| 22 | `'files.get'` | `GET /api/files/:ref` | `'rest'` | `'user'` | `{ checkFileRef: true }` | `false` | `true` | `core.db.read` (`?meta=true`), `core.files.getSignedDownloadUrl` (`?signed=true`), or `core.files.get()` (`files/routes.ts`) |
|
|
339
|
+
| 23 | `'files.delete'` | `DELETE /api/files/:ref` | `'rest'` | `'user'` | `{ checkFileRef: true }` | `false` | `true` | `core.files.delete()` (`files/routes.ts` → `files/service.ts`) |
|
|
340
|
+
| 24 | `'chat.list'` | `GET /api/chats` | `'rest'` | `'user'` | — | `false` | `true` | `core.db.list('chat', ...)` (`chat/routes.ts`) |
|
|
341
|
+
| 25 | `'chat.get'` | `GET /api/chats/:chatId` | `'rest'` | `'user'` | `{ checkCollectionField: { collection: 'chat', param: 'chatId' } }` | `false` | `true` | `core.db.read('chat', chatId)` (`chat/routes.ts`) |
|
|
342
|
+
| 26 | `'chat.messages'` | `GET /api/chats/:chatId/messages` | `'rest'` | `'user'` | `{ checkCollectionField: { collection: 'chat', param: 'chatId' } }` | `false` | `true` | `core.chat.listMessagesRoute()` (`chat/routes.ts` → `chat/service.ts`) |
|
|
343
|
+
| 27 | `'chat.save'` | `POST /api/chats` | `'rest'` | `'user'` | — | `false` | `true` | `core.chat.saveChatRoute()` (`chat/routes.ts` → `chat/service.ts`) |
|
|
344
|
+
| 28 | `'chat.delete'` | `DELETE /api/chats/:chatId` | `'rest'` | `'user'` | `{ checkCollectionField: { collection: 'chat', param: 'chatId' } }` | `false` | `true` | `core.db.delete('chat', chatId)` (`chat/routes.ts`) |
|
|
345
|
+
| 29 | `'chat.stream'` | `POST /api/chats/:chatId` | `'rest'` | `'user'` | — | `false` | `true` | `core.chat.streamChatRoute()` (`chat/routes.ts` → `chat/service.ts`) |
|
|
346
|
+
| 30 | `'chat.cancel'` | `POST /api/chats/:chatId/cancel` | `'rest'` | `'user'` | `{ checkCollectionField: { collection: 'chat', param: 'chatId' } }` | `false` | `true` | `core.chat.cancelChatRoute()` (`chat/routes.ts` → `chat/service.ts`) |
|
|
347
|
+
| 31 | `'openai.models'` | `GET /api/v1/models` | `'rest'` | `'user'` | — | `false` | `true` | `core.chat.listOpenaiModelsRoute()` (`chat/routes.ts` → `chat/service.ts`) |
|
|
348
|
+
| 32 | `'openai.completions'` | `POST /api/v1/chat/completions` | `'rest'` | `'user'` | — | `false` | `true` | `core.chat.openaiCompletionsRoute()` (`chat/routes.ts` → `chat/service.ts`) |
|
|
349
|
+
| 33 | `'attachments.scan'` | `POST /api/scan-attachment` | `'workflow'` | `'user'` | `{ checkFileRef: true }` | `true` | `true` | `core.attachments.scan()` + `core.attachments.scanShortCircuit` (`attachments/routes.ts`) |
|
|
350
|
+
| 34 | `'attachments.process'` | `POST /api/process-attachments` | `'workflow'` | `'user'` | `{ checkFileRef: true }` | `true` | `true` | `core.attachments.process()` (`attachments/routes.ts`) |
|
|
351
|
+
| 35 | `'routes.list'` | `GET /api/routes` | `'rest'` | `'admin'` | — | `false` | `true` | `buildRoutesListRoute()` (`tasks/routes.ts` — JSON or `?format=md` Markdown) |
|
|
352
|
+
| 36 | `'tasks.status'` | `GET /api/tasks/:taskId` | `'rest'` | `'user'` | — | `false` | `true` | `buildTaskStatusRoute()` (`tasks/routes.ts` → `core.tasks.status()`) |
|
|
353
|
+
| 37 | `'tasks.signal'` | `POST /api/tasks/:taskId/signal` | `'rest'` | `'user'` | — | `false` | `true` | `buildTaskSignalRoute()` (`tasks/routes.ts` → `core.tasks.signal()`) |
|
|
354
|
+
| 38 | `'tasks.cancel'` | `DELETE /api/tasks/:taskId` | `'rest'` | `'user'` | — | `false` | `true` | `buildTaskCancelRoute()` (`tasks/routes.ts` → `core.tasks.cancel()`) |
|
|
355
|
+
| 39 | `'emails.send'` | `POST /api/emails/send` | `'rest'` | `'admin'` | — | `false` | `true` | `core.emails.send()` (`emails/routes.ts` → `emails/service.ts`) |
|
|
356
|
+
| 40 | `'emails.batch'` | `POST /api/emails/batch` | `'rest'` | `'admin'` | — | `false` | `true` | `core.emails.sendBatch()` (`emails/routes.ts` → `emails/service.ts`) |
|
|
357
|
+
| 41 | `'emails.logs'` | `GET /api/emails/logs` | `'rest'` | `'admin'` | — | `false` | `true` | `core.emails.listLogs()` (`emails/routes.ts` → `emails/service.ts`) |
|
|
358
|
+
| 42 | `'emails.webhook'` | `POST /api/emails/webhooks/:provider` | `'rest'` | `'none'` | — | `false` | `false` | `core.internals.emails.verifyAndProcessWebhook()` (`emails/routes.ts` → `emails/service.ts`) |
|
|
359
|
+
| 43 | `'payments.checkout'` | `POST /api/payments/checkout` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.createCheckout()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
360
|
+
| 44 | `'payments.paymentLink'` | `POST /api/payments/payment-link` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.createPaymentLink()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
361
|
+
| 45 | `'payments.invoice'` | `POST /api/payments/invoice` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.createInvoice()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
362
|
+
| 46 | `'payments.charge'` | `POST /api/payments/charge` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.charge()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
363
|
+
| 47 | `'payments.subscribe'` | `POST /api/payments/subscribe` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.subscribe()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
364
|
+
| 48 | `'payments.changeTier'` | `POST /api/payments/subscription/change-tier` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.changeSubscriptionTier()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
365
|
+
| 49 | `'payments.cancelSubscription'` | `POST /api/payments/subscription/cancel` | `'rest'` | `'user'` | — | `false` | `true` | `core.payments.cancelSubscription()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
366
|
+
| 50 | `'payments.status'` | `GET /api/payments/status` | `'rest'` | `'user'` | — | `false` | `true` | `{ subscription: core.payments.getSubscription(), usage: core.payments.getUsage() }` (`payments/routes.ts`) |
|
|
367
|
+
| 51 | `'payments.webhook'` | `POST /api/payments/webhooks/:provider` | `'rest'` | `'none'` | — | `false` | `false` | `core.internals.payments.verifyAndProcessWebhook()` (`payments/routes.ts` → `payments/service.ts`) |
|
|
368
|
+
| *52* | *MCP Server* | `ALL /api/mcp` | `'rest'` | `'user'` | — | — | — | `mcpRoute()` (`chat/routes.ts`, mounted directly in `router/service.ts`) |
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### 3.4 Master In-Memory Registries, Caches & `AsyncLocalStorage` Stores
|
|
373
|
+
|
|
374
|
+
| State Variable | File | Type | Scope / Eviction Policy | Purpose |
|
|
375
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
376
|
+
| `coreStorage` | `src/core/index.ts` | `AsyncLocalStorage<CoreRuntimeState>` | Per async execution tree | Propagates `{ env, userId, role, step }` to `core` and `useCore()`. |
|
|
377
|
+
| `workflowStorage` | `src/core/tasks/worker.ts` | `AsyncLocalStorage<ActiveWorkflowStore>` | Per workflow execution tree | Propagates `{ workflow, step, env }` to `useStep()` and `getWorkflowContext()`. |
|
|
378
|
+
| `dbs` / `buckets` | `src/core/db/service.ts` | `[Firestore?, Firestore?]` / `[Bucket?, Bucket?]` | Process singleton (`[prod, dev]`) | Lazy-initialized Firestore & Storage Bucket instances for `isDev = false \| true`. |
|
|
379
|
+
| `cachedToken` / `cachedTokenExp` | `src/core/db/service.ts` | `string` / `number` | Refreshed 60s before expiry | Cached GCP ADC OAuth2 access token used by Cloud Tasks & Firestore Admin export. |
|
|
380
|
+
| `collectionRegistry` | `src/core/db/schema.ts` | `Map<string, CollectionDefinition<any>>` | Populated at `createCoreApp()` | Holds `chat`, `file`, `user_auth_meta`, `auth_tokens`, `email_log`, `email_suppression`, `payment_customer`, `payment_usage`, `payment_transaction` + user `collections`. |
|
|
381
|
+
| `registeredAttachmentTypes` | `src/core/attachments/schema.ts` | `AttachmentTypesMap` | Populated at `createCoreApp()` | Holds document schemas from `init.attachmentTypes` and `agents[].attachmentTypes`. |
|
|
382
|
+
| `emailTemplateRegistry` | `src/core/emails/schema.ts` | `Map<string, EmailTemplateDefinition<any>>` | Populated at `createCoreApp()` | Holds registered transactional email templates from `init.emailTemplates`. |
|
|
383
|
+
| `registeredPlans` / `registeredProducts` | `src/core/payments/schema.ts` | `Record<string, PlanDefinition>` / `Record<string, ProductDefinition>` | Populated via `definePlans` / `defineProducts` / `createCoreApp()` | Holds registered subscription tiers and one-time purchasable products/token packs. |
|
|
384
|
+
| `agentRegistry` | `src/core/chat/service.ts` | `Map<string, AgentConfig>` | Populated at `createCoreApp()` | Registered AI agents by agent ID. |
|
|
385
|
+
| `toolRegistry` | `src/core/chat/service.ts` | `Map<string, WorkflowToolSpec>` | Populated at `createCoreApp()` | Sanitized route/workflow tools available to AI agents and `/api/mcp`. |
|
|
386
|
+
| `contextCatalog` / `markdownCache` | `src/core/chat/context.ts` | `readonly ContextItemSpec[]` / `Map<string, string>` | Populated at `createCoreApp()` / lazy file cache | InfoBase `.md` catalog and in-memory file contents cache. |
|
|
387
|
+
| `activeGenerations` | `src/core/chat/stream.ts` | `Map<string, ActiveGenerationEntry>` | Keyed by `${isDev}:${chatId}`; deleted in `finally` | Tracks in-flight SSE chat streams for instant cancellation & double-submit locking. |
|
|
388
|
+
| `tokenCache` | `src/core/auth/session.ts` | `Map<string, CachedTokenEntry>` | Max `2000` entries (FIFO eviction), `5m` TTL | Caches verified session cookies and Firebase Bearer ID tokens. |
|
|
389
|
+
| `revocationWatermarks` | `src/core/auth/session.ts` | `[RevocationWatermarkState, RevocationWatermarkState]` | Synced every `2s`/`3s` with `_system/auth_revocations` | Tracks per-UID revocation timestamps for prod (`[0]`) and dev (`[1]`). |
|
|
390
|
+
| `jwksCache` | `src/core/auth/oidc.ts` | `Map<OAuthProvider, JwksCacheEntry>` | `1h` TTL (`60 * 60 * 1000` ms) | Caches Google and Apple OIDC public JWKs. |
|
|
391
|
+
| `#devEmailOutbox` | `src/core/emails/service.ts` | `Map<string, TransactionalEmailPayload>` | Keyed by `${purpose}:${email}`, max `500` entries (dev/test only) | Captures transactional OTP/magic-link emails in memory for dev/E2E tests (`getLastDevEmail`). |
|
|
392
|
+
| `cachedJev` / `cachedMistral` | `src/core/ai/service.ts` | `TypeSafeClient` / `Mistral` | Re-instantiated only if API key changes | Cached upstream AI SDK clients (Mistral client includes OCR rate-limit response hook). |
|
|
393
|
+
| `activeTaskExecutions` | `src/core/tasks/worker.ts` | `Map<string, ActiveTaskExecution>` | Registered during `executeWorkflowTask` | Allows `cancel()` on the same instance to abort & compensate an in-flight worker immediately. |
|
|
394
|
+
| `compensationLocks` | `src/core/tasks/utils.ts` | `Map<string, Promise<void>>` | Per `taskId` promise chain | Serializes Saga `compensate()` calls so worker & cancel paths never double-run rollbacks. |
|
|
395
|
+
| `batchQueue` | `src/core/tasks/watcher.ts` | `BatchItem[]` | Flushed on `queueMicrotask` in chunks of `100` across `20` shards | Coalesces concurrent Cloud Tasks dispatches into `tasks:batchCreate` HTTP calls. |
|
|
396
|
+
| `activeWatcherCount` | `src/core/tasks/watcher.ts` | `number` | Incremented during `watchTask`, decremented in `finally` | Tracks active Firestore task watchers (`getActiveWatcherCount()`). |
|
|
397
|
+
| `inflight` | `src/core/utils/service.ts` | `Map<string, Promise<unknown>>` | Deleted in `.finally()` when promise settles | Coalesces identical concurrent operations (`tenant:<userId>:<key>` or `global:<key>`). |
|
|
398
|
+
| `limiters` / `httpCounters` / `breakerCache` | `src/core/utils/service.ts` | `Map`s | Keyed by `${isDev}:${bucket}` / 5,000 max keys / `3s` TTL | Upstream token-bucket limiters, per-user/IP HTTP counters, and Firestore circuit breaker cache. |
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 4. Subsystem Architecture & Call-Graph Diagrams (Mermaid)
|
|
403
|
+
|
|
404
|
+
### 4.1 Request & Route Execution Pipeline (`rest` vs `workflow`)
|
|
405
|
+
|
|
406
|
+
```mermaid
|
|
407
|
+
flowchart TD
|
|
408
|
+
Req["Incoming HTTP Request (/api/*)"] --> CORS["CORS + 25MiB BodyLimit + Env Binding Middleware (router/service.ts)"]
|
|
409
|
+
CORS --> Auth["#authenticateRequest(c, cfg) (router/service.ts)"]
|
|
410
|
+
Auth --> RL["#applyRateLimit(c, cfg, userId) -> resilience.acquireHttpLimit() (utils/service.ts)"]
|
|
411
|
+
RL --> Invoke["Route Invoker: invoke(rawBody, env, userId, c, role, { wait }) (router/service.ts)"]
|
|
412
|
+
Invoke --> ZodIn["Parse params, query, and input via Zod schemas"]
|
|
413
|
+
ZodIn --> Guard["#enforceGuard(c, cfg, userId, parsed, role) (router/service.ts)"]
|
|
414
|
+
Guard --> ModeCheck{"cfg.mode === 'rest'?"}
|
|
415
|
+
|
|
416
|
+
ModeCheck -- "'rest' (default)" --> RestStep["tasks.createStepRunner('rest') (tasks/service.ts)"]
|
|
417
|
+
RestStep --> RunCore["runWithCore({ env, userId, role, step }) (index.ts)"]
|
|
418
|
+
RunCore --> HandlerRest["cfg.handler(makeCtx(...))"]
|
|
419
|
+
HandlerRest --> ParseOut["Validate cfg.output (if defined) & return 200 JSON / Response"]
|
|
420
|
+
|
|
421
|
+
ModeCheck -- "'workflow'" --> ShortCircuit{"cfg.shortCircuit defined?"}
|
|
422
|
+
ShortCircuit -- "Hit (non-null)" --> ParseOut
|
|
423
|
+
ShortCircuit -- "Miss / Undefined" --> Dispatch["tasks.dispatchWorkflowTask(wfName, runTask, parsed, opts) (tasks/service.ts)"]
|
|
424
|
+
Dispatch --> AsyncCheck{"Prefer: respond-async or ?wait=false or maxWaitMs (60s) detached?"}
|
|
425
|
+
AsyncCheck -- "Detached / Async" --> Res202["202 Accepted { taskId, status: 'running', pollUrl: '/api/tasks/:taskId' }"]
|
|
426
|
+
AsyncCheck -- "Completed within wait window" --> ParseOut
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 4.2 Durable Workflow Engine, Epoch Fencing & Saga Compensation
|
|
430
|
+
|
|
431
|
+
```mermaid
|
|
432
|
+
flowchart TD
|
|
433
|
+
Dispatch["tasks.dispatchWorkflowTask() (tasks/service.ts)"] --> Dedupe{"dedupeKey provided?"}
|
|
434
|
+
Dedupe -- "Yes: Transaction on _tasks/dedupe_<hash>" --> JoinOrInit["Join active run / return completed response / init epoch: 0"]
|
|
435
|
+
Dedupe -- "No: Create _tasks/<taskId> (epoch: 0)" --> Enqueue
|
|
436
|
+
JoinOrInit --> Enqueue["#enqueueBatched(payload, deadline) + watchTask(taskId) (tasks/service.ts)"]
|
|
437
|
+
|
|
438
|
+
Enqueue --> MicroBatch["queueMicrotask -> Shard 0..19 -> Cloud Tasks tasks:batchCreate"]
|
|
439
|
+
MicroBatch --> Worker["tasks.executeWorkflowTask(payload) on Cloud Functions Worker (tasks/service.ts)"]
|
|
440
|
+
|
|
441
|
+
Worker --> ClaimTx["Firestore Transaction: Verify expectedEpoch & non-terminal status -> Increment epoch = claimedEpoch"]
|
|
442
|
+
ClaimTx --> Heartbeat["Start 20s Heartbeat Transaction Timer (aborts controller on StaleWorkerEpochError)"]
|
|
443
|
+
Heartbeat --> StepRun["#runWithWorkflowContext -> wf.runTask(effectiveParams, step)"]
|
|
444
|
+
|
|
445
|
+
StepRun --> StepExec["executeSingleStep(stepId) (tasks/service.ts)"]
|
|
446
|
+
StepExec --> CachedCheck{"stepId in steps?"}
|
|
447
|
+
CachedCheck -- "Yes (Checkpoint Hit)" --> Hydrate["#maybeHydrateStepValue() (from Firestore or GCS _tasks/<id>/steps/<stepId>.json)"]
|
|
448
|
+
CachedCheck -- "No (Execute)" --> BudgetCheck["checkTimeBudgetYield() (75% of deadlineSeconds)"]
|
|
449
|
+
BudgetCheck -- "Exceeded" --> YieldSig["Throw TaskYieldSignal('yield') -> Increment epoch & re-enqueue"]
|
|
450
|
+
BudgetCheck -- "Within Budget" --> RunStep["#runStep(fn, retryPolicy, timeout, signal) (tasks/service.ts)"]
|
|
451
|
+
RunStep --> Offload["#maybeOffloadStepValue() (>200KiB single or >150KiB cumulative -> GCS Claim-Check)"]
|
|
452
|
+
Offload --> FencedPersist["runFencedPersistOnce() Transaction: Verify doc.epoch === claimedEpoch & merge steps"]
|
|
453
|
+
|
|
454
|
+
StepRun -- "Throws Error (Terminal)" --> CompTx["Transaction: Set status='compensating', epoch = claimedEpoch + 1"]
|
|
455
|
+
CompTx --> Saga["step.compensate() -> runSerializedCompensation() in reverse order"]
|
|
456
|
+
Saga --> MarkErr["Set status='errored' on _tasks/<taskId>"]
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### 4.3 Attachment Scanning & Sharded Fan-Out Pipeline
|
|
460
|
+
|
|
461
|
+
```mermaid
|
|
462
|
+
flowchart LR
|
|
463
|
+
Proc["core.attachments.process({ refs }) (attachments/service.ts)"] --> Preload["Batch-fetch existing 'file' docs via db.list(ids, bypassRbac: true)"]
|
|
464
|
+
Preload --> StepMap["step.map('process-attachments', tasks, #scanAttachmentTask, { shortCircuit })"]
|
|
465
|
+
StepMap -- "shortCircuit Hit (fileDoc.scan / scanError)" --> CachedOut["Return cached { ref, scan } or { ref, error } immediately"]
|
|
466
|
+
StepMap -- "Cache Miss (20-way worker pool)" --> Scan["attachments.scan(input, { types, step })"]
|
|
467
|
+
|
|
468
|
+
Scan --> SF["resilience.singleflight('scan:<env>:<ref>:<type>')"]
|
|
469
|
+
SF --> ShaDedup["#findCrossUserByShaSum(ref, shaSum, type) (cross-user Firestore dedupe)"]
|
|
470
|
+
ShaDedup -- "Miss" --> OCR["step('ocr'): ai.vision(dataUrl) -> Mistral OCR (or Vertex Vision if breaker OPEN)"]
|
|
471
|
+
OCR --> Classify["step('classify'): attachments.classify(markdown) -> ai.decide() (TypeSafe Jev)"]
|
|
472
|
+
Classify --> Extract["step('extract'): ai.vision(dataUrl, { schema }) -> parseFields() via Zod"]
|
|
473
|
+
Extract --> Persist["step('persist'): db.update('file', ref, { scan, scanError: null })"]
|
|
474
|
+
Persist --> Defects{"Permanent Defect? (400/415/422/encrypted/blank)"}
|
|
475
|
+
Defects -- "Yes" --> PersistErr["Persist FileScanError on 'file' doc & throw NonRetryableError"]
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 4.4 Declarative AI Agent, Hybrid RAG Context & Streaming Pipeline
|
|
479
|
+
|
|
480
|
+
```mermaid
|
|
481
|
+
flowchart TD
|
|
482
|
+
Client["POST /api/chats/:chatId (streamChatRoute) or POST /api/v1/chat/completions"] --> Lock["Acquire activeGenerations lock + set activeGenerationId on 'chat' doc + attach onSnapshot cancel watcher"]
|
|
483
|
+
Lock --> ParallelPrep["Promise.all: Stage Attachments + Hybrid RAG Retrieval"]
|
|
484
|
+
|
|
485
|
+
ParallelPrep --> Stage["#stageAndScanAttachments(): Upload data URLs / verify FileRefs -> attachments.process()"]
|
|
486
|
+
ParallelPrep --> Hybrid["chat.retrieveContext() (chat/service.ts)"]
|
|
487
|
+
|
|
488
|
+
Hybrid --> H1["1. Sliding Window: Last 15 messages from chat/{chatId}/messages"]
|
|
489
|
+
Hybrid --> H2["2. Vector Recall: ai.embed(prompt) -> in-chat (top 5) + cross-chat collectionGroup (top 3) COSINE <= 0.35"]
|
|
490
|
+
Hybrid --> H3["3. Per-User Context: agent.resolveUserContext({ userId, core, signal }) (3.5s timeout)"]
|
|
491
|
+
Hybrid --> H4["4. InfoBase Selection: chat.getContext(prompt, catalog, { audiences, userState }) via Jev 'noul' >= 0.5"]
|
|
492
|
+
|
|
493
|
+
Stage & H1 & H2 & H3 & H4 --> RunAgent["chat.stream(agent, messages, userId, opts) (chat/service.ts)"]
|
|
494
|
+
RunAgent --> SysPrompts["System Message 1: Static Instructions (Anthropic/Vertex ephemeral cacheControl)\nSystem Message 2: #buildDynamicContextBlock (<user_state>, <user_records>, <recalled_memory>, <infobase_context>, <staged_attachments>)"]
|
|
495
|
+
SysPrompts --> StreamText["AI SDK streamText() with #buildAgentToolsForTurn() + prepareStep(#resolveStepActiveTools)"]
|
|
496
|
+
StreamText --> Tee["Tee SSE Stream: Client Response Stream + Background Monitor -> Atomic Firestore Batch Commit (user msg + assistant msg + vectors)"]
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 4.5 Authentication, OIDC Social Login, Token Cache & Revocation Sync
|
|
500
|
+
|
|
501
|
+
```mermaid
|
|
502
|
+
flowchart TD
|
|
503
|
+
TokenIn["#authenticateRequest(): Bearer Token or Session Cookie"] --> MasterCheck{"Matches SDK_API_KEY (timingSafeEqual)?"}
|
|
504
|
+
MasterCheck -- "Yes" --> AdminCtx["userId = 'admin', role = 'admin'"]
|
|
505
|
+
MasterCheck -- "No" --> Verify["auth.verifyToken(token, isCookie, checkRevoked, isDev) (auth/service.ts)"]
|
|
506
|
+
|
|
507
|
+
Verify --> CacheHit{"In tokenCache & not expired?"}
|
|
508
|
+
CacheHit -- "Yes" --> SyncCheck["If >3s since last sync -> async auth.syncRevocations() (_system/auth_revocations)"]
|
|
509
|
+
SyncCheck --> RevokedCheck{"revokedUsers[uid] >= cachedAt?"}
|
|
510
|
+
RevokedCheck -- "No (Valid)" --> ReturnAuth["Return { uid, role, emailVerified }"]
|
|
511
|
+
RevokedCheck -- "Yes (Revoked)" --> Evict["#evictUserTokens(uid) -> Re-verify with Firebase Auth"]
|
|
512
|
+
CacheHit -- "No" --> Evict
|
|
513
|
+
Evict --> FbVerify["singleflight -> getAuth().verifySessionCookie / verifyIdToken -> auth.setTokenCache()"]
|
|
514
|
+
FbVerify --> ReturnAuth
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## 5. Entrypoint, Core Class & Config Module (`index.ts`, `config.ts`) — Per-Function Spec
|
|
520
|
+
|
|
521
|
+
### 5.1 [`src/core/index.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/index.ts)
|
|
522
|
+
|
|
523
|
+
The sole public entrypoint for `@core`, defining `Core`, `core`, `useCore`, `runWithCore`, `getActiveCoreState`, and the `core.types.*` namespace.
|
|
524
|
+
|
|
525
|
+
#### Module-Level State
|
|
526
|
+
- **`coreStorage`**: `new AsyncLocalStorage<CoreRuntimeState>()` — Stores `{ env: AppBindings, userId: EntryId, role: DbRole, step?: StepRunner }` across async call trees.
|
|
527
|
+
|
|
528
|
+
#### Functions & Class Methods in `src/core/index.ts`
|
|
529
|
+
|
|
530
|
+
##### 1. `runWithCore`
|
|
531
|
+
- **Tier**: `Tier 2: Internal Hook` (`core.internals.context.run`) & `Tier 3: Module Export`
|
|
532
|
+
- **Signature**:
|
|
533
|
+
```ts
|
|
534
|
+
export const runWithCore = <T>(state: CoreRuntimeState, fn: () => Promise<T>): Promise<T>
|
|
535
|
+
```
|
|
536
|
+
- **Contract**: Executes `fn` inside `coreStorage.run(state, fn)` so any downstream call to `core.*` or `useCore()` inherits `{ env, userId, role, step }`.
|
|
537
|
+
|
|
538
|
+
##### 2. `getActiveCoreState`
|
|
539
|
+
- **Tier**: `Tier 2: Internal Hook` (`core.internals.context.getState`) & `Tier 3: Module Export`
|
|
540
|
+
- **Signature**:
|
|
541
|
+
```ts
|
|
542
|
+
export const getActiveCoreState = (): CoreRuntimeState | undefined
|
|
543
|
+
```
|
|
544
|
+
- **Contract**: Returns `coreStorage.getStore()`, or `undefined` when called outside an active request/workflow context.
|
|
545
|
+
|
|
546
|
+
##### 3. `Core.constructor`, `Core.#state`, and `Core.#defaultEnv`
|
|
547
|
+
- **Tier**: `Tier 1: Public` (`new Core(state?)`) / `Tier 4: Private` (`#state`, `#defaultEnv`)
|
|
548
|
+
- **Signature**:
|
|
549
|
+
```ts
|
|
550
|
+
constructor(state?: CoreRuntimeState)
|
|
551
|
+
get #state(): CoreRuntimeState
|
|
552
|
+
#defaultEnv = (isDev = false): AppBindings
|
|
553
|
+
```
|
|
554
|
+
- **Resolution Order**:
|
|
555
|
+
1. Explicit `this.#explicitState` passed to `new Core(state)`.
|
|
556
|
+
2. Active `coreStorage.getStore()` from `AsyncLocalStorage`.
|
|
557
|
+
3. Active workflow store `this.tasks.getActiveWorkflowStore()` + `this.#defaultEnv(isDevDefault)` with `{ userId: CORE_CONFIG.auth.adminUserId, role: 'admin' }`.
|
|
558
|
+
|
|
559
|
+
##### 4. `Core` Context Getters (`env`, `userId`, `role`, `caller`, `step`)
|
|
560
|
+
- **Tier**: `Tier 1: Public (core.*)`
|
|
561
|
+
- **Signatures**:
|
|
562
|
+
```ts
|
|
563
|
+
get env(): AppBindings
|
|
564
|
+
get userId(): EntryId
|
|
565
|
+
get role(): DbRole
|
|
566
|
+
get caller(): { userId: EntryId; role: DbRole }
|
|
567
|
+
get step(): StepRunner
|
|
568
|
+
```
|
|
569
|
+
- **Contract**: Reads from `this.#state`; `this.step` returns `this.#state.step ?? this.tasks.useStep()`.
|
|
570
|
+
|
|
571
|
+
##### 5. `Core` Service Composition (`errors`, `resilience`, `db`, `files`, `ai`, `attachments`, `tasks`, `auth`, `chat`, `router`, `createApp`, `define`, `schemas`, `internals`)
|
|
572
|
+
- **Tier**: `Tier 1: Public (core.*)` (and `Tier 2` for `core.internals`)
|
|
573
|
+
- **Contract**: Instantiates domain `*Service` classes passing `this` (`new ErrorsService(this)`, `new ResilienceService(this)`, `new DbService(this)`, `new FilesService(this)`, `new AiService(this)`, `new AttachmentsService(this)`, `new TasksService(this)`, `new AuthService(this)`, `new ChatService(this)`, `new RouterService(this)`). Exposes `core.internals` via a non-enumerable prototype getter (`get internals()`).
|
|
574
|
+
|
|
575
|
+
##### 6. `useCore`
|
|
576
|
+
- **Tier**: `Tier 1: Public (@core)`
|
|
577
|
+
- **Signature**:
|
|
578
|
+
```ts
|
|
579
|
+
export const useCore = (): Core
|
|
580
|
+
```
|
|
581
|
+
- **Contract**: Checks `const active = coreStorage.getStore()`; if present, returns a snapshot-bound `new Core(active)`, otherwise returns the `core` singleton.
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
### 5.2 [`src/core/config.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/config.ts)
|
|
586
|
+
|
|
587
|
+
Centralized platform configuration, secret names, RBAC types, and deep-merge override helper.
|
|
588
|
+
|
|
589
|
+
#### Constants & State
|
|
590
|
+
- **`SECRET_NAMES`**: `['SDK_API_KEY', 'MISTRAL_API_KEY', 'TYPESAFE_API_KEY', 'JINA_API_KEY', 'WEB_API_KEY'] as const`
|
|
591
|
+
- **`CORE_CONFIG`**: `CoreConfig` singleton initialized from `process.env.GCP_PROJECT!` and `region = 'me-central1'`.
|
|
592
|
+
|
|
593
|
+
#### Functions in `src/core/config.ts`
|
|
594
|
+
|
|
595
|
+
##### 1. `mergeDeep`
|
|
596
|
+
- **Tier**: `Tier 4: File-Private`
|
|
597
|
+
- **Signature**:
|
|
598
|
+
```ts
|
|
599
|
+
const mergeDeep = <T extends Record<string, any>>(target: T, source?: DeepPartial<T>): T
|
|
600
|
+
```
|
|
601
|
+
- **Contract**: Recursively mutates `target` in-place with defined keys from `source`, preserving arrays and `RegExp` instances as atomic values.
|
|
602
|
+
|
|
603
|
+
##### 2. `defineConfig`
|
|
604
|
+
- **Tier**: `Tier 1: Public` (`core.define.config`) & `Tier 3: Module Export`
|
|
605
|
+
- **Signature**:
|
|
606
|
+
```ts
|
|
607
|
+
export const defineConfig = (overrides?: DeepPartial<CoreConfig>): CoreConfig
|
|
608
|
+
```
|
|
609
|
+
- **Contract**: If `overrides?.gcpProject` differs from `CORE_CONFIG.gcpProject`, automatically recomputes `appName`, `firestoreDbs`, `storageBuckets`, `serviceAccount`, `workerUris`, and `queueBaseNames` before calling `mergeDeep(CORE_CONFIG, overrides)`. Mutates and returns `CORE_CONFIG`.
|
|
610
|
+
|
|
611
|
+
#### Full `CoreConfig` & `AppBindings` Type Definitions
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
export type DbRole = 'admin' | 'user' | 'anon'
|
|
615
|
+
export type DbAction = 'read' | 'write' | 'create' | 'delete' | '*'
|
|
616
|
+
export type DbAccessScope = 'own' | 'any'
|
|
617
|
+
export type RbacRoleScopes = { [Scope in DbAccessScope]?: readonly DbAction[] }
|
|
618
|
+
export type RbacConfig = { [Role in DbRole]?: RbacRoleScopes }
|
|
619
|
+
|
|
620
|
+
export type AuthTokenPurpose =
|
|
621
|
+
| 'verify_email'
|
|
622
|
+
| 'reset_password'
|
|
623
|
+
| 'verify_recovery_email'
|
|
624
|
+
| 'account_recovery'
|
|
625
|
+
|
|
626
|
+
export interface TransactionalEmailPayload {
|
|
627
|
+
to: string
|
|
628
|
+
purpose: AuthTokenPurpose
|
|
629
|
+
code: string
|
|
630
|
+
token: string
|
|
631
|
+
expiresInMinutes: number
|
|
632
|
+
userId: string
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export type EmailSenderFn = (payload: TransactionalEmailPayload, env: AppBindings) => Promise<void>
|
|
636
|
+
|
|
637
|
+
export interface CoreConfig {
|
|
638
|
+
appName: string
|
|
639
|
+
gcpProject: string
|
|
640
|
+
region: string
|
|
641
|
+
secrets: readonly string[]
|
|
642
|
+
firestoreDbs: { prod: string; dev: string }
|
|
643
|
+
storageBuckets: { prod: string; dev: string }
|
|
644
|
+
db: { rbac: RbacConfig }
|
|
645
|
+
context: { dir: string; maxSources: number; userContextTimeoutMs: number }
|
|
646
|
+
serviceAccount: string
|
|
647
|
+
computeAccount: string
|
|
648
|
+
workerUris: { prod: string; dev: string }
|
|
649
|
+
queueBaseNames: { prod: string; dev: string }
|
|
650
|
+
cloudRun: {
|
|
651
|
+
memory: '1GiB' | '2GiB' | '4GiB' | '8GiB'
|
|
652
|
+
cpu: number
|
|
653
|
+
http: { timeoutSeconds: number; concurrency: number; maxInstances: number; invoker: 'public' | 'private' }
|
|
654
|
+
worker: { timeoutSeconds: number; concurrency: number; maxInstances: number }
|
|
655
|
+
}
|
|
656
|
+
tasks: {
|
|
657
|
+
useCloudTasks: boolean
|
|
658
|
+
defaultShardsCount: number // 20
|
|
659
|
+
batchCreateSize: number // 100
|
|
660
|
+
maxAttempts: number // 3
|
|
661
|
+
hedgeAfterMs: number // 65_000
|
|
662
|
+
heartbeatIntervalMs: number // 20_000
|
|
663
|
+
heartbeatStaleMs: number // 30_000
|
|
664
|
+
timeoutMs: number // 510_000
|
|
665
|
+
ttlMs: number // 3_600_000
|
|
666
|
+
scanDeadlineSeconds: number // 120
|
|
667
|
+
defaultDeadlineSeconds: number // 540
|
|
668
|
+
claimCheckThresholdBytes: number // 204_800 (200 KiB)
|
|
669
|
+
yieldBudgetRatio: number // 0.75
|
|
670
|
+
}
|
|
671
|
+
models: {
|
|
672
|
+
vertex: string // 'claude-haiku-5-5'
|
|
673
|
+
vertexExpert: string // 'claude-sonnet-4-5'
|
|
674
|
+
vertexEmbedding: string // 'text-embedding-005'
|
|
675
|
+
vertexLocation: string // 'global'
|
|
676
|
+
mistralOcr: string // 'mistral-ocr-latest'
|
|
677
|
+
jev: string // 'jev-latest'
|
|
678
|
+
defaultAgentMaxSteps: number // 8
|
|
679
|
+
}
|
|
680
|
+
limits: {
|
|
681
|
+
maxAttachments: number // 500
|
|
682
|
+
maxChatAttachments: number // 20
|
|
683
|
+
chatMessagesLimit: number // 15
|
|
684
|
+
defaultListLimit: number // 100
|
|
685
|
+
maxFileBytes: number // 20 MiB
|
|
686
|
+
maxBodyBytes: number // 25 MiB
|
|
687
|
+
maxWaitMs: number // 60_000
|
|
688
|
+
httpUser: RateLimitSpec // { max: 300, windowMs: 60_000, keyBy: 'user' }
|
|
689
|
+
httpIp: RateLimitSpec // { max: 60, windowMs: 60_000, keyBy: 'ip' }
|
|
690
|
+
buckets: {
|
|
691
|
+
mistral: { windowMs: number; max: number } // 60s, 1250
|
|
692
|
+
jev: { windowMs: number; max: number } // 60s, 1200
|
|
693
|
+
ai: { windowMs: number; max: number } // 60s, 2000
|
|
694
|
+
workflows: { windowMs: number; max: number } // 1s, 500
|
|
695
|
+
firestore: { windowMs: number; max: number } // 1s, 500
|
|
696
|
+
storage: { windowMs: number; max: number } // 1s, 1000
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
auth: {
|
|
700
|
+
sessionCookie: string // 'session'
|
|
701
|
+
sessionExpiresInMs: number // 14 days
|
|
702
|
+
tokenCacheTtlMs: number // 5 minutes
|
|
703
|
+
tokenCacheMaxSize: number // 2000
|
|
704
|
+
adminUserId: string // 'admin'
|
|
705
|
+
allowedOrigins: (string | RegExp)[]
|
|
706
|
+
oauth?: { googleClientIds?: string[]; appleClientIds?: string[] }
|
|
707
|
+
}
|
|
708
|
+
email: {
|
|
709
|
+
sendEmail?: EmailSenderFn
|
|
710
|
+
otpTtlMinutes: number // 15
|
|
711
|
+
recoveryOtpTtlMinutes: number // 10
|
|
712
|
+
maxVerifyAttempts: number // 5
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## 6. Database & RBAC Module (`src/core/db/*`) — Per-Function Spec
|
|
721
|
+
|
|
722
|
+
### 6.1 [`src/core/db/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/db/schema.ts)
|
|
723
|
+
|
|
724
|
+
#### Schemas, Constants & State
|
|
725
|
+
- **`entryId`**: `z.string().regex(/^[A-Za-z0-9_-]{1,128}$/)` (`core.schemas.entryId`)
|
|
726
|
+
- **`fileRef`**: `z.string().regex(/^[A-Za-z0-9_-]{1,128}_[a-f0-9]{24}$/)` (`core.schemas.fileRef`)
|
|
727
|
+
- **`fileScan`**: `z.object({ type: z.string(), confidence: z.number(), fields: z.record(z.string(), z.any()), markdown: z.string() })` (`core.schemas.fileScan`)
|
|
728
|
+
- **`unparseableReason`**: `z.enum(['CORRUPTED_FILE', 'PASSWORD_PROTECTED', 'EMPTY_OR_UNREADABLE', 'UNSUPPORTED_FORMAT', 'CLASSIFICATION_REJECTED'])`
|
|
729
|
+
- **`fileScanError`**: `z.object({ status: z.literal('unparseable'), reason: unparseableReason, agentHint: z.string(), diagnosticMessage: z.string().optional(), failedAt: z.string() })`
|
|
730
|
+
- **`baseEntrySchemaShape`** *(Tier 4: File-Private)*: `{ id: entryId, deleted: z.boolean().default(false), createdAt: z.date(), updatedAt: z.date() }`
|
|
731
|
+
- **`coreCollections`** *(Tier 3: Module Export)*: Built-in collections registered via `defineCollections`:
|
|
732
|
+
1. `chat`: `{ userId, agentId, title, model, messageCount, summary, activeGenerationId }`, `1:N` on `userId`, `sortByUpdated: true`
|
|
733
|
+
2. `file`: `{ id: fileRef, shaSum, fileName, contentType, size, scan, scanError, userId }`, `1:N` on `userId`
|
|
734
|
+
3. `user_auth_meta`: `{ email, emailVerified, name, avatarUrl, recoveryEmail, recoveryEmailVerified, linkedProviders, providerKeys }`, `1:1` on `id`
|
|
735
|
+
4. `auth_tokens`: `{ userId, purpose, targetEmail, tokenHash, urlTokenHash, attempts, expiresAt, consumedAt }`, `1:N` on `userId`
|
|
736
|
+
- **`CORE_COLLECTION_NAMES`** *(Tier 4: File-Private)*: `['chat', 'file', 'user_auth_meta', 'auth_tokens'] as const`
|
|
737
|
+
- **`collectionRegistry`** *(Tier 3: Module Export)*: `Map<string, CollectionDefinition<any>>` initialized with `coreCollections`.
|
|
738
|
+
|
|
739
|
+
#### Functions in `src/core/db/schema.ts` & `src/core/db/utils.ts`
|
|
740
|
+
|
|
741
|
+
##### 1. `resolveJunction` (`src/core/db/utils.ts`)
|
|
742
|
+
- **Tier**: `Tier 4: Domain-Internal` (`src/core/db/utils.ts`)
|
|
743
|
+
- **Signature**:
|
|
744
|
+
```ts
|
|
745
|
+
export const resolveJunction = (
|
|
746
|
+
collection: string,
|
|
747
|
+
j: NonNullable<UserRelationConfig['junction']>
|
|
748
|
+
): { collection: string; userField: string; targetField: string }
|
|
749
|
+
```
|
|
750
|
+
- **Contract**: Normalizes an `'M:N'` junction configuration, defaulting `userField` to `'userId'` and `targetField` to `${collection}Id`.
|
|
751
|
+
- **Callers**: `DbService.#isEntryOwner`, `DbService.list`, & `DbService.#cascadeEraseUserCollections` (`src/core/db/service.ts`).
|
|
752
|
+
|
|
753
|
+
##### 2. `defineCollection`
|
|
754
|
+
- **Tier**: `Tier 1: Public` (`core.define.collection`) & `Tier 3: Module Export`
|
|
755
|
+
- **Signature**:
|
|
756
|
+
```ts
|
|
757
|
+
export const defineCollection = <S extends z.ZodObject<any>>(
|
|
758
|
+
def: CollectionDefinition<S>
|
|
759
|
+
): CollectionDefinition<S>
|
|
760
|
+
```
|
|
761
|
+
- **Contract**: Merges `baseEntrySchemaShape` (`id`, `deleted`, `createdAt`, `updatedAt`) beneath `def.schema.shape` and normalizes `userRelation` (`type: def.userRelation?.type ?? '1:N'`, `userField: relType === '1:1' ? 'id' : 'userId'`).
|
|
762
|
+
|
|
763
|
+
##### 3. `defineCollections`
|
|
764
|
+
- **Tier**: `Tier 1: Public` (`core.define.collections`) & `Tier 3: Module Export`
|
|
765
|
+
- **Signature**:
|
|
766
|
+
```ts
|
|
767
|
+
export const defineCollections = <T extends Record<string, CollectionDefinition<any>>>(defs: T): T
|
|
768
|
+
```
|
|
769
|
+
- **Contract**: Maps over `defs`, injecting each key as `def.name` and wrapping each entry with `defineCollection()`.
|
|
770
|
+
|
|
771
|
+
##### 4. `registerCollections`
|
|
772
|
+
- **Tier**: `Tier 2: Internal Hook` (`core.internals.registry.registerCollections`) & `Tier 3: Module Export`
|
|
773
|
+
- **Signature**:
|
|
774
|
+
```ts
|
|
775
|
+
export const registerCollections = (
|
|
776
|
+
defs: Record<string, CollectionDefinition<any>>,
|
|
777
|
+
reset = false
|
|
778
|
+
): void
|
|
779
|
+
```
|
|
780
|
+
- **Contract**: When `reset === true`, clears `collectionRegistry` and restores the 6 `coreCollections` (`chat`, `file`, `user_auth_meta`, `auth_tokens`, `email_log`, `email_suppression`), then normalizes and inserts all `defs` into `collectionRegistry`.
|
|
781
|
+
- **Callers**: `createCoreApp` (`router/app.ts`) and test suites.
|
|
782
|
+
|
|
783
|
+
##### 5. `unregisterCollections`
|
|
784
|
+
- **Tier**: `Tier 2: Internal Hook` (`core.internals.registry.unregisterCollections`) & `Tier 3: Module Export`
|
|
785
|
+
- **Signature**:
|
|
786
|
+
```ts
|
|
787
|
+
export const unregisterCollections = (names: string[]): void
|
|
788
|
+
```
|
|
789
|
+
- **Contract**: Deletes each name in `names` from `collectionRegistry` except protected `CORE_COLLECTION_NAMES`.
|
|
790
|
+
|
|
791
|
+
##### 6. `getRegisteredCollectionNames`
|
|
792
|
+
- **Tier**: `Tier 3: Module Export`
|
|
793
|
+
- **Signature**:
|
|
794
|
+
```ts
|
|
795
|
+
export const getRegisteredCollectionNames = (): string[]
|
|
796
|
+
```
|
|
797
|
+
- **Contract**: Returns `[...collectionRegistry.keys()]`. Used by `triggerFirestoreBackup` (`auth/users.ts`) for pre-erasure GCS exports.
|
|
798
|
+
|
|
799
|
+
##### 7. `getCollectionDef`
|
|
800
|
+
- **Tier**: `Tier 3: Module Export`
|
|
801
|
+
- **Signature**:
|
|
802
|
+
```ts
|
|
803
|
+
export const getCollectionDef = (name: string): CollectionDefinition<any>
|
|
804
|
+
```
|
|
805
|
+
- **Contract**: Looks up `name` in `collectionRegistry`; throws `Error('Unregistered collection: ${name}')` if missing.
|
|
806
|
+
|
|
807
|
+
##### 8. `getEffectiveRbac`
|
|
808
|
+
- **Tier**: `Tier 3: Module Export`
|
|
809
|
+
- **Signature**:
|
|
810
|
+
```ts
|
|
811
|
+
export const getEffectiveRbac = (collection: string): RbacConfig
|
|
812
|
+
```
|
|
813
|
+
- **Contract**: Merges `CORE_CONFIG.db.rbac` role defaults (`admin`, `user`, `anon`) with per-collection `def.rbac` overrides.
|
|
814
|
+
- **Callers**: `resolveAllowedScope` (`db/access.ts`), `buildRoutesIntrospection` (`tasks/routes.ts`).
|
|
815
|
+
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
### 6.2 [`src/core/db/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/db/service.ts) (`class DbService`)
|
|
819
|
+
|
|
820
|
+
#### Module Initialization & State
|
|
821
|
+
- Initializes `firebase-admin/app` at module load with `projectId: CORE_CONFIG.gcpProject`, `storageBucket: CORE_CONFIG.storageBuckets.prod`, `serviceAccountId: CORE_CONFIG.serviceAccount`, and an HTTPS `Agent({ keepAlive: true })`.
|
|
822
|
+
- **`dbs`** *(Tier 4: File-Private)*: `[Firestore | undefined, Firestore | undefined]` (`[0]` = prod, `[1]` = dev).
|
|
823
|
+
- **`buckets`** *(Tier 4: File-Private)*: `[Bucket | undefined, Bucket | undefined]` (`[0]` = prod, `[1]` = dev).
|
|
824
|
+
- **`cachedToken` / `cachedTokenExp`** *(Tier 4: File-Private)*: Cached GCP ADC access token and expiry timestamp.
|
|
825
|
+
|
|
826
|
+
#### Public & Internal Methods on `DbService` (`readonly #core: Core`)
|
|
827
|
+
|
|
828
|
+
##### 1. `firestore`
|
|
829
|
+
- **Tier**: `Tier 1: Public` (`core.db.firestore`)
|
|
830
|
+
- **Signature**:
|
|
831
|
+
```ts
|
|
832
|
+
firestore = (isDev: boolean = this.#core.env.isDev): Firestore
|
|
833
|
+
```
|
|
834
|
+
- **Contract**: Returns the cached `Firestore` client for `CORE_CONFIG.firestoreDbs.dev` (`isDev === true`) or `CORE_CONFIG.firestoreDbs.prod` (`isDev === false`), configuring `ignoreUndefinedProperties: true` on first initialization.
|
|
835
|
+
|
|
836
|
+
##### 2. `bucket`
|
|
837
|
+
- **Tier**: `Tier 1: Public` (`core.files.bucket`)
|
|
838
|
+
- **Signature**:
|
|
839
|
+
```ts
|
|
840
|
+
bucket = (isDev: boolean = this.#core.env.isDev): StorageBucket
|
|
841
|
+
```
|
|
842
|
+
- **Contract**: Returns the cached Firebase Storage `Bucket` instance for `CORE_CONFIG.storageBuckets.dev` or `CORE_CONFIG.storageBuckets.prod`.
|
|
843
|
+
|
|
844
|
+
##### 3. `getGcpAccessToken`
|
|
845
|
+
- **Tier**: `Tier 3: Internal Service Method`
|
|
846
|
+
- **Signature**:
|
|
847
|
+
```ts
|
|
848
|
+
getGcpAccessToken = async (): Promise<string>
|
|
849
|
+
```
|
|
850
|
+
- **Contract**: Returns `cachedToken` if valid for `> 60s`; otherwise coalesces via `this.#core.resilience.singleflight('gcp:token', ..., { scope: 'global' })` to call `getApp().options.credential?.getAccessToken()`.
|
|
851
|
+
|
|
852
|
+
##### 4. `#resolveRole`, `#hasAction`, `#resolveAllowedScope`, `#normalizeDates`, `#fromFirestoreDoc`, `#isEntryOwner`, `#fetchRawEntry`
|
|
853
|
+
- **Tier**: `Tier 4: Private (#method)`
|
|
854
|
+
- **Contract**: RBAC scope resolution (`'any' | 'own' | 'none'`), recursive Firestore `Timestamp` to `Date` normalization, schema validation, and document ownership evaluation across all 5 relation types (`1:1`, `1:N`, `N:1`, `M:N`, `global`).
|
|
855
|
+
|
|
856
|
+
##### 5. `canAccess`
|
|
857
|
+
- **Tier**: `Tier 1: Public` (`core.db.canAccess`)
|
|
858
|
+
- **Signature**:
|
|
859
|
+
```ts
|
|
860
|
+
canAccess = async <N extends CollectionName>(
|
|
861
|
+
collection: N,
|
|
862
|
+
docOrId: SelectModel<N> | EntryId | Record<string, any> | undefined,
|
|
863
|
+
userId: EntryId = this.#core.userId,
|
|
864
|
+
action: DbAction = 'read',
|
|
865
|
+
userRole: DbRole = userId === this.#core.userId ? this.#core.role : resolveUserRole(userId)
|
|
866
|
+
): Promise<boolean>
|
|
867
|
+
```
|
|
868
|
+
- **Contract**: Evaluates `#resolveAllowedScope(collection, role, action)`. If `'none'`, returns `false`. If `'any'`, verifies existence if `docOrId` is a string ID, else returns `true`. If `'own'`, fetches doc if `docOrId` is a string ID and evaluates `#isEntryOwner(collection, doc, userId)`.
|
|
869
|
+
|
|
870
|
+
##### 6. `create`, `read`, `list`, `listPage`, `update`, `delete`, `hardDelete`, `deleteChatMessagesSubcollection`
|
|
871
|
+
- **Tier**: `Tier 1: Public` (`core.db.*`)
|
|
872
|
+
- **Contract**:
|
|
873
|
+
- `create`: Enforces `'create'` RBAC scope, calls `this.#core.resilience.acquireLimit('firestore')`, validates against `def.schema.parse()`, and writes new doc.
|
|
874
|
+
- `read`: Reads `collection/id`, normalizes dates, and enforces `'read'` RBAC scope.
|
|
875
|
+
- `list` & `listPage`: Queries/batches docs with RBAC auto-scoping, automatic missing-index in-memory sort fallback, and non-enumerable `nextCursor`.
|
|
876
|
+
- `update`: Enforces `canAccess(..., 'write')`, strips immutable/ownership fields, validates partial schema, updates doc, and returns `{ id, updated: true }`.
|
|
877
|
+
- `delete`: Soft-deletes doc (`deleted: true`) after `'delete'` RBAC check and returns `{ id, deleted: true }`.
|
|
878
|
+
- `hardDelete`: Permanently deletes doc, GCS file/chat subcollections, invokes `def.onHardDelete(existing, new Core(this.#core.stateSnapshot))`, and returns `{ id, deleted: true, hard: true }`.
|
|
879
|
+
|
|
880
|
+
##### 7. `cascadeEraseUserCollections`
|
|
881
|
+
- **Tier**: `Tier 2: Internal Hook` (`core.internals.registry.cascadeEraseUserCollections`)
|
|
882
|
+
- **Signature**:
|
|
883
|
+
```ts
|
|
884
|
+
cascadeEraseUserCollections = async (
|
|
885
|
+
userId: EntryId,
|
|
886
|
+
step: StepRunner = this.#core.step
|
|
887
|
+
): Promise<{ erasedFiles: number; erasedChats: number; erasedCollections: Record<string, number> }>
|
|
888
|
+
```
|
|
889
|
+
- **Contract**: Step-checkpointed GDPR cascade erasure across `file`, `chat` (plus orphan `collectionGroup('messages')`), and all registered custom collections.
|
|
890
|
+
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
## 7. Files & Content-Addressed Storage Module (`src/core/files/*`) — Per-Function Spec
|
|
894
|
+
|
|
895
|
+
### 7.1 [`src/core/files/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/files/schema.ts)
|
|
896
|
+
|
|
897
|
+
#### Schemas & Constants
|
|
898
|
+
- **`fileType`**: `z.enum(['application/pdf', 'image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff', 'image/avif'])` (`core.schemas.fileType`)
|
|
899
|
+
- **`MAX_FILE_BYTES`**: `CORE_CONFIG.limits.maxFileBytes` (`20 MiB`)
|
|
900
|
+
- **`fileBody`**: `z.object({ bytes: z.instanceof(Uint8Array).refine(1B..20MiB), name: z.string().min(1).max(256), type: fileType })` (`core.schemas.fileBody`)
|
|
901
|
+
- **`signedUploadInput`**: `z.object({ sha256: z.string().regex(/^[a-fA-F0-9]{24,64}$/), name: z.string().min(1).max(256), type: fileType, size: z.number().int().positive().max(MAX_FILE_BYTES).optional() })` (`core.schemas.signedUploadInput`)
|
|
902
|
+
|
|
903
|
+
#### Functions in `src/core/files/utils.ts`
|
|
904
|
+
|
|
905
|
+
##### 1. `canAccessFile` (`src/core/files/utils.ts`)
|
|
906
|
+
- **Tier**: `Tier 1: Public` (`core.files.canAccess`) & `Tier 4: Domain-Internal` (`src/core/files/utils.ts`)
|
|
907
|
+
- **Signature**:
|
|
908
|
+
```ts
|
|
909
|
+
export const canAccessFile = (userId: string, ref: string, role?: DbRole): boolean
|
|
910
|
+
```
|
|
911
|
+
- **Contract**: $O(1)$ zero-I/O ownership check. Returns `true` if `role === 'admin' || userId === CORE_CONFIG.auth.adminUserId` or (`fileRef.safeParse(ref).success && ref.slice(0, -25) === userId`).
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
### 7.2 [`src/core/files/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/files/service.ts) (`class FilesService`)
|
|
916
|
+
|
|
917
|
+
#### Methods in `src/core/files/service.ts` (`readonly #core: Core`)
|
|
918
|
+
|
|
919
|
+
##### 1. `upload`
|
|
920
|
+
- **Tier**: `Tier 1: Public` (`core.files.upload`, also `POST /api/files`)
|
|
921
|
+
- **Signature**:
|
|
922
|
+
```ts
|
|
923
|
+
upload = async (
|
|
924
|
+
rawFile: FileBody,
|
|
925
|
+
userId: EntryId = this.#core.userId
|
|
926
|
+
): Promise<{ ref: FileRef }>
|
|
927
|
+
```
|
|
928
|
+
- **Contract & Side Effects**:
|
|
929
|
+
- Validates `userId` and `file`, computes `shaSum = sha256(file.bytes).slice(0, 24)`, and constructs content-addressed `ref = "${userId}_${shaSum}"`.
|
|
930
|
+
- Checks `this.#core.db.read('file', ref, true, { bypassRbac: true })` for existing metadata.
|
|
931
|
+
- **`step('upload-storage')`**: If `!existing`, uploads bytes to Firebase Storage (`this.bucket().file(ref).save(...)`) with `rollback` deleting the GCS object if a subsequent step fails.
|
|
932
|
+
- **`step('upload-meta')`**: Creates the `'file'` Firestore entry (or undeletes it if `existing.deleted === true`) with `rollback` hard-deleting or re-marking `deleted: true`.
|
|
933
|
+
|
|
934
|
+
##### 2. `#signGcsUrl`
|
|
935
|
+
- **Tier**: `Tier 4: Private (#method)`
|
|
936
|
+
- **Signature**:
|
|
937
|
+
```ts
|
|
938
|
+
#signGcsUrl = async (
|
|
939
|
+
ref: FileRef,
|
|
940
|
+
action: 'read' | 'write',
|
|
941
|
+
expiresMs: number,
|
|
942
|
+
contentType?: FileType
|
|
943
|
+
): Promise<string>
|
|
944
|
+
```
|
|
945
|
+
- **Contract**: Calls `this.#core.resilience.acquireLimit('storage')` and `bucket.file(ref).getSignedUrl({ version: 'v4', action, expires: expiresMs, contentType })`. Falls back to a deterministic `https://storage.googleapis.com/${bucket.name}/${ref}?...` URL if local ADC lacks signing credentials in dev/test.
|
|
946
|
+
|
|
947
|
+
##### 3. `getSignedUploadUrl`
|
|
948
|
+
- **Tier**: `Tier 1: Public` (`core.files.getSignedUploadUrl`)
|
|
949
|
+
- **Signature**:
|
|
950
|
+
```ts
|
|
951
|
+
getSignedUploadUrl = async (
|
|
952
|
+
rawInput: SignedUploadInput,
|
|
953
|
+
userId: EntryId = this.#core.userId,
|
|
954
|
+
expiresInMs = 15 * 60_000
|
|
955
|
+
): Promise<SignedUploadUrlResult>
|
|
956
|
+
```
|
|
957
|
+
- **Contract**: Computes `ref = "${userId}_${input.sha256.toLowerCase().slice(0, 24)}"`. If an active (non-deleted) `file` entry already exists for `ref`, returns `{ ref, url: '', expiresAt, deduplicated: true }` immediately without generating a GCS upload URL; otherwise signs a V4 `write` URL (`deduplicated: false`).
|
|
958
|
+
|
|
959
|
+
##### 4. `getSignedDownloadUrl`
|
|
960
|
+
- **Tier**: `Tier 1: Public` (`core.files.getSignedDownloadUrl`, also `GET /api/files/:ref?signed=true`)
|
|
961
|
+
- **Signature**:
|
|
962
|
+
```ts
|
|
963
|
+
getSignedDownloadUrl = async (
|
|
964
|
+
ref: FileRef,
|
|
965
|
+
expiresInMs = 15 * 60_000
|
|
966
|
+
): Promise<SignedDownloadUrlResult>
|
|
967
|
+
```
|
|
968
|
+
- **Contract**: Verifies `this.#core.db.read('file', ref, false, { bypassRbac: true })` exists (`404 NOT_FOUND` if missing) and signs a V4 `read` URL via `#signGcsUrl`.
|
|
969
|
+
|
|
970
|
+
##### 5. `download`, `get`, `list`, `delete`, `hardDelete`, `canAccess`, `bucket`
|
|
971
|
+
- **Tier**: `Tier 1: Public` (`core.files.*`)
|
|
972
|
+
- **Contract**:
|
|
973
|
+
- `download(ref)`: Calls `this.#core.resilience.acquireLimit('storage')` and downloads `(await this.bucket().file(ref).download())[0]`.
|
|
974
|
+
- `get(ref)`: Runs `download(ref)` and `this.#core.db.read('file', ref)` in parallel and returns validated `{ bytes, name, type }`.
|
|
975
|
+
- `list(options)`: Delegates to `this.#core.db.list('file', options)`.
|
|
976
|
+
- `delete(ref)`: Soft-deletes via `this.#core.db.delete('file', ref, { bypassRbac: true })`.
|
|
977
|
+
- `hardDelete(ref)`: Wraps `this.#core.db.hardDelete('file', ref, { bypassRbac: true })` inside `step('hard-delete-file')` with a Saga `rollback` that restores the previous Firestore `file` metadata document if the parent workflow fails.
|
|
978
|
+
- `canAccess(ref, userId)`: Delegates to `canAccessFile(userId, ref)`.
|
|
979
|
+
- `bucket(isDev)`: Delegates to `this.#core.db.bucket(isDev)`.
|
|
980
|
+
|
|
981
|
+
---
|
|
982
|
+
|
|
983
|
+
### 7.3 [`src/core/files/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/files/routes.ts)
|
|
984
|
+
|
|
985
|
+
Exports `coreFilesRoutes` (`Tier 3: Module Export`) mounted into `RouterService.createApp`:
|
|
986
|
+
- **`'files.list'`** (`GET /api/files`): Calls `core.db.list('file', { userId })`.
|
|
987
|
+
- **`'files.upload'`** (`POST /api/files`): Parses `multipart/form-data` field `'file'` and calls `core.files.upload(...)`.
|
|
988
|
+
- **`'files.get'`** (`GET /api/files/:ref`): Supports `?meta=true` (returns Firestore `file` doc), `?signed=true` (returns `core.files.getSignedDownloadUrl`), or default binary `Response` (`cache-control: private, max-age=3600, immutable`).
|
|
989
|
+
- **`'files.delete'`** (`DELETE /api/files/:ref`): Calls `core.files.delete(ref)`.
|
|
990
|
+
|
|
991
|
+
---
|
|
992
|
+
|
|
993
|
+
## 8. AI Primitives Module (`src/core/ai/*`) — Per-Function Spec
|
|
994
|
+
|
|
995
|
+
### 8.1 [`src/core/ai/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/ai/schema.ts)
|
|
996
|
+
|
|
997
|
+
Defines `JevQuestion`, `DecideResult`, `VisionOptions`, `VisionResult`, `AiGenerateOptions<O>`, and `AiGenerateResult<O>`.
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
### 8.2 [`src/core/ai/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/ai/service.ts) (`class AiService`) & [`src/core/ai/utils.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/ai/utils.ts)
|
|
1002
|
+
|
|
1003
|
+
Structured probabilistic reasoning via TypeSafe Jev (`systemOne`), Mistral OCR, Vertex Anthropic Vision fallback, Vertex LLM text/object generation, and Vertex `text-embedding-005` embeddings.
|
|
1004
|
+
|
|
1005
|
+
#### Helpers (`src/core/ai/utils.ts`) & Methods on `AiService` (`readonly #core: Core`)
|
|
1006
|
+
- **`formatOcrPages` & `normalizeUsage`** *(Tier 4: Domain-Internal in `src/core/ai/utils.ts`)*: Formats Mistral OCR pages into `### Page N\n...` sections, filtering out `'image'` blocks and blocks with `averageContentConfidenceScore < 0.5`.
|
|
1007
|
+
- **`languageModel(modelId?)`** (`Tier 1: core.ai.languageModel`): Returns a cached Vertex Anthropic `LanguageModel` instance.
|
|
1008
|
+
- **`decide(state, questions)`** (`Tier 1: core.ai.decide`): Executes inside `this.#core.internals.resilience.circuitBreaker('jev', ...)`, acquires `'jev'` limit, calls `TypeSafeClient.systemOne({ state, questions })`, and validates discriminated union responses.
|
|
1009
|
+
- **`vision(documentUrl, opts?)`** (`Tier 1: core.ai.vision`): Executes inside `this.#core.internals.resilience.circuitBreaker('mistral', primaryFn, fallbackFn)`, acquires `'mistral'` limit, calls `Mistral.ocr.process(..., opts?.signal ? { fetchOptions: { signal: opts.signal } } : undefined)` with automatic `#vertexVisionFallback` and `describeImage` fallback.
|
|
1010
|
+
- **`generate(promptOrMessages, opts?)`** (`Tier 1: core.ai.generate`): Executes inside `this.#core.internals.resilience.circuitBreaker('vertex', ...)`, acquires `'ai'` limit, and calls `generateText` on `this.languageModel(model)` with optional `Output.object({ schema })`.
|
|
1011
|
+
- **`embed(text)`** (`Tier 1: core.ai.embed`): Returns `null` if `text.trim().length < 15`; otherwise executes inside `this.#core.internals.resilience.circuitBreaker('vertex', ...)`, acquires `'ai'` limit, and calls `embed()` on Vertex `text-embedding-005` (`768` dimensions, `AbortSignal.timeout(15_000)`).
|
|
1012
|
+
|
|
1013
|
+
---
|
|
1014
|
+
|
|
1015
|
+
## 9. Attachments Scanning & Batch Fan-Out Module (`src/core/attachments/*`) — Per-Function Spec
|
|
1016
|
+
|
|
1017
|
+
### 9.1 [`src/core/attachments/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/attachments/schema.ts)
|
|
1018
|
+
|
|
1019
|
+
#### Functions in `src/core/attachments/schema.ts` & `src/core/attachments/utils.ts`
|
|
1020
|
+
|
|
1021
|
+
##### 1. `defineAttachmentTypes`
|
|
1022
|
+
- **Tier**: `Tier 1: Public` (`core.define.attachmentTypes`) & `Tier 3: Module Export`
|
|
1023
|
+
- **Signature**:
|
|
1024
|
+
```ts
|
|
1025
|
+
export const defineAttachmentTypes = <T extends AttachmentTypesMap>(types: T): T => types
|
|
1026
|
+
```
|
|
1027
|
+
- **Contract**: Type-inference identity helper for declaring `Record<string, { description: string; schema: z.ZodType }>`.
|
|
1028
|
+
|
|
1029
|
+
##### 2. `registerAttachmentTypes`
|
|
1030
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1031
|
+
- **Signature**:
|
|
1032
|
+
```ts
|
|
1033
|
+
export const registerAttachmentTypes = (types?: AttachmentTypesMap, reset = false): void
|
|
1034
|
+
```
|
|
1035
|
+
- **Contract**: Optionally clears `registeredAttachmentTypes` when `reset === true` and merges `types` into `registeredAttachmentTypes`. Called by `createCoreApp` and `registerAgents`.
|
|
1036
|
+
|
|
1037
|
+
##### 3. `getRegisteredAttachmentTypes`
|
|
1038
|
+
- **Tier**: `Tier 2: Internal Hook` (`core.internals.registry.getAttachmentTypes`) & `Tier 3: Module Export`
|
|
1039
|
+
- **Signature**:
|
|
1040
|
+
```ts
|
|
1041
|
+
export const getRegisteredAttachmentTypes = (): AttachmentTypesMap
|
|
1042
|
+
```
|
|
1043
|
+
- **Contract**: Returns the live `registeredAttachmentTypes` dictionary.
|
|
1044
|
+
|
|
1045
|
+
##### 4. `schemaFor`
|
|
1046
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1047
|
+
- **Signature**:
|
|
1048
|
+
```ts
|
|
1049
|
+
export const schemaFor = (
|
|
1050
|
+
type: string,
|
|
1051
|
+
types: AttachmentTypesMap = registeredAttachmentTypes
|
|
1052
|
+
): Record<string, unknown> | undefined
|
|
1053
|
+
```
|
|
1054
|
+
- **Contract**: Converts `types[type].schema` into a JSON Schema object via `z.toJSONSchema(types[type]!.schema, { io: 'output' })` for Mistral OCR / Vertex Vision structured extraction.
|
|
1055
|
+
|
|
1056
|
+
##### 5. `parseFields`
|
|
1057
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1058
|
+
- **Signature**:
|
|
1059
|
+
```ts
|
|
1060
|
+
export const parseFields = (
|
|
1061
|
+
type: string,
|
|
1062
|
+
fields: Record<string, unknown>,
|
|
1063
|
+
types: AttachmentTypesMap = registeredAttachmentTypes
|
|
1064
|
+
): Record<string, unknown>
|
|
1065
|
+
```
|
|
1066
|
+
- **Contract**: Validates and coerces extracted `fields` through `types[type]!.schema.parse(fields)` (returns `{}` if `type` is not in `types`).
|
|
1067
|
+
|
|
1068
|
+
##### 6. `extractAttachmentTasks`
|
|
1069
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1070
|
+
- **Signature**:
|
|
1071
|
+
```ts
|
|
1072
|
+
export const extractAttachmentTasks = (input: ProcessAttachmentsInput): ScanAttachmentInput[]
|
|
1073
|
+
```
|
|
1074
|
+
- **Contract**: Parses `input` via `processAttachmentsInput` (`1..500` items) and normalizes bare `FileRef` strings into `{ ref }`.
|
|
1075
|
+
|
|
1076
|
+
---
|
|
1077
|
+
|
|
1078
|
+
### 9.2 [`src/core/attachments/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/attachments/service.ts) (`class AttachmentsService`)
|
|
1079
|
+
|
|
1080
|
+
#### Methods in `src/core/attachments/service.ts` (`readonly #core: Core`)
|
|
1081
|
+
|
|
1082
|
+
##### 1. `#buildScanError` & 2. `#extractHttpStatus`
|
|
1083
|
+
- **Tier**: `Tier 4: Private (#method)`
|
|
1084
|
+
- **Contract**: Constructs a structured `FileScanError` (`status: 'unparseable'`, `reason`, `agentHint`, `diagnosticMessage` truncated to 500 chars, `failedAt`) and extracts HTTP status codes from error objects.
|
|
1085
|
+
|
|
1086
|
+
##### 3. `classifyPermanentScanDefect`
|
|
1087
|
+
- **Tier**: `Tier 3: Internal Service Method`
|
|
1088
|
+
- **Signature**:
|
|
1089
|
+
```ts
|
|
1090
|
+
classifyPermanentScanDefect = (err: unknown): FileScanError | null
|
|
1091
|
+
```
|
|
1092
|
+
- **Contract**: Distinguishes transient upstream failures (`408`, `429`, `5xx`, timeouts, socket resets — returns `null` so retries/hedging proceed) from permanent document defects:
|
|
1093
|
+
- `'PASSWORD_PROTECTED'` (`password` / `encrypted`)
|
|
1094
|
+
- `'UNSUPPORTED_FORMAT'` (`415` / `unsupported media`)
|
|
1095
|
+
- `'EMPTY_OR_UNREADABLE'` (`empty description` / `no readable text`)
|
|
1096
|
+
- `'CLASSIFICATION_REJECTED'` (`classification_rejected`)
|
|
1097
|
+
- `'CORRUPTED_FILE'` (`400` / `422` / `corrupt` / `malformed` / `invalid pdf` / `invalid image`)
|
|
1098
|
+
|
|
1099
|
+
##### 4. `#wrapScanStep` & 5. `#findCrossUserByShaSum`
|
|
1100
|
+
- **Tier**: `Tier 4: Private (#method)`
|
|
1101
|
+
- **Contract**: `#wrapScanStep` converts permanent defects into `NonRetryableError`; `#findCrossUserByShaSum` queries `file` (`where('shaSum', '==', shaSum).limit(5)`) to reuse already-validated `scan` or `scanError` across users.
|
|
1102
|
+
|
|
1103
|
+
##### 6. `classify`
|
|
1104
|
+
- **Tier**: `Tier 1: Public` (`core.attachments.classify`)
|
|
1105
|
+
- **Signature**:
|
|
1106
|
+
```ts
|
|
1107
|
+
classify = async (
|
|
1108
|
+
attachmentContent: string,
|
|
1109
|
+
types: AttachmentTypesMap = getRegisteredAttachmentTypes()
|
|
1110
|
+
): Promise<{ attachmentType: string; confidence: number }>
|
|
1111
|
+
```
|
|
1112
|
+
- **Contract**: Sends up to `80,000` characters of `attachmentContent` to `this.#core.ai.decide` with a `'choice'` question across all registered `types` + `'other'`.
|
|
1113
|
+
|
|
1114
|
+
##### 7. `scan`
|
|
1115
|
+
- **Tier**: `Tier 1: Public` (`core.attachments.scan`, also `POST /api/scan-attachment`)
|
|
1116
|
+
- **Signature**:
|
|
1117
|
+
```ts
|
|
1118
|
+
scan = async (
|
|
1119
|
+
raw: ScanAttachmentInput,
|
|
1120
|
+
opts?: { types?: AttachmentTypesMap; step?: StepRunner; preloadedFile?: SelectModel<'file'> }
|
|
1121
|
+
): Promise<ScanAttachmentOutput>
|
|
1122
|
+
```
|
|
1123
|
+
- **Contract & Step Pipeline**:
|
|
1124
|
+
- Coalesced via `this.#core.resilience.singleflight('scan:${env}:${ref}:${type}')`.
|
|
1125
|
+
- **`step('read-meta')`**: Reads `file` doc (unless `preloadedFile` was passed by `process`). Returns cached `entry.scan` or throws `NonRetryableError` if `entry.scanError` exists.
|
|
1126
|
+
- Checks `#findCrossUserByShaSum(ref, shaSum, type)` for cross-user SHA-256 cache hits.
|
|
1127
|
+
- **`getDataUrl`** *(inner lazy closure)*: Downloads file bytes from GCS once and encodes as `data:${contentType};base64,...`.
|
|
1128
|
+
- **`step('ocr')`**: Calls `this.#core.ai.vision(await getDataUrl(), { fallbackVision: breaker.state === 'OPEN', signal })`.
|
|
1129
|
+
- **`step('vision')`**: Triggered if OCR returned empty markdown, or if an image was classified as `'other'`.
|
|
1130
|
+
- **`step('classify')`**: Calls `this.classify(markdown, types)` when `type` was not pre-specified.
|
|
1131
|
+
- **`step('extract')`**: If `schemaFor(resolvedType, types)` exists, calls `this.#core.ai.vision(..., { schema })` and validates via `parseFields()`.
|
|
1132
|
+
- Rejects documents with `< 5` chars and `0` extracted fields as `'EMPTY_OR_UNREADABLE'`.
|
|
1133
|
+
- **`step('persist')`**: Writes `{ scan, scanError: null }` to `file/{ref}`. On permanent defect, persists `{ scanError: defect, scan: null }` to `file/{ref}` and throws `NonRetryableError`.
|
|
1134
|
+
|
|
1135
|
+
##### 8. `scanShortCircuit` & 9. `#scanAttachmentTask`
|
|
1136
|
+
- **Tier**: `Tier 3: Internal Service Method` / `Tier 4: Private (#method)`
|
|
1137
|
+
- **Contract**: `scanShortCircuit` reads `file/{ref}` before enqueuing a Cloud Task and returns cached `scan` in ~15ms; `#scanAttachmentTask` wraps `scan` for `process` fan-out.
|
|
1138
|
+
|
|
1139
|
+
##### 10. `process`
|
|
1140
|
+
- **Tier**: `Tier 1: Public` (`core.attachments.process`, also `POST /api/process-attachments`)
|
|
1141
|
+
- **Signature**:
|
|
1142
|
+
```ts
|
|
1143
|
+
process = async (
|
|
1144
|
+
raw: ProcessAttachmentsInput,
|
|
1145
|
+
opts?: { types?: AttachmentTypesMap; step?: StepRunner }
|
|
1146
|
+
): Promise<ProcessAttachmentsOutput>
|
|
1147
|
+
```
|
|
1148
|
+
- **Contract & Side Effects**:
|
|
1149
|
+
- Parses `tasks = extractAttachmentTasks(raw)` and batch-fetches all unique `file` docs in a single `this.#core.db.list('file', { ids: uniqueRefs, bypassRbac: true })` call.
|
|
1150
|
+
- Executes `step.map('process-attachments', tasks, ...)` with per-item `shortCircuit` (resolving cached `fileDoc.scan` or `fileDoc.scanError` immediately) and `scanDeadlineSeconds` (`120s`) timeout per uncached item.
|
|
1151
|
+
|
|
1152
|
+
---
|
|
1153
|
+
|
|
1154
|
+
### 9.3 [`src/core/attachments/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/attachments/routes.ts)
|
|
1155
|
+
|
|
1156
|
+
Exports `coreAttachmentsRoutes` (`Tier 3: Module Export`):
|
|
1157
|
+
- **`'attachments.scan'`** (`POST /api/scan-attachment`, `mode: 'workflow'`): Uses `core.attachments.scanShortCircuit` and `core.attachments.scan`.
|
|
1158
|
+
- **`'attachments.process'`** (`POST /api/process-attachments`, `mode: 'workflow'`): Calls `core.attachments.process(input, { step })`.
|
|
1159
|
+
|
|
1160
|
+
---
|
|
1161
|
+
|
|
1162
|
+
## 10. Authentication, OIDC Social & Account Recovery Module (`src/core/auth/*`) — Per-Function Spec
|
|
1163
|
+
|
|
1164
|
+
### 10.1 [`src/core/auth/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/auth/schema.ts)
|
|
1165
|
+
|
|
1166
|
+
#### Constants & Schemas
|
|
1167
|
+
- **`SESSION_COOKIE`**: `CORE_CONFIG.auth.sessionCookie` (`'session'`)
|
|
1168
|
+
- **`SESSION_EXPIRES_IN`**: `CORE_CONFIG.auth.sessionExpiresInMs` (`14 days`)
|
|
1169
|
+
- **`ADMIN_USER_ID`**: `CORE_CONFIG.auth.adminUserId` (`'admin'`)
|
|
1170
|
+
- **Zod Schemas**: `eraseUserInput`, `eraseUserOutput`, `customTokenBody`, `sessionBody`, `emailSignUpBody`, `emailSignInBody`, `socialSignInInput`, `sendVerificationEmailBody`, `verifyEmailBody`, `forgotPasswordBody`, `resetPasswordBody`, `setRecoveryEmailBody`, `verifyRecoveryEmailBody`, `initiateAccountRecoveryBody`, `completeAccountRecoveryBody`, `blockUserBody`, `signOutBody`.
|
|
1171
|
+
- **`resolveUserRole`**: Returns `explicitRole` if it is `'admin' | 'user' | 'anon'`; otherwise returns `'admin'` when `userId === ADMIN_USER_ID`, `'user'` when `userId` is non-empty, and `'anon'` when `userId` is empty/undefined.
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
### 10.2 [`src/core/auth/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/auth/service.ts) (`class AuthService`) & Sub-Engines (`session.ts`, `oidc.ts`, `recovery.ts`, `utils.ts`)
|
|
1176
|
+
|
|
1177
|
+
Session cookies, in-memory token caching (`auth/session.ts`), cross-instance Firestore revocation watermarks (`_system/auth_revocations` in `auth/session.ts`), OIDC JWK verification for Google & Apple Sign-In (`auth/oidc.ts`), IdentityToolkit email/password flows, OTP/magic-token lifecycle (`auth_tokens` in `auth/recovery.ts`), account recovery, and GDPR user erasure (`auth/service.ts`).
|
|
1178
|
+
|
|
1179
|
+
#### Module-Level Warmup & State
|
|
1180
|
+
- **`authWarm`** *(Tier 3: Module Export)*: Pre-fetches Firebase Auth ID token & session cookie public keys at cold start.
|
|
1181
|
+
- **`tokenCache`** *(Tier 2: `core.internals.auth.tokenCache`)*: `new Map<string, CachedTokenEntry>()`
|
|
1182
|
+
- **`revocationWatermarks`** *(Tier 4: File-Private)*: `[RevocationWatermarkState, RevocationWatermarkState]` for prod (`[0]`) and dev (`[1]`).
|
|
1183
|
+
- **`jwksCache`** *(Tier 4: File-Private)*: `new Map<OAuthProvider, JwksCacheEntry>()` caching Google and Apple public keys for 1 hour.
|
|
1184
|
+
- **`devEmailOutbox`** *(Tier 4: File-Private)*: `new Map<string, TransactionalEmailPayload>()`
|
|
1185
|
+
|
|
1186
|
+
#### Public & Internal Methods on `AuthService` (`readonly #core: Core`)
|
|
1187
|
+
1. **Token Cache & Revocation Watermarks**:
|
|
1188
|
+
- `setTokenCache(token, uid, role?, exp?, strictVerified?, emailVerified?)` (`Tier 2: core.internals.auth.setTokenCache`)
|
|
1189
|
+
- `#evictUserTokens(uid, revokedAt?)` (`Tier 4: Private`)
|
|
1190
|
+
- `recordRevocation(uid, isDev?)` (`Tier 2: core.internals.auth.recordRevocation`)
|
|
1191
|
+
- `#clearUserRevocation(uid, isDev?)` (`Tier 4: Private`)
|
|
1192
|
+
- `syncRevocations(isDev?, force?)` (`Tier 2: core.internals.auth.syncRevocations`)
|
|
1193
|
+
- `verifyToken(token, isCookie, checkRevoked, isDev?)` (`Tier 2: core.internals.auth.verifyToken`)
|
|
1194
|
+
2. **Sessions, Email Auth & OIDC Social Login**:
|
|
1195
|
+
- `setSessionCookie(c, sessionCookie)` (`Tier 3: Internal Service Method`)
|
|
1196
|
+
- `mintCustomToken(uid?)` (`POST /api/auth/custom-token`)
|
|
1197
|
+
- `createSession(idToken, profile?)` (`POST /api/auth/session`)
|
|
1198
|
+
- `signUpEmail(email, password, name?)` & `signInEmail(email, password)` (`POST /api/auth/sign-up/email`, `POST /api/auth/sign-in/email`)
|
|
1199
|
+
- `registerTestOidcPublicKey(jwk)` (`Tier 2: core.internals.auth.registerTestOidcPublicKey`)
|
|
1200
|
+
- `signInSocial(raw)` (`Tier 1: core.auth.signInSocial`, `POST /api/auth/sign-in/social`)
|
|
1201
|
+
- `revokeSession(sessionCookie, deleteUser?)` (`POST /api/auth/sign-out`)
|
|
1202
|
+
3. **Transactional OTPs, Verification, Password Reset & Account Recovery**:
|
|
1203
|
+
- `getLastDevEmail(to, purpose)` (`Tier 2: core.internals.auth.getLastDevEmail`)
|
|
1204
|
+
- `hashToken(raw)` (`Tier 2: core.internals.auth.hashToken`)
|
|
1205
|
+
- `sendVerificationEmail(userId?)` & `verifyEmail(raw)` (`Tier 1: core.auth.*`)
|
|
1206
|
+
- `forgotPassword(raw)` & `resetPassword(raw)` (`Tier 1: core.auth.*`)
|
|
1207
|
+
- `setRecoveryEmail(raw, userId?)` & `verifyRecoveryEmail(raw, userId?)` (`Tier 1: core.auth.*`)
|
|
1208
|
+
- `initiateAccountRecovery(raw)` & `completeAccountRecovery(raw)` (`Tier 1: core.auth.*`)
|
|
1209
|
+
4. **User Administration & GDPR Erasure**:
|
|
1210
|
+
- `getUser(uid)` (`Tier 1: core.auth.getUser`)
|
|
1211
|
+
- `listUsers(includeDeleted?)` (`Tier 1: core.auth.listUsers`)
|
|
1212
|
+
- `setClaims(uid, claims)` (`Tier 1: core.auth.setClaims`)
|
|
1213
|
+
- `blockUser(userId?, blocked?)` (`Tier 1: core.auth.blockUser`)
|
|
1214
|
+
- `eraseUser(raw, step?)` (`Tier 1: core.auth.eraseUser`, `POST /api/erase-user` workflow): Runs `step('backup')`, `this.#core.db.cascadeEraseUserCollections(userId, step)`, `step('erase-tasks')`, and `step('erase-user')`.
|
|
1215
|
+
|
|
1216
|
+
---
|
|
1217
|
+
|
|
1218
|
+
### 10.3 [`src/core/auth/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/auth/routes.ts)
|
|
1219
|
+
|
|
1220
|
+
- **`respondWithSession`** *(Tier 4: File-Private)*: Awaits `{ user, sessionCookie }`, calls `core.auth.setSessionCookie(c, sessionCookie)`, and returns `user`.
|
|
1221
|
+
- **`coreAuthRoutes`** *(Tier 3: Module Export)*: Defines the 18 authentication route handlers listed in [Section 3.3](#33-master-built-in-http-routes-core_route_defaults--38-endpoints).
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
---
|
|
1225
|
+
|
|
1226
|
+
## 11. Chat, Agents, InfoBase Context, MCP & OpenAI Module (`src/core/chat/*`) — Per-Function Spec
|
|
1227
|
+
|
|
1228
|
+
### 11.1 [`src/core/chat/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/chat/schema.ts)
|
|
1229
|
+
|
|
1230
|
+
#### Schemas & Types
|
|
1231
|
+
- **Types**: `VectorValue`, `SystemPromptBuilder`, `AgentToolPresets`, `UserContext`, `UserContextResolver`, `AgentConfig`, `ChatMetadataDoc`, `ChatMessageRole`, `ChatMessageDoc`, `ChatMessagePublic`, `RecalledMemorySnippet`, `ContextItemSpec`, `ContextEntry`, `RawAttachmentPart`, `StreamChatMessageInput`.
|
|
1232
|
+
- **Zod Schemas**: `chatIdParam`, `listChatMessagesQuery`, `saveChatBody`, `streamChatMessageInput`, `streamChatBody` (`core.schemas.streamChatBody`), `openaiMessagePartSchema`, `openaiMessageSchema`, `openaiCompletionsBody` (`core.schemas.openaiCompletionsBody`), `webSearchInput`.
|
|
1233
|
+
|
|
1234
|
+
#### Functions in `src/core/chat/utils.ts`
|
|
1235
|
+
|
|
1236
|
+
##### 1. `formatAttachmentLine`
|
|
1237
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1238
|
+
- **Signature**:
|
|
1239
|
+
```ts
|
|
1240
|
+
export const formatAttachmentLine = (filename: string, item: ProcessedAttachment): string
|
|
1241
|
+
```
|
|
1242
|
+
- **Contract**: Formats a scanned attachment into `[attached <filename> (ref: <ref>) (type: <type>) (fields: <json>) (markdown: <md>)]\n` (or `(error: <err>)`) for injection into `<staged_attachments>`.
|
|
1243
|
+
|
|
1244
|
+
##### 2. `isSafePublicUrl`
|
|
1245
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1246
|
+
- **Signature**:
|
|
1247
|
+
```ts
|
|
1248
|
+
export const isSafePublicUrl = (raw?: string): boolean
|
|
1249
|
+
```
|
|
1250
|
+
- **Contract**: SSRF protection predicate for `web_search`. Allows only `http:` / `https:` URLs and blocks numeric/hex IPs, `localhost`, loopback (`127.*`, `::1`), RFC1918/CGNAT (`10.*`, `100.64/10`, `169.254.*`, `172.16/12`, `192.168.*`), cloud metadata (`metadata.*`), IPv6 link-local/ULA, and `.internal` / `.local` / `.nip.io` / `.sslip.io` domains.
|
|
1251
|
+
|
|
1252
|
+
##### 3. `extractPart`
|
|
1253
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1254
|
+
- **Signature**:
|
|
1255
|
+
```ts
|
|
1256
|
+
export const extractPart = (p: Record<string, unknown>): RawAttachmentPart | undefined
|
|
1257
|
+
```
|
|
1258
|
+
- **Contract**: Extracts a `data:` URL or `/api/files/<FileRef>` reference and `filename` from a UI message part object.
|
|
1259
|
+
|
|
1260
|
+
##### 4. `extractMessageText`
|
|
1261
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1262
|
+
- **Signature**:
|
|
1263
|
+
```ts
|
|
1264
|
+
export const extractMessageText = (msg: { content?: unknown; parts?: readonly unknown[] }): string
|
|
1265
|
+
```
|
|
1266
|
+
- **Contract**: Extracts plain text from either string `msg.content` or `{ type: 'text', text }[]` arrays in `msg.content` / `msg.parts`.
|
|
1267
|
+
|
|
1268
|
+
##### 5. `deriveTitle`
|
|
1269
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1270
|
+
- **Signature**:
|
|
1271
|
+
```ts
|
|
1272
|
+
export const deriveTitle = (
|
|
1273
|
+
msgOrList: { content?: unknown; parts?: readonly unknown[]; role?: string } | readonly Record<string, unknown>[]
|
|
1274
|
+
): string
|
|
1275
|
+
```
|
|
1276
|
+
- **Contract**: Derives a chat title (`<= 80` chars) from the first user message's text, defaulting to `'New Chat'`.
|
|
1277
|
+
|
|
1278
|
+
##### 6. `extractLatestUserPrompt`
|
|
1279
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1280
|
+
- **Signature**:
|
|
1281
|
+
```ts
|
|
1282
|
+
export const extractLatestUserPrompt = (
|
|
1283
|
+
messages: readonly (Record<string, unknown> | ModelMessage)[]
|
|
1284
|
+
): string
|
|
1285
|
+
```
|
|
1286
|
+
- **Contract**: Scans `messages` in reverse order and returns the text of the most recent `'user'` message.
|
|
1287
|
+
|
|
1288
|
+
##### 7. `formatUsage`
|
|
1289
|
+
- **Tier**: `Tier 3: Module Export`
|
|
1290
|
+
- **Signature**:
|
|
1291
|
+
```ts
|
|
1292
|
+
export const formatUsage = (usage: LanguageModelUsage | null): {
|
|
1293
|
+
prompt_tokens: number
|
|
1294
|
+
completion_tokens: number
|
|
1295
|
+
total_tokens: number
|
|
1296
|
+
}
|
|
1297
|
+
```
|
|
1298
|
+
- **Contract**: Converts AI SDK `LanguageModelUsage` into OpenAI-compatible token usage counters.
|
|
1299
|
+
|
|
1300
|
+
---
|
|
1301
|
+
|
|
1302
|
+
### 11.2 [`src/core/chat/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/chat/service.ts) (`class ChatService`) & Sub-Engines (`context.ts`, `stream.ts`, `utils.ts`)
|
|
1303
|
+
|
|
1304
|
+
#### Module Constants & Registries
|
|
1305
|
+
- **`FORBIDDEN_TOOL_SCHEMA_KEYS`** *(Tier 4: File-Private)*: `new Set(['userId', 'chatId', 'role', 'userRole', 'token', 'idToken', 'apiKey', 'session'])` — Stripped from both LLM tool JSON Schemas and runtime LLM tool arguments to prevent prompt-injected privilege/tenant spoofing.
|
|
1306
|
+
- **`toolRegistry`** *(Tier 4: File-Private)*: `new Map<string, WorkflowToolSpec>()`
|
|
1307
|
+
- **`agentRegistry`** *(Tier 4: File-Private)*: `new Map<string, AgentConfig>()`
|
|
1308
|
+
- **`contextCatalog` & `markdownCache`** *(Tier 4: File-Private)*: `readonly ContextItemSpec[]` and `Map<string, string>()`
|
|
1309
|
+
- **`activeGenerations`** *(Tier 4: File-Private)*: `new Map<string, ActiveGenerationEntry>()`
|
|
1310
|
+
|
|
1311
|
+
#### Public & Internal Methods on `ChatService` (`readonly #core: Core`)
|
|
1312
|
+
|
|
1313
|
+
##### 1. Tool & Agent Registries (`registerWorkflowTool`, `clearWorkflowTools`, `getWorkflowTools`, `getMcpTools`, `registerAgents`, `getRegisteredAgents`, `registerContextCatalog`, `getRegisteredContextCatalog`)
|
|
1314
|
+
- **Tier**: `Tier 3: Internal Service Method` (`getMcpTools` is also `Tier 2: core.internals.context.getMcpTools`)
|
|
1315
|
+
- **Contract**: Manages `toolRegistry` (sanitizing input schemas via `#sanitizeToolInputSchema` and filtering `mcp === true` for `getMcpTools()`), `agentRegistry` (auto-registering agent `attachmentTypes`), and `contextCatalog`.
|
|
1316
|
+
|
|
1317
|
+
##### 2. Built-In AI Tools & Tool Builder (`#webSearchTool`, `#createReasoningExpertTool`, `#buildAgentToolsForTurn`, `#resolveStepActiveTools`)
|
|
1318
|
+
- **Tier**: `Tier 4: Private (#method)`
|
|
1319
|
+
- **Contract**:
|
|
1320
|
+
- `#webSearchTool`: Validates `url` with `isSafePublicUrl(url)` (falling back to DuckDuckGo HTML search if `url` is absent/unsafe) and streams up to `6,000` chars (URL) or `4,000` chars (search) via Jina Reader (`https://r.jina.ai/${target}`).
|
|
1321
|
+
- `#createReasoningExpertTool`: Delegates complex multi-constraint reasoning problems to `ctx.core.ai.generate(..., { schema: ReasoningExpertOutputDto, model: CORE_CONFIG.models.vertexExpert ?? 'claude-sonnet-4-5', signal: ctx.abortSignal })`.
|
|
1322
|
+
- `#buildAgentToolsForTurn`: Resolves `toolKeys` into AI SDK `ToolSet` entries, wrapping every workflow tool execution inside `runWithCore(coreState, ...)` and stripping `FORBIDDEN_TOOL_SCHEMA_KEYS`.
|
|
1323
|
+
- `#resolveStepActiveTools`: Implements phased tool exposure via `agent.toolPresets` (`stepNumber === 0` exposes `discovery` + `workflow`; `stepNumber > 0` narrows to `workflow`).
|
|
1324
|
+
|
|
1325
|
+
##### 3. `getContext` & `retrieveContext`
|
|
1326
|
+
- **Tier**: `Tier 1: Public` (`core.chat.getContext`, `core.chat.retrieveContext`)
|
|
1327
|
+
- **Signatures**:
|
|
1328
|
+
```ts
|
|
1329
|
+
getContext = async (
|
|
1330
|
+
prompt: string,
|
|
1331
|
+
catalog: readonly ContextItemSpec[] = contextCatalog,
|
|
1332
|
+
options?: GetContextOptions
|
|
1333
|
+
): Promise<ContextEntry[]>
|
|
1334
|
+
retrieveContext = async (
|
|
1335
|
+
opts: Omit<HybridRetrievalOptions, 'userId'> & { userId?: EntryId }
|
|
1336
|
+
): Promise<HybridRetrievalResult>
|
|
1337
|
+
```
|
|
1338
|
+
- **Contract**:
|
|
1339
|
+
- `getContext`: Filters `catalog` by `audiences`, scores relevance via `this.#core.ai.decide` (`'noul' >= 0.5`), and loads matched `.md` files via `#readContextMarkdown`.
|
|
1340
|
+
- `retrieveContext`: Runs 4 branches in parallel: 1) `#resolveTurnUserContext` (`agent.resolveUserContext` bounded by `3.5s`), 2) sliding window (last `15` messages), 3) `this.#core.ai.embed` + Firestore `findNearest` (`COSINE <= 0.35`, top 5 in-chat + top 3 cross-chat `collectionGroup('messages')`), and 4) `getContext`.
|
|
1341
|
+
|
|
1342
|
+
##### 4. `stream` & `ask`
|
|
1343
|
+
- **Tier**: `Tier 1: Public` (`core.chat.stream`, `core.chat.ask`)
|
|
1344
|
+
- **Signatures**:
|
|
1345
|
+
```ts
|
|
1346
|
+
stream = async (
|
|
1347
|
+
agent: AgentConfig,
|
|
1348
|
+
messages: ModelMessage[],
|
|
1349
|
+
userId: EntryId = this.#core.userId,
|
|
1350
|
+
opts?: RunAgentOptions
|
|
1351
|
+
)
|
|
1352
|
+
ask = async (
|
|
1353
|
+
agentOrId: string | AgentConfig,
|
|
1354
|
+
prompt: string | ModelMessage[],
|
|
1355
|
+
options?: AskAgentOptions
|
|
1356
|
+
): Promise<{ text: string; messages: ModelMessage[] }>
|
|
1357
|
+
```
|
|
1358
|
+
- **Contract**: `stream` builds static (ephemeral cached) and dynamic (`#buildDynamicContextBlock`) system messages and calls AI SDK `streamText`. `ask` runs `retrieveContext` + `stream` and resolves `{ text, messages }`.
|
|
1359
|
+
|
|
1360
|
+
##### 5. `messages` & `cancel`
|
|
1361
|
+
- **Tier**: `Tier 1: Public` (`core.chat.messages`, `core.chat.cancel`)
|
|
1362
|
+
- **Signatures**:
|
|
1363
|
+
```ts
|
|
1364
|
+
messages = async (chatId: EntryId, opts?: ListChatMessagesOptions): Promise<ChatMessagePublic[]>
|
|
1365
|
+
cancel = async (chatId: EntryId): Promise<{ chatId: string; cancelled: true }>
|
|
1366
|
+
```
|
|
1367
|
+
- **Contract**: `messages` verifies chat ownership and lists paginated messages from `chat/{chatId}/messages` in chronological order. `cancel` sets `activeGenerationId: null` on `chat/{chatId}`, aborts local `activeGenerations` controller, and awaits `completionPromise` (up to `2.5s`) so partial assistant text is committed.
|
|
1368
|
+
|
|
1369
|
+
##### 6. Route Backing Methods (`saveChatRoute`, `listMessagesRoute`, `cancelChatRoute`, `streamChatRoute`, `mcpRoute`, `listOpenaiModelsRoute`, `openaiCompletionsRoute`)
|
|
1370
|
+
- **Tier**: `Tier 3: Internal Service Method`
|
|
1371
|
+
- **Contract**: Backs `/api/chats*`, `/api/mcp`, `/api/v1/models`, and `/api/v1/chat/completions`.
|
|
1372
|
+
|
|
1373
|
+
---
|
|
1374
|
+
|
|
1375
|
+
### 11.3 [`src/core/chat/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/chat/routes.ts)
|
|
1376
|
+
|
|
1377
|
+
Exports `coreChatRoutes` (`Tier 3: Module Export`) binding the 9 chat and OpenAI routes (`chat.list`, `chat.get`, `chat.messages`, `chat.save`, `chat.delete`, `chat.stream`, `chat.cancel`, `openai.models`, `openai.completions`) to `core.db.*` and `core.chat.*`.
|
|
1378
|
+
|
|
1379
|
+
---
|
|
1380
|
+
|
|
1381
|
+
## 12. Durable Workflows, StepRunner, Cloud Tasks & Watcher Module (`src/core/tasks/*`) — Per-Function Spec
|
|
1382
|
+
|
|
1383
|
+
### 12.1 [`src/core/tasks/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/tasks/schema.ts)
|
|
1384
|
+
|
|
1385
|
+
Defines all workflow types and interfaces: `TaskStatus` (`'running' | 'sleeping' | 'waiting_signal' | 'compensating' | 'completed' | 'errored' | 'cancelled'`), `TaskExecutionFence` (`epoch`, `activeRunId`, `lastHeartbeatAt`), `TaskProgressUpdate`, `TaskStatusRecord<O>`, `WorkflowContext`, `StepRetryPolicy`, `StepContext`, `StepOptions<T>`, `StepMapOptions<T, R>`, `StepSpec<T>`, `StepFn`, `WorkflowTargetRef<I, O>`, `StepRunner`, `RollbackLeg<T>`, `WorkflowTaskPayload`, and `WorkflowRuntimeEntry`.
|
|
1386
|
+
|
|
1387
|
+
---
|
|
1388
|
+
|
|
1389
|
+
### 12.2 [`src/core/tasks/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/tasks/service.ts) (`class TasksService`) & Sub-Engines (`steps.ts`, `watcher.ts`, `worker.ts`, `utils.ts`)
|
|
1390
|
+
|
|
1391
|
+
Consolidates step retry/Claim-Check (`tasks/steps.ts` & `tasks/utils.ts`), durable `StepRunner` (`tasks/steps.ts`), sharded Cloud Tasks micro-batch queue & Firestore watcher (`tasks/watcher.ts`), workflow dispatcher/worker executor (`tasks/worker.ts`), and task status/signal/cancel operations (`tasks/service.ts`).
|
|
1392
|
+
|
|
1393
|
+
#### Signal Classes & Module State
|
|
1394
|
+
- **`TaskYieldSignal`**: Control-flow signal (`'sleeping' | 'waiting_signal' | 'continue_as_new' | 'yield'`) thrown by `step.sleep`, `step.waitForSignal`, `step.continueAsNew`, and `checkTimeBudgetYield`.
|
|
1395
|
+
- **`ReplayCompleteSignal`**: Thrown when `replayOnly: true` (used by `cancel` on detached workflows) finishes re-registering all completed step rollbacks and hits the first unexecuted step.
|
|
1396
|
+
- **`TaskWaitDetachedSignal` & `isTaskWaitDetachedSignal`**: Thrown when an HTTP request's synchronous wait exceeds `maxWaitMs` (`60s`), instructing `RouterService.createApp` to return `202 Accepted`.
|
|
1397
|
+
- **Module State**: `workflowStorage` (`AsyncLocalStorage<ActiveWorkflowStore>`), `activeTaskExecutions`, `compensationLocks`, `batchQueue`, `watchers` (`[prod, dev]`), `activeWatcherCount`, `activeRegistry`.
|
|
1398
|
+
|
|
1399
|
+
#### Public & Internal Methods on `TasksService` (`readonly #core: Core`)
|
|
1400
|
+
|
|
1401
|
+
##### 1. `#maybeOffloadStepValue`, `#maybeHydrateStepValue`, `#runStep`, `allWithRollback`
|
|
1402
|
+
- **Tier**: `Tier 4: Private` / `Tier 2: core.internals.workflows.allWithRollback`
|
|
1403
|
+
- **Contract**:
|
|
1404
|
+
- `#maybeOffloadStepValue` & `#maybeHydrateStepValue`: Transparently offloads step/task payloads `> 200 KiB` (or `> 150 KiB` cumulative) to Firebase Storage at `_tasks/${taskId}/steps/${stepId}.json` (`{ __claimCheck: gcsPath }`) and hydrates on read.
|
|
1405
|
+
- `#runStep`: Executes step function with per-attempt `AbortController`, timeout, exponential backoff + jitter, and immediate re-throw on `TaskYieldSignal` or `isNonRetryableError`.
|
|
1406
|
+
- `allWithRollback`: Runs parallel `RollbackLeg<T>[]` via `Promise.allSettled` and compensates fulfilled legs if any leg rejects.
|
|
1407
|
+
|
|
1408
|
+
##### 2. `createStepRunner`, `useStep`, `getWorkflowContext`, `getActiveWorkflowStore`
|
|
1409
|
+
- **Tier**: `Tier 2: core.internals.workflows.*`
|
|
1410
|
+
- **Contract**: `createStepRunner` builds a durable (`'workflow'`) or in-memory (`'rest'`) `StepRunner` with epoch-fenced checkpoint persistence (`runFencedPersistOnce`), 75% time-budget continuation yielding (`checkTimeBudgetYield`), `step.map` 20-way worker pool, `step.invoke` / `step.spawn` child workflow execution, `step.sleep`, `step.waitForSignal`, `step.continueAsNew`, `step.progress`, and serialized reverse-order Saga `compensate()`.
|
|
1411
|
+
|
|
1412
|
+
##### 3. `watchTask` & `getActiveWatcherCount`
|
|
1413
|
+
- **Tier**: `Tier 2: core.internals.workflows.watchTask` & `getActiveWatcherCount`
|
|
1414
|
+
- **Contract**: Multiplexes 1) `docRef.onSnapshot`, 2) `5,000ms` fallback polling, 3) `65s` heartbeat-aware straggler hedging (`scan-attachment`), 4) `60s` `maxWaitMs` synchronous `202 Accepted` detach, and 5) `510s` hard timeout, cleaning up all listeners/timers in `finally`.
|
|
1415
|
+
|
|
1416
|
+
##### 4. `registerWorkflowRuntime`, `getWorkflowRuntime`, `dispatchWorkflowTask`, `executeWorkflowTask`
|
|
1417
|
+
- **Tier**: `Tier 2: core.internals.workflows.*`
|
|
1418
|
+
- **Contract**:
|
|
1419
|
+
- `registerWorkflowRuntime(registry)` & `getWorkflowRuntime()`: Binds and returns `{ registry }`.
|
|
1420
|
+
- `dispatchWorkflowTask`: Handles deterministic `dedupe_<hash>` transactions, fast inline execution, or durable `_tasks` + sharded Cloud Tasks (`#enqueueBatched` across 20 shards in batches of 100) + `watchTask`.
|
|
1421
|
+
- `executeWorkflowTask`: Claims monotonic `epoch` in a Firestore transaction, runs a `20s` heartbeat transaction loop, executes `wf.runTask(effectiveParams, step)` inside `runWithCore`, handles `TaskYieldSignal` continuation re-enqueues, and runs serialized Saga compensation on terminal errors.
|
|
1422
|
+
|
|
1423
|
+
##### 5. `status`, `signal`, `cancel`
|
|
1424
|
+
- **Tier**: `Tier 1: Public` (`core.tasks.status`, `core.tasks.signal`, `core.tasks.cancel`)
|
|
1425
|
+
- **Contract**:
|
|
1426
|
+
- `status(taskId)`: Reads `_tasks/{taskId}`, enforces `#assertTaskOwnership`, and hydrates `response` via `#maybeHydrateStepValue`.
|
|
1427
|
+
- `signal(taskId, signalName, payload?)`: Atomically records `signals[signalName]` and wakes `'waiting_signal'` workflows by bumping `epoch` and re-enqueuing.
|
|
1428
|
+
- `cancel(taskId)`: Transitions task to `'compensating'` with `epoch + 1`, aborts local worker or runs `replayOnly: true` Saga compensation for detached tasks, and marks `status: 'cancelled'`.
|
|
1429
|
+
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
### 12.3 [`src/core/tasks/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/tasks/routes.ts)
|
|
1433
|
+
|
|
1434
|
+
- **`zodToJsonSchemaSafe`**, **`buildRoutesIntrospection`**, **`renderApiContractMarkdown`** (`Tier 3: Module Export`): Generates the JSON and Markdown API Contract & Schema Reference served by `GET /api/routes` and `GET /api/routes?format=md`.
|
|
1435
|
+
- **`buildHealthRoute`, `buildRoutesListRoute`, `buildTaskStatusRoute`, `buildTaskSignalRoute`, `buildTaskCancelRoute`** (`Tier 3: Module Export`): Route builders mounted into `RouterService.createApp`.
|
|
1436
|
+
|
|
1437
|
+
---
|
|
1438
|
+
|
|
1439
|
+
## 13. Router, Security Guards & Cloud Functions Module (`src/core/router/*`) — Per-Function Spec
|
|
1440
|
+
|
|
1441
|
+
### 13.1 [`src/core/router/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/router/schema.ts)
|
|
1442
|
+
|
|
1443
|
+
- **`defineRoute`, `defineRoutes`, `defineRouteOverrides`** (`Tier 1: core.define.*` & `Tier 3: Module Export`).
|
|
1444
|
+
- **`CORE_ROUTE_DEFAULTS`** (`Tier 2: core.internals.context.routeDefaults` & `Tier 3: Module Export`): Declarative table of all 51 built-in `CoreRouteId` configurations ([Section 3.3](#33-master-built-in-http-routes-core_route_defaults--51-endpoints)).
|
|
1445
|
+
|
|
1446
|
+
---
|
|
1447
|
+
|
|
1448
|
+
### 13.2 [`src/core/router/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/router/service.ts) (`class RouterService`) & [`src/core/router/guard.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/router/guard.ts) (`class RouterGuardEngine`)
|
|
1449
|
+
|
|
1450
|
+
Consolidates IP extraction, HTTP rate limiting, request authentication, declarative resource guards, and HTTP idempotency (`router/guard.ts`) with Cloud Functions v2 handler creation and `createApp` Hono bootstrap (`router/service.ts`).
|
|
1451
|
+
|
|
1452
|
+
#### Methods on `RouterService` (`readonly #core: Core`)
|
|
1453
|
+
1. **`#getClientIp`, `#isMasterKey`, `#applyRateLimit`** (`Tier 4: Private (#method)`): Extracts spoof-resistant client IP (`xff.at(-2)`), verifies `SDK_API_KEY` via constant-time `timingSafeEqual` on SHA-256 digests, and enforces per-route/user/IP HTTP rate limits via `this.#core.resilience.acquireHttpLimit`.
|
|
1454
|
+
2. **`#authenticateRequest`** (`Tier 4: Private (#method)`): Authenticates `Bearer` token, `session` cookie, or `SDK_API_KEY` via `this.#core.auth.verifyToken`, sets `x-auth-time-ms` / `Server-Timing` headers, and applies rate limits.
|
|
1455
|
+
3. **`#enforceGuard`** (`Tier 4: Private (#method)`): Evaluates declarative `cfg.guard` rules (`checkFileRef`, `checkCollectionField`, `checkUserId`, `requireEmailVerified`, `custom`).
|
|
1456
|
+
4. **`#createCloudFunctions`** (`Tier 4: Private (#method)`): Creates the 4 Cloud Functions 2nd Gen handlers (`httpHandler`, `devHttpHandler`, `tasksHandler`, `devTasksHandler`).
|
|
1457
|
+
5. **`createApp`** (`Tier 1: Public` — `core.createApp`): Registers collections, agents, attachment types, context catalog, and email sender; compiles unified route invokers (HTTP, AI Agent tools, MCP tools, and durable workflows with `202 Accepted` detach); mounts all routes + `ALL /api/mcp` onto prod/dev Hono apps; and returns `{ app, devApp, functions }`.
|
|
1458
|
+
|
|
1459
|
+
---
|
|
1460
|
+
|
|
1461
|
+
## 14. Resilience, Rate Limiting, Circuit Breaker & Error Utilities (`src/core/utils/*`) + End-to-End DX Examples
|
|
1462
|
+
|
|
1463
|
+
### 14.1 [`src/core/utils/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/utils/schema.ts)
|
|
1464
|
+
|
|
1465
|
+
Defines `errorBodySchema` (`core.schemas.errorBodySchema`), `ErrorBody`, `Duration`, `BreakerName`, `CircuitState`, `CircuitBreakerDoc`, `SingleflightScope`, and `SingleflightOptions`.
|
|
1466
|
+
|
|
1467
|
+
---
|
|
1468
|
+
|
|
1469
|
+
### 14.2 [`src/core/utils/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/utils/service.ts) (`ErrorsService` & `ResilienceService`)
|
|
1470
|
+
|
|
1471
|
+
#### 1. `ErrorsService` (`core.errors.*`)
|
|
1472
|
+
|
|
1473
|
+
| Symbol | Tier | Kind | Signature | Contract |
|
|
1474
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
1475
|
+
| `parseDuration` | `Tier 1: Public` (`core.errors.parseDuration`) | Method | `(d?: Duration, fallbackMs = 0) => number` | Parses numbers or `'500ms' \| '30s' \| '5m' \| '2h' \| '7d'` into milliseconds. |
|
|
1476
|
+
| `NonRetryableError` | `Tier 1: Public` (`core.errors.NonRetryableError`) | Class | `new NonRetryableError(message: string, code?: string)` | Error subclass (`nonRetryable = true`) that aborts step/workflow retries immediately. |
|
|
1477
|
+
| `StaleWorkerEpochError` | `Tier 1: Public` (`core.errors.StaleWorkerEpochError`) | Class | `new StaleWorkerEpochError(message?, meta?: { taskId?; claimedEpoch?; actualEpoch? })` | Subclass of `NonRetryableError` thrown when a worker's monotonic `epoch` is superseded. |
|
|
1478
|
+
| `isStaleWorkerEpochError` | `Tier 1: Public` (`core.errors.isStaleWorkerEpochError`) | Method | `(err: unknown) => err is StaleWorkerEpochError` | Type guard checking `StaleWorkerEpochError` instance or shape. |
|
|
1479
|
+
| `CircuitBreakerOpenError` | `Tier 1: Public` (`core.errors.CircuitBreakerOpenError`) | Class | `new CircuitBreakerOpenError(breakerKey: string, message?)` | Subclass of `NonRetryableError` thrown when a circuit breaker is `OPEN` with no fallback. |
|
|
1480
|
+
| `nonRetryableError` | `Tier 1: Public` (`core.errors.nonRetryableError`) | Method | `(message: string, code?: string) => never` | Throws `new NonRetryableError(message, code)`. |
|
|
1481
|
+
| `httpError` | `Tier 1: Public` (`core.errors.httpError`) | Method | `(status: ContentfulStatusCode, message: string, code?: string, retryAfterMs?: number) => never` | Throws Hono `HTTPException` with JSON body and optional `Retry-After` header. |
|
|
1482
|
+
| `isNonRetryableError` | `Tier 1: Public` (`core.errors.isNonRetryableError`) | Method | `(err: unknown) => boolean` | Returns `true` for `NonRetryableError`, `ZodError`, 4xx `HTTPException`, or `NOT_FOUND` errors. |
|
|
1483
|
+
| `toHttpError` | `Tier 3: Internal Service Method` | Method | `(err: unknown) => HTTPException` | Global Hono error normalizer converting `ZodError` (`400`), `NonRetryableError` (`400`), or unhandled errors (`500`) into `HTTPException`. |
|
|
1484
|
+
|
|
1485
|
+
#### 2. `ResilienceService` (`core.internals.resilience.*`)
|
|
1486
|
+
- **`singleflight(key, fn, opts?)`** (`Tier 2: core.internals.resilience.singleflight`): Coalesces concurrent promises by `tenant:${this.#core.userId}:${key}` (default) or `global:${key}` (executed inside a neutral admin `runWithCore` scope so caller ALS state never leaks).
|
|
1487
|
+
- **`acquireLimit(bucket, cost?)`, `chargeLimit(bucket, extra)`, `syncLimit(bucket, remaining, max)`** (`Tier 2 & Tier 3`): Sliding-window token-bucket rate limiter (`RateLimiter`) per upstream bucket (`mistral`, `jev`, `ai`, `workflows`, `firestore`, `storage`).
|
|
1488
|
+
- **`acquireHttpLimit(key, max, windowMs)`** (`Tier 2: core.internals.resilience.acquireHttpLimit`): Fixed-window counter per user/IP key (capped at 5,000 active keys); throws `429 RATE_LIMITED` with `Retry-After` header when `count >= max`.
|
|
1489
|
+
- **`circuitBreaker(breakerKey, fn, fallbackFn?, opts?)`, `getCircuitBreakerState(breakerKey, isDev?)`, `resetCircuitBreaker(breakerKey, isDev?)`** (`Tier 2: core.internals.resilience.*`): Distributed 3-state (`CLOSED` / `OPEN` / `HALF_OPEN`) circuit breaker backed by `_system/breaker_${breakerKey}` in Firestore with a `3,000ms` local cache and automatic fallback execution.
|
|
1490
|
+
|
|
1491
|
+
---
|
|
1492
|
+
|
|
1493
|
+
### 14.3 End-to-End Developer Experience (DX) Examples
|
|
1494
|
+
|
|
1495
|
+
#### 1. Single-File Durable Workflow Route (`src/routes/portfolio.ts`)
|
|
1496
|
+
|
|
1497
|
+
```ts
|
|
1498
|
+
import { core } from '@core'
|
|
1499
|
+
import { CASE_IDS, portfolioCaseSchema, portfolioService } from '@services/portfolio'
|
|
1500
|
+
import { z } from 'zod'
|
|
1501
|
+
|
|
1502
|
+
export const processPortfolioInput = z.object({
|
|
1503
|
+
refs: z.array(core.schemas.fileRef).min(1)
|
|
1504
|
+
})
|
|
1505
|
+
|
|
1506
|
+
export const processPortfolioOutput = z.object({
|
|
1507
|
+
attachments: z.array(core.schemas.processedAttachment),
|
|
1508
|
+
case: portfolioCaseSchema,
|
|
1509
|
+
notes: z.array(z.string())
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
export const processPortfolioRoute = core.define.route({
|
|
1513
|
+
description: `Scan all portfolio attachment refs in parallel, then classify the portfolio registration case (${CASE_IDS.join(', ')}) with flags, confidence, and notes.`,
|
|
1514
|
+
guard: { checkFileRef: true },
|
|
1515
|
+
mode: 'workflow',
|
|
1516
|
+
mcp: true,
|
|
1517
|
+
input: processPortfolioInput,
|
|
1518
|
+
output: processPortfolioOutput,
|
|
1519
|
+
handler: async ({ input, core, step }) => {
|
|
1520
|
+
const { attachments } = await core.attachments.process(input)
|
|
1521
|
+
const decision = await step('portfolio', () => portfolioService({ attachments }))
|
|
1522
|
+
return { attachments, case: decision, notes: decision.notes }
|
|
1523
|
+
}
|
|
1524
|
+
})
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
#### 2. Reusable Business Service Using `useCore()` (`src/services/portfolio.ts`)
|
|
1528
|
+
|
|
1529
|
+
```ts
|
|
1530
|
+
import { z } from 'zod'
|
|
1531
|
+
import { core, useCore } from '@core'
|
|
1532
|
+
|
|
1533
|
+
export const CASE_IDS = ['A1', 'A2', 'B1', 'B2'] as const
|
|
1534
|
+
export const portfolioCaseSchema = z.object({
|
|
1535
|
+
case: z.enum(CASE_IDS),
|
|
1536
|
+
flags: z.object({ poaPending: z.boolean() }),
|
|
1537
|
+
confidence: z.number(),
|
|
1538
|
+
notes: z.array(z.string())
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
export const portfolioService = async ({
|
|
1542
|
+
attachments
|
|
1543
|
+
}: {
|
|
1544
|
+
attachments: core.types.ProcessedAttachment[]
|
|
1545
|
+
}): Promise<z.infer<typeof portfolioCaseSchema>> => {
|
|
1546
|
+
const coreClient = useCore()
|
|
1547
|
+
const valid = attachments.filter((d): d is { ref: string; scan: core.types.FileScan } => 'scan' in d)
|
|
1548
|
+
if (valid.length === 0) throw new Error('All attachment scans failed')
|
|
1549
|
+
|
|
1550
|
+
const documents = valid.map((d) => ({
|
|
1551
|
+
ref: d.ref,
|
|
1552
|
+
type: d.scan.type,
|
|
1553
|
+
...d.scan.fields,
|
|
1554
|
+
content: d.scan.markdown.slice(0, 2000)
|
|
1555
|
+
}))
|
|
1556
|
+
|
|
1557
|
+
const { answers } = await coreClient.ai.decide({ documents }, QUESTIONS)
|
|
1558
|
+
// ...
|
|
1559
|
+
}
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
#### 3. Custom Collections with Role-Based Access Control (`src/config/collections.ts`)
|
|
1563
|
+
|
|
1564
|
+
```ts
|
|
1565
|
+
import { z } from 'zod'
|
|
1566
|
+
import { core } from '@core'
|
|
1567
|
+
|
|
1568
|
+
export const collections = core.define.collections({
|
|
1569
|
+
portfolios: {
|
|
1570
|
+
schema: z.object({
|
|
1571
|
+
userId: core.schemas.entryId,
|
|
1572
|
+
case: z.enum(['A1', 'A2', 'B1', 'B2']),
|
|
1573
|
+
refs: z.array(core.schemas.fileRef),
|
|
1574
|
+
confidence: z.number(),
|
|
1575
|
+
notes: z.array(z.string())
|
|
1576
|
+
}),
|
|
1577
|
+
rbac: {
|
|
1578
|
+
user: { own: ['read', 'create'] }
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
})
|
|
1582
|
+
|
|
1583
|
+
declare module '@core' {
|
|
1584
|
+
interface Register {
|
|
1585
|
+
collections: typeof collections
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
#### 4. Application Bootstrap (`src/index.ts`)
|
|
1591
|
+
|
|
1592
|
+
```ts
|
|
1593
|
+
import { agents } from '@config/agents'
|
|
1594
|
+
import { collections } from '@config/collections'
|
|
1595
|
+
import { context } from '@config/context'
|
|
1596
|
+
import { coreRouteOverrides } from '@config/core-overrides'
|
|
1597
|
+
import { routes } from '@config/routes'
|
|
1598
|
+
import { core } from '@core'
|
|
1599
|
+
|
|
1600
|
+
export const {
|
|
1601
|
+
app,
|
|
1602
|
+
devApp,
|
|
1603
|
+
createApp,
|
|
1604
|
+
workflowRegistry,
|
|
1605
|
+
httpHandler,
|
|
1606
|
+
devHttpHandler,
|
|
1607
|
+
tasksHandler,
|
|
1608
|
+
devTasksHandler,
|
|
1609
|
+
functions
|
|
1610
|
+
} = core.createApp({
|
|
1611
|
+
collections,
|
|
1612
|
+
context,
|
|
1613
|
+
agents,
|
|
1614
|
+
routes,
|
|
1615
|
+
coreRouteOverrides
|
|
1616
|
+
})
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
---
|
|
1620
|
+
|
|
1621
|
+
## 15. Transactional Emails & Payments Modules (`src/core/emails/*`, `src/core/payments/*`) — Per-Function Spec
|
|
1622
|
+
|
|
1623
|
+
### 15.1 Transactional Emails Module (`src/core/emails/`)
|
|
1624
|
+
|
|
1625
|
+
#### [`src/core/emails/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/emails/schema.ts), [`src/core/emails/utils.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/emails/utils.ts), [`src/core/emails/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/emails/service.ts) & [`src/core/emails/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/emails/routes.ts) (`class EmailsService`)
|
|
1626
|
+
|
|
1627
|
+
- **`core.define.emailTemplates(templates)`**: Registers strongly-typed email templates with Zod `variables`, optional `onBeforeSend(vars, to)` interceptor (which can mutate variables or return `false` to suppress sending), and provider-specific overrides (`providers.sendgrid.templateId`, `providers.resend.{templateId, subject, html, text}`).
|
|
1628
|
+
- **`core.emails.send(input, opts?)`**: Validates template variables, enforces deterministic idempotency via `idempotencyKey` (`em_<sha256_40>`), checks `email_suppression`, dispatches through SendGrid (`POST /v3/mail/send`) or Resend (`POST /emails`) inside the `'emails'` circuit breaker and rate limiter, and writes an audit record to `email_log`.
|
|
1629
|
+
- **`core.emails.sendBatch(inputs, opts?)`**: Dispatches up to 100 emails in a single batch (using Resend's native `/emails/batch` endpoint or parallel `send` calls) and returns `{ total, sent, suppressed, failed, results }`.
|
|
1630
|
+
- **`core.emails.sendAuthEmail(purpose, to, vars?, opts?)`**: Dispatches transactional authentication emails (`verify_email`, `reset_password`, `verify_recovery_email`, `account_recovery`, `welcome`, `security_alert`) with `onAuthEmail` / `authFlows[purpose].onSend` hooks, custom templates, or Firebase Identity Toolkit OOB fallback.
|
|
1631
|
+
- **`core.internals.emails.verifyAndProcessWebhook(provider, rawBody, headers)`**: Verifies Resend (`svix-id`, `svix-timestamp`, `svix-signature`) and SendGrid (`x-twilio-email-event-webhook-timestamp`, `x-twilio-email-event-webhook-signature`) HMAC-SHA256 webhooks in constant time with replay-window enforcement, updates `email_log` status (`delivered`, `opened`, `clicked`, `bounced`, `complained`), and automatically records hard bounces and spam complaints in `email_suppression`.
|
|
1632
|
+
|
|
1633
|
+
---
|
|
1634
|
+
|
|
1635
|
+
### 15.2 Payments, Subscriptions & AI Token Metering Module (`src/core/payments/`)
|
|
1636
|
+
|
|
1637
|
+
#### [`src/core/payments/schema.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/payments/schema.ts), [`src/core/payments/utils.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/payments/utils.ts), [`src/core/payments/billing.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/payments/billing.ts), [`src/core/payments/service.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/payments/service.ts) & [`src/core/payments/routes.ts`](file:///Users/nikdelvin/Work/bso-ai/src/core/payments/routes.ts) (`class PaymentsService` & `class PaymentsBillingEngine`)
|
|
1638
|
+
|
|
1639
|
+
- **`core.define.plans(plans)` & `core.define.products(products)`**: Declaratively registers recurring/free subscription tiers (`month` / `year` / `free`, environment-scoped `providers.{stripe, dodo}`, `quota.aiTokens`, `quota.metrics`, `quota.overage`) and one-time purchasable products/add-ons (`grants.{aiTokens, credits, metrics}`). Automatically sets `CORE_CONFIG.payments.enabled = true` when definitions are registered.
|
|
1640
|
+
- **`core.payments.{createCheckout, createPaymentLink, createInvoice, charge}`**: Supports both predefined catalog aliases (`{ plan }` / `{ product }`) and dynamic ad-hoc line items (`{ custom: { name, amount, currency, quantity } }`) across **Stripe** (`/v1/*` form-urlencoded REST API) and **Dodo Payments** (`https://live.dodopayments.com` / `https://test.dodopayments.com` JSON REST API) inside the `'payments'` circuit breaker and rate limiter.
|
|
1641
|
+
- **`core.payments.{subscribe, changeSubscriptionTier, cancelSubscription, getSubscription}`**: Manages full subscription lifecycles including free-tier activation, recurring checkouts, prorated upgrades/downgrades (`prorated_immediately`, `next_cycle`, `none`), and period-end or immediate cancellation, synchronized with `payment_customer/{userId}`.
|
|
1642
|
+
- **`core.payments.{getUsage, checkQuota, recordUsage}` & Automatic AI Token Metering (`core.internals.payments.{enforceAiQuota, trackAiUsage}`)**: Tracks per-period AI token and custom metric consumption in `payment_usage/{userId}_{periodKey}` via Firestore transactions, automatically deducts one-time `bonusAiTokens` / `bonusMetrics` when base plan limits are reached, enforces `402 Payment Required` (`QUOTA_EXCEEDED`) when hard caps are exceeded, and ingests overage meter events to Dodo (`POST /events/ingest`) and Stripe (`POST /v1/billing/meter_events`).
|
|
1643
|
+
- **`core.internals.payments.verifyAndProcessWebhook(provider, rawBody, headers)`**: Verifies Stripe (`Stripe-Signature: t=...,v1=...`) and Dodo Payments (`webhook-id`, `webhook-timestamp`, `webhook-signature` Standard Webhooks `whsec_<base64>`) HMAC-SHA256 signatures using `node:crypto` `timingSafeEqual` and a 300-second replay window, deduplicates events in `payment_transaction/{txId}`, reconciles `payment_customer/{userId}`, and atomically grants `bonusAiTokens` / `credits` / `bonusMetrics`.
|
|
1644
|
+
|
|
1645
|
+
|