@soulcraft/sdk 1.4.6 → 1.4.8

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.
@@ -0,0 +1,248 @@
1
+ # ADR-002: Transport Protocol — Wire Format, Auth, and Reconnection
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-03-02
5
+ **Supersedes:** None
6
+ **See also:** `ADR-001-sdk-design.md` (Decision 5)
7
+
8
+ ---
9
+
10
+ ## Context
11
+
12
+ The SDK must communicate between kit apps (browser) or product backends and a Brainy
13
+ server over a network. Four distinct communication patterns exist across the platform:
14
+
15
+ 1. **Stateless RPC** — kit apps call Brainy methods on demand (no persistent connection needed)
16
+ 2. **Real-time bidirectional RPC + push** — Venue kit apps need live change events alongside RPC
17
+ 3. **Server-push only** — Workshop workspace event stream (VFS/entity mutations)
18
+ 4. **In-process** — server-mode SDK calls Brainy directly with zero serialization overhead
19
+
20
+ These patterns have meaningfully different requirements for serialization, auth, and
21
+ error handling. This ADR records the decisions made for each.
22
+
23
+ ---
24
+
25
+ ## Decision 1: Four Transports, Fixed Serialization
26
+
27
+ Four transport implementations are provided. Serialization format is fixed per transport
28
+ and is never configurable:
29
+
30
+ | Transport | Class | Serialization | Direction | Use case |
31
+ |-----------|-------|--------------|-----------|---------|
32
+ | `local` | `LocalTransport` | None (in-process) | — | Server mode, zero overhead |
33
+ | `http` | `HttpTransport` | JSON | Request/response | Stateless RPC — kit apps, simple clients |
34
+ | `ws` | `WsTransport` | MessagePack binary | Bidirectional | Real-time — RPC + change push events |
35
+ | `sse` | `SseTransport` | text/event-stream | Server → Client | Live updates only — VFS/entity events |
36
+
37
+ **Rationale for fixed serialization:**
38
+ - HTTP=JSON: universally debuggable with `curl`, browser DevTools, and server logs.
39
+ The request volume at typical HTTP RPC frequencies makes binary encoding a
40
+ premature optimization with no measurable benefit.
41
+ - WebSocket=MessagePack: bidirectional real-time traffic has high message frequency
42
+ and persistent connections where binary encoding is measurably more efficient.
43
+ MessagePack was already the wire format in `@soulcraft/brainy-client`.
44
+ - Making serialization configurable adds complexity with no practical benefit —
45
+ consumers never need to mix formats within a single transport.
46
+
47
+ **Rejected alternatives:**
48
+ - MessagePack over HTTP: non-standard, incompatible with standard proxies and logging tools.
49
+ - gRPC/Protobuf: over-engineered for the current scale and requires a schema compilation step.
50
+ - Configurable serializer per transport: extra complexity for a use case that has never arisen.
51
+
52
+ ---
53
+
54
+ ## Decision 2: HTTP Transport Wire Format
55
+
56
+ **Endpoint:** `POST {baseUrl}/api/brainy/rpc`
57
+
58
+ **Request body** (JSON):
59
+ ```json
60
+ { "method": "find", "args": [{ "query": "candle kits", "limit": 10 }] }
61
+ ```
62
+
63
+ **Success response** (JSON):
64
+ ```json
65
+ { "result": [ ... ] }
66
+ ```
67
+
68
+ **Error response** (JSON):
69
+ ```json
70
+ { "error": { "code": "BRAINY_NOT_FOUND", "message": "Entity not found" } }
71
+ ```
72
+
73
+ **Auth:** `Authorization: Bearer <capability-token>` for server-to-server calls;
74
+ `credentials: 'include'` (session cookies) for browser kit apps.
75
+
76
+ **Timeout:** 30 seconds via `AbortController`. Throws `SDKTimeoutError` on expiry.
77
+
78
+ **HTTP status mapping:**
79
+ | Status | Error class |
80
+ |--------|------------|
81
+ | 401 | `SDKAuthError` |
82
+ | 403 | `SDKForbiddenError` |
83
+ | network failure | `SDKDisconnectedError` |
84
+ | 200 + `error` body | `SDKRpcError` |
85
+
86
+ The HTTP transport is stateless — `isAlive()` always returns `true`. Errors surface
87
+ as rejected promises from individual `call()` invocations.
88
+
89
+ ---
90
+
91
+ ## Decision 3: WebSocket Transport Wire Format
92
+
93
+ **Endpoint:** `wss://{host}/api/brainy/ws`
94
+
95
+ **Auth:** Capability token sent as `Authorization: Bearer <token>` on the WebSocket
96
+ upgrade request. This is a Bun-specific extension (`@ts-expect-error` in source).
97
+ Standard browser `WebSocket` does not support custom headers on the upgrade request —
98
+ browser kit apps use the HTTP transport or pass the token as a `?token=` query param.
99
+
100
+ **Scope param:** `?scope=<tenantSlug>` on the connection URL. Venue uses this to
101
+ select the correct per-tenant Brainy instance.
102
+
103
+ ### Connection handshake
104
+
105
+ After a successful upgrade + auth check the server sends a `ready` message:
106
+ ```
107
+ Server → Client: { "type": "ready", "scope": "wicks-and-whiskers" }
108
+ ```
109
+
110
+ `WsTransport.connect()` resolves only after this message. A 15-second timeout
111
+ applies — if no `ready` arrives the connection is aborted with an error.
112
+
113
+ ### RPC messages (binary MessagePack)
114
+
115
+ **Client → Server:**
116
+ ```
117
+ { id: "42", method: "vfs.readdir", args: ["/workspace/docs"] }
118
+ ```
119
+
120
+ The `id` is a monotonically incrementing integer cast to string, unique per connection.
121
+
122
+ **Server → Client (RPC response):**
123
+ ```
124
+ { id: "42", result: [ ... ] }
125
+ { id: "42", error: { code: "VFS_NOT_FOUND", message: "Path not found" } }
126
+ ```
127
+
128
+ Pending RPCs are matched to their response by `id`. A per-call 30-second timeout
129
+ rejects calls that receive no response.
130
+
131
+ ### Change push events (binary MessagePack)
132
+
133
+ ```
134
+ { type: "change", event: "add"|"update"|"delete"|"relate"|"unrelate",
135
+ entity?: { ... }, relation?: { ... } }
136
+ ```
137
+
138
+ Push events have no `id` field and are dispatched to all registered `onEvent` handlers.
139
+
140
+ ### Reconnection
141
+
142
+ On unexpected disconnection (any close code except 4001, 4003, or explicit `close()`):
143
+
144
+ - All pending RPC calls are rejected with `SDKDisconnectedError`
145
+ - Reconnect is attempted with **exponential backoff**: `min(1000 × 2^attempt, 30_000)` ms
146
+ - Maximum 10 attempts (configurable via constructor)
147
+ - After 10 failed attempts the transport enters `closed` state permanently
148
+
149
+ **Auth failure codes — no retry:**
150
+ | Close code | Meaning | Behaviour |
151
+ |-----------|---------|----------|
152
+ | 4001 | Unauthorized (bad/expired token) | Reject pending calls with `SDKAuthError`, set `closed` |
153
+ | 4003 | Forbidden (insufficient scope) | Reject pending calls with `SDKForbiddenError`, set `closed` |
154
+
155
+ ### Connection states
156
+
157
+ ```
158
+ disconnected → connecting → connected → disconnected (unexpected) → connecting (reconnect) ...
159
+ → closed (explicit close() or auth failure)
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Decision 4: SSE Transport Wire Format
165
+
166
+ **Endpoint:** `GET {baseUrl}/api/workspace/events[?workspaceId=<id>]`
167
+
168
+ Uses the browser's native `EventSource` API. `EventSource` handles reconnection
169
+ automatically using the server-sent `retry:` interval. Additionally, the SDK applies
170
+ manual exponential backoff (identical schedule to WsTransport) on `onerror` events
171
+ to prevent thundering-herd reconnects on server restart.
172
+
173
+ **Event format** (JSON-encoded in the SSE `data:` field):
174
+ ```
175
+ data: {"type":"change","event":"update","entity":{"id":"abc","nounType":"Booking",...}}
176
+ ```
177
+
178
+ **Connection confirmation** (internal, not dispatched to listeners):
179
+ ```
180
+ data: {"type":"connected","connectionId":"conn-abc123"}
181
+ ```
182
+
183
+ **Receive-only:** `call()` throws `SDKError` with code `SSE_NO_RPC`. For outbound
184
+ operations pair this transport with an `HttpTransport`.
185
+
186
+ ---
187
+
188
+ ## Decision 5: Local Transport (In-Process)
189
+
190
+ Used exclusively in server mode. Wraps a native `Brainy` instance. No serialization,
191
+ no network, no error class mapping.
192
+
193
+ **Method resolution:** Dot-separated method paths are resolved by recursive property
194
+ traversal on the Brainy instance. `'vfs.readdir'` → `brain.vfs.readdir(args)`.
195
+
196
+ **Change events:** `onEvent`/`offEvent` delegate to Brainy's built-in change listeners
197
+ directly on the instance. No intermediary.
198
+
199
+ **isAlive():** Always `true`. **close():** No-op.
200
+
201
+ ---
202
+
203
+ ## Decision 6: Shared Error Classes
204
+
205
+ All transports share a common error hierarchy:
206
+
207
+ ```
208
+ SDKError (base, has `code: string`)
209
+ ├── SDKAuthError (401 / close code 4001)
210
+ ├── SDKForbiddenError (403 / close code 4003)
211
+ ├── SDKTimeoutError (30s per-call timeout)
212
+ ├── SDKDisconnectedError (transport not connected, or unexpected close)
213
+ └── SDKRpcError (server returned { error: { code, message } })
214
+ ```
215
+
216
+ Consumer code catches `SDKAuthError` to redirect to login, `SDKDisconnectedError`
217
+ to show a reconnecting state, etc.
218
+
219
+ ---
220
+
221
+ ## Decision 7: Capability Tokens for Server-to-Server Auth
222
+
223
+ Transport-level auth uses HMAC-SHA256 capability tokens:
224
+
225
+ ```
226
+ <base64url(payload)>.<base64url(signature)>
227
+ ```
228
+
229
+ `payload` is `{ sub, scope, iat, exp }` JSON. Default TTL: 1 hour.
230
+
231
+ **Timing-safe verification** uses `crypto.timingSafeEqual` to prevent timing attacks.
232
+
233
+ These tokens are separate from OIDC session tokens. They are used for:
234
+ - Kit apps making RPC calls to the Venue or Workshop backend
235
+ - Server-to-server calls (e.g. Academy → Workshop)
236
+
237
+ Session cookie auth (browser kit apps in same-origin context) uses `credentials: 'include'`
238
+ on the HTTP transport and does not require a capability token.
239
+
240
+ ---
241
+
242
+ ## Related Documents
243
+
244
+ - `ADR-001-sdk-design.md` — Overall SDK architecture (Decision 5: transport rationale)
245
+ - `ADR-003-instance-strategies.md` — How server-side Brainy instances are pooled and selected
246
+ - `src/transports/` — Implementation of all four transports
247
+ - `src/modules/brainy/errors.ts` — Shared error class hierarchy
248
+ - `src/modules/brainy/auth.ts` — Capability token creation and verification
@@ -0,0 +1,216 @@
1
+ # ADR-003: Brainy Instance Strategies — Pooling, Lifecycle, and Cortex Activation
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-03-02
5
+ **Supersedes:** Workshop's `brainy-singleton.ts`, Venue's inline `tenantCache`
6
+ **See also:** `ADR-001-sdk-design.md` (Decision 4), `ADR-002-transport-protocol.md`
7
+
8
+ ---
9
+
10
+ ## Context
11
+
12
+ Each product manages Brainy instances differently:
13
+
14
+ - **Workshop** — one Brainy per user × workspace. Potentially hundreds of instances
15
+ across all Workshop users. Each user's data is isolated by email hash.
16
+ - **Venue** — one Brainy per tenant (one Brainy for all of "Wicks & Whiskers", another
17
+ for "The Candle Studio"). Tenant count is bounded but tenants can be busy.
18
+ - **Academy** — one Brainy per course section, or per learner × course. Domain-specific
19
+ key logic that doesn't fit either Workshop or Venue's pattern.
20
+
21
+ Before the SDK, each product maintained its own singleton/cache implementation with
22
+ diverging eviction policies, flush-on-shutdown behaviour, and Cortex activation code.
23
+ This caused drift and subtle bugs.
24
+
25
+ `BrainyInstancePool` centralizes the pattern in one place.
26
+
27
+ ---
28
+
29
+ ## Decision 1: Three Built-In Strategies
30
+
31
+ | Strategy | Key | Storage path | Product |
32
+ |----------|-----|-------------|---------|
33
+ | `per-user` | `emailHash:workspaceId` | `{dataPath}/{emailHash}/{workspaceId}/` | Workshop |
34
+ | `per-tenant` | `tenantSlug` | `{dataPath}/{tenantSlug}/` | Venue |
35
+ | `per-scope` | caller-supplied `scopeKey(userId, workspaceId)` | caller-supplied factory | Academy, custom |
36
+
37
+ `per-scope` is a general escape hatch — the caller provides both the key function
38
+ and a `factory: () => Promise<Brainy>` per scope. This avoids a fourth built-in
39
+ strategy that would only serve edge cases.
40
+
41
+ Default max instances: `200` for `per-user`, `50` for `per-tenant`. Both are
42
+ configurable via `maxInstances`.
43
+
44
+ ---
45
+
46
+ ## Decision 2: LRU Eviction with Non-Blocking Flush
47
+
48
+ The pool is backed by `lru-cache`. When the cache is full and a new instance is
49
+ needed, the LRU entry is evicted.
50
+
51
+ **Eviction is synchronous** — `lru-cache`'s `dispose` callback fires synchronously.
52
+ `brain.flush()` is initiated non-blocking from within `dispose` if `flushOnEvict: true`.
53
+ This is intentional: delaying eviction until flush completes would block the incoming
54
+ request that triggered the eviction.
55
+
56
+ **Consequence:** On abrupt process termination, recently evicted instances may not have
57
+ flushed. Products should call `sdk.shutdown()` (which calls `pool.shutdown()`) in their
58
+ `SIGTERM` handler to ensure all pending flushes complete before exit.
59
+
60
+ **The `flushOnEvict` default is `true`.** Setting it to `false` is only appropriate for
61
+ ephemeral development data or when the caller guarantees clean shutdown via explicit
62
+ `pool.flush(key)` calls.
63
+
64
+ ---
65
+
66
+ ## Decision 3: Concurrent Init Deduplication
67
+
68
+ Brainy's cold start takes 25–30 seconds on a dataset of typical size. In a multi-request
69
+ environment it is common for several requests to arrive simultaneously for the same scope
70
+ before the first init completes.
71
+
72
+ Without deduplication each request would spawn a separate Brainy init, creating multiple
73
+ instances pointing at the same storage path — a correctness violation (concurrent writers
74
+ on the same mmap files corrupt data).
75
+
76
+ The pool uses a `pending: Map<string, Promise<Brainy>>` to deduplicate. The first request
77
+ for a key starts the init and stores the Promise. Subsequent requests for the same key
78
+ return the existing Promise and await the same init. Only one Brainy is ever created per key.
79
+
80
+ ---
81
+
82
+ ## Decision 4: Storage Backends and Cortex Activation
83
+
84
+ Two `storage` values are supported:
85
+
86
+ | Value | When to use | Cortex behavior |
87
+ |-------|-------------|-----------------|
88
+ | `'filesystem'` | Local development | No Cortex plugin loaded |
89
+ | `'mmap-filesystem'` | Production (GCE with persistent disk) | Cortex plugin loaded |
90
+
91
+ When `storage: 'mmap-filesystem'`, the pool passes `plugins: ['@soulcraft/cortex']` to
92
+ the Brainy constructor. Cortex intercepts Brainy's storage layer at init time and upgrades
93
+ the filesystem provider to native Rust mmap SSTables. From the pool's perspective Brainy
94
+ is initialized the same way in both modes — the storage upgrade is transparent.
95
+
96
+ **Cortex is a Brainy plugin, not a separate init step.** The pool does not call any
97
+ Cortex activation API directly. License activation is handled by `fromLicense()` which
98
+ writes `.soulcraft.json` before any Brainy instance is created. Cortex reads the JWT
99
+ from `.soulcraft.json` at plugin load time.
100
+
101
+ ### Cortex license flow (from Workshop experience)
102
+
103
+ This sequence is critical and order-dependent:
104
+
105
+ ```
106
+ 1. fromLicense({ product }) — exchanges license key with Portal
107
+ 2. → Portal returns sc_cortex_<jwt> in the config bundle
108
+ 3. → fromLicense() writes { "cortex": "sc_cortex_..." } to .soulcraft.json atomically
109
+ 4. Pool._initBrainy() called on first request
110
+ 5. → new Brainy({ plugins: ['@soulcraft/cortex'] })
111
+ 6. → Cortex plugin loads, reads .soulcraft.json, validates JWT offline
112
+ 7. → If valid: upgrades storage to native mmap — logs "Providers: N/10 native"
113
+ 8. → brain.init() completes
114
+ ```
115
+
116
+ **If step 3 is skipped** (`.soulcraft.json` absent or contains a short-code like
117
+ `CX-63QM-UV67` instead of a JWT), Cortex logs a validation error and falls back to
118
+ pure filesystem storage. The SDK continues to function — Brainy works without Cortex —
119
+ but native SIMD acceleration is disabled.
120
+
121
+ **The CX code is a short-code, not the JWT.** `fromLicense()` exchanges the short-code
122
+ for a JWT. The JWT is what Cortex validates offline. Never write a raw `CX-…` code into
123
+ `.soulcraft.json` and expect Cortex to accept it — it will not.
124
+
125
+ ### Known issue (Cortex ≤ 2.1.3, fixed in 2.1.5)
126
+
127
+ In Cortex 2.1.3 the public key compiled into the native binary did not match the key
128
+ Portal used to sign JWTs. Every JWT was rejected with `invalid signature` regardless
129
+ of validity. This was fixed in `@soulcraft/cortex@2.1.5` (compile-time key rotation
130
+ by the Cortex team). The minimum required version is now `>=2.1.5` in peer dependencies.
131
+
132
+ ---
133
+
134
+ ## Decision 5: `onInit` Hook for Product-Specific Post-Init Logic
135
+
136
+ The pool config accepts an optional `onInit(brain, storagePath)` async callback
137
+ called after each new Brainy instance completes `init()`. This is the escape hatch
138
+ for product-specific logic that must run once per instance creation:
139
+
140
+ - **Workshop:** VFS integrity check, project metadata migration
141
+ - **Venue:** Tenant schema validation, default entity seeding
142
+ - **Academy:** Course-specific index warming
143
+
144
+ The `onInit` callback runs before the instance is added to the cache. If it throws,
145
+ the init fails, the instance is not cached, and the caller receives the error. A
146
+ subsequent request for the same key will retry the full init sequence.
147
+
148
+ ---
149
+
150
+ ## Decision 6: Graceful Shutdown
151
+
152
+ `sdk.shutdown()` calls `pool.shutdown()` which:
153
+ 1. Calls `brain.flush()` on every live instance in parallel
154
+ 2. Clears the cache
155
+ 3. Clears the pending-init map
156
+
157
+ Products must call `sdk.shutdown()` from their `SIGTERM` handler:
158
+
159
+ ```typescript
160
+ process.on('SIGTERM', async () => {
161
+ await sdk.shutdown()
162
+ process.exit(0)
163
+ })
164
+ ```
165
+
166
+ The `fromLicense()` factory also calls `pool.shutdown()` as part of its shutdown
167
+ sequence (billing buffer flush → heartbeat stop → pool flush).
168
+
169
+ ---
170
+
171
+ ## Decision 7: Storage Path Conventions
172
+
173
+ | Strategy | Path |
174
+ |----------|------|
175
+ | `per-user` | `{dataPath}/{emailHash}/{workspaceId}/` |
176
+ | `per-tenant` | `{dataPath}/{tenantSlug}/` |
177
+ | `per-scope` | Determined by caller's `factory` |
178
+
179
+ `emailHash` is an 8-character SHA-256 hex prefix of the lowercase+trimmed email
180
+ address, computed by `computeEmailHash()`. Using a hash rather than the raw email
181
+ avoids filesystem encoding issues and obscures PII from directory listings.
182
+
183
+ All paths are created with `fs.mkdir(path, { recursive: true })` at init time — the
184
+ pool does not require pre-existing directories.
185
+
186
+ ---
187
+
188
+ ## Decision 8: `BrainyInitializingError` for Non-Blocking Cold Start
189
+
190
+ Products that prefer to return HTTP 503 during cold start (rather than blocking the
191
+ client for 25–30 seconds) can pass a non-blocking flag and catch
192
+ `BrainyInitializingError`:
193
+
194
+ ```typescript
195
+ app.onError((err, c) => {
196
+ if (err instanceof BrainyInitializingError) {
197
+ return c.json({ error: 'Service starting', retryAfter: err.retryAfter }, 503, {
198
+ 'Retry-After': String(err.retryAfter),
199
+ })
200
+ }
201
+ })
202
+ ```
203
+
204
+ This is optional — the default pool behaviour blocks the request until init completes,
205
+ which is correct for Workshop (users expect their workspace to load, even if slowly).
206
+ Venue may prefer the 503 pattern to avoid long-hanging HTTP requests on the first
207
+ tenant request after a cold deploy.
208
+
209
+ ---
210
+
211
+ ## Related Documents
212
+
213
+ - `ADR-001-sdk-design.md` — Overall SDK architecture (Decision 3: instance strategies)
214
+ - `ADR-002-transport-protocol.md` — How clients connect to the pool's Brainy instances
215
+ - `src/server/instance-pool.ts` — Full implementation
216
+ - `src/server/from-license.ts` — License activation and `.soulcraft.json` write (step 3 above)
@@ -1,6 +1,6 @@
1
1
  # @soulcraft/sdk — Detailed Implementation Plan
2
2
 
3
- **Status:** Ready to implement
3
+ **Status:** Complete as of v1.4.6
4
4
  **Decisions:** See `ADR-001-sdk-design.md` for all architecture decisions and rationale.
5
5
  **Research basis:** Full cross-project exploration of Workshop, Venue, Academy, brainy-client,
6
6
  and @soulcraft/auth conducted 2026-02-27.
@@ -38,11 +38,6 @@ A new session MUST read the source file(s) before implementing each module.
38
38
 
39
39
  All items done. Repo created, ADR written, scaffold committed.
40
40
 
41
- Remaining scaffolding items (minor, do at start of Phase 2):
42
- - [ ] `vitest.config.ts` — test runner config
43
- - [ ] `.env.example` — document any env vars the SDK needs
44
- - [ ] Update `src/modules/*/types.ts` placeholder comments as modules are implemented
45
-
46
41
  ---
47
42
 
48
43
  ## Phase 2 — Core Data Layer
@@ -634,48 +629,35 @@ renderEmailTemplate(template: string, data: Record<string, unknown>): string
634
629
 
635
630
  ## Phase 6 — npm Publish + Migrate All Products
636
631
 
637
- ### 6a. Pre-publish checklist
638
- - [ ] All tests passing (`bun test`)
639
- - [ ] TypeScript builds cleanly (`bun run check`)
640
- - [ ] No stubs, no TODOs, no `as any`
641
- - [ ] JSDoc on every exported symbol
642
- - [ ] Module-level `@module` block on every file
643
- - [ ] `package.json` version set to `1.0.0`
644
- - [ ] `README.md` written with usage examples for all three entry points
632
+ ### 6a. Pre-publish checklist ✅ Complete (v1.4.6)
633
+ - [x] All tests passing (`bun test`) — 330 tests, 21 suites
634
+ - [x] TypeScript builds cleanly (`bun run check`)
635
+ - [x] No stubs, no TODOs, no `as any`
636
+ - [x] JSDoc on every exported symbol
637
+ - [x] Module-level `@module` block on every file
638
+ - [x] Published as `@soulcraft/sdk@1.4.6` (first published as `1.0.0`, now at `1.4.6`)
639
+ - [x] `README.md` written with usage examples for all three entry points
645
640
 
646
641
  ### 6b. Publish
647
642
  ```bash
648
643
  npm publish --access restricted
649
644
  ```
650
645
 
651
- ### 6c. Migrate Workshop
652
- Files to update in `/media/dpsifr/storage/home/Projects/workshop/`:
653
- 1. `package.json` add `@soulcraft/sdk`, remove `@soulcraft/brainy-client`, `@soulcraft/auth`
654
- 2. `build.sh` — add `sdk` to bundle loop, remove `brainy-client` and `auth`
655
- 3. `auth/better-auth.ts` replace with `createProductAuth('workshop', ...)` from SDK
656
- 4. `server-hono.ts` replace brainy-client handler factories with SDK handlers
657
- 5. `brainy/brainy-singleton.ts` — replace with `sdk.server.instancePool`
658
- 6. `brainy/remote-brainy.ts` — replace with SDK `WsTransport`
659
- 7. `brainy/usage-tracking.ts` replace with `sdk.license.credits.*` + `sdk.billing.*`
660
- 8. All `import { ... } from '@soulcraft/auth'` → `from '@soulcraft/sdk'`
661
- 9. All `import { createBrainyClient } from '@soulcraft/brainy-client'` → `from '@soulcraft/sdk/client'`
662
-
663
- ### 6d. Migrate Venue
664
- Files to update in `/home/dpsifr/Projects/venue/`:
665
- 1. `apps/web/package.json` — same package swap
666
- 2. `apps/web/server.ts` — replace brainy-client WS handler with SDK
667
- 3. `apps/web/src/lib/server/auth.ts` — replace with `createProductAuth('venue', ...)`
668
- 4. `apps/web/src/routes/api/brainy/rpc/+server.ts` — replace with SDK HTTP handler
669
- 5. All notification code → `sdk.notifications.*`
670
-
671
- ### 6e. Migrate Academy
672
- Files to update in `/media/dpsifr/storage/home/Projects/academy/`:
673
- 1. `package.json`, `auth/better-auth.ts`, `server.ts` — same patterns as Workshop
646
+ ### 6c. Migrate Workshop ✅ Complete
647
+ Workshop migrated to `@soulcraft/sdk@1.4.6`. Uses `fromLicense({ product: 'workshop' })`.
648
+ All brainy-client and auth imports replaced. Bundle loop updated.
649
+
650
+ ### 6d. Migrate Venue In progress
651
+ Venue has open actions in the handoff file `fromLicense()` migration pending.
652
+ See `/home/dpsifr/.strategy/PLATFORM-HANDOFF.md`.
653
+
654
+ ### 6e. Migrate Academy Complete
655
+ Academy migrated to `@soulcraft/sdk@1.4.6`.
674
656
 
675
657
  ### 6f. Post-migration
676
- - Deprecate `@soulcraft/brainy-client` on npmjs.com (`npm deprecate`)
677
- - Deprecate `@soulcraft/auth` on npmjs.com
678
- - Update `PLATFORM-HANDOFF.md` thread to RESOLVED
658
+ - `@soulcraft/brainy-client` — deprecated on npmjs.com
659
+ - `@soulcraft/auth` — deprecated on npmjs.com
660
+ - Handoff thread: resolved for Workshop and Academy; Venue still pending
679
661
 
680
662
  ---
681
663