@soulcraft/sdk 2.0.0 → 2.0.2
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/client/index.d.ts +5 -38
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -47
- package/dist/client/index.js.map +1 -1
- package/dist/client/namespace-proxy.d.ts +3 -4
- package/dist/client/namespace-proxy.d.ts.map +1 -1
- package/dist/client/namespace-proxy.js +3 -4
- package/dist/client/namespace-proxy.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/modules/hall/browser.d.ts +83 -27
- package/dist/modules/hall/browser.d.ts.map +1 -1
- package/dist/modules/hall/browser.js +238 -49
- package/dist/modules/hall/browser.js.map +1 -1
- package/dist/modules/hall/media.d.ts +164 -0
- package/dist/modules/hall/media.d.ts.map +1 -0
- package/dist/modules/hall/media.js +182 -0
- package/dist/modules/hall/media.js.map +1 -0
- package/dist/modules/hall/server.d.ts +83 -6
- package/dist/modules/hall/server.d.ts.map +1 -1
- package/dist/modules/hall/server.js +206 -9
- package/dist/modules/hall/server.js.map +1 -1
- package/dist/modules/hall/types.d.ts +548 -25
- package/dist/modules/hall/types.d.ts.map +1 -1
- package/dist/modules/hall/types.js +12 -7
- package/dist/modules/hall/types.js.map +1 -1
- package/dist/server/hall-handlers.d.ts +40 -12
- package/dist/server/hall-handlers.d.ts.map +1 -1
- package/dist/server/hall-handlers.js +40 -12
- package/dist/server/hall-handlers.js.map +1 -1
- package/dist/server/handlers/chat/engine.d.ts.map +1 -1
- package/dist/server/handlers/chat/engine.js +5 -1
- package/dist/server/handlers/chat/engine.js.map +1 -1
- package/dist/server/handlers/chat/types.d.ts +17 -2
- package/dist/server/handlers/chat/types.d.ts.map +1 -1
- package/dist/server/hono-router.d.ts +2 -9
- package/dist/server/hono-router.d.ts.map +1 -1
- package/dist/server/hono-router.js +2 -46
- package/dist/server/hono-router.js.map +1 -1
- package/dist/server/index.d.ts +4 -19
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +10 -29
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +2 -41
- package/dist/types.d.ts.map +1 -1
- package/docs/ADR-005-hall-integration.md +449 -0
- package/package.json +1 -1
- package/dist/client/create-client-sdk.d.ts +0 -113
- package/dist/client/create-client-sdk.d.ts.map +0 -1
- package/dist/client/create-client-sdk.js +0 -169
- package/dist/client/create-client-sdk.js.map +0 -1
- package/dist/modules/app-context/index.d.ts +0 -214
- package/dist/modules/app-context/index.d.ts.map +0 -1
- package/dist/modules/app-context/index.js +0 -569
- package/dist/modules/app-context/index.js.map +0 -1
- package/dist/modules/billing/firestore-provider.d.ts +0 -60
- package/dist/modules/billing/firestore-provider.d.ts.map +0 -1
- package/dist/modules/billing/firestore-provider.js +0 -315
- package/dist/modules/billing/firestore-provider.js.map +0 -1
- package/dist/modules/brainy/proxy.d.ts +0 -48
- package/dist/modules/brainy/proxy.d.ts.map +0 -1
- package/dist/modules/brainy/proxy.js +0 -95
- package/dist/modules/brainy/proxy.js.map +0 -1
- package/dist/server/create-sdk.d.ts +0 -74
- package/dist/server/create-sdk.d.ts.map +0 -1
- package/dist/server/create-sdk.js +0 -104
- package/dist/server/create-sdk.js.map +0 -1
- package/dist/server/from-license.d.ts +0 -252
- package/dist/server/from-license.d.ts.map +0 -1
- package/dist/server/from-license.js +0 -349
- package/dist/server/from-license.js.map +0 -1
- package/dist/server/handlers.d.ts +0 -312
- package/dist/server/handlers.d.ts.map +0 -1
- package/dist/server/handlers.js +0 -376
- package/dist/server/handlers.js.map +0 -1
- package/dist/server/postmessage-handler.d.ts +0 -152
- package/dist/server/postmessage-handler.d.ts.map +0 -1
- package/dist/server/postmessage-handler.js +0 -138
- package/dist/server/postmessage-handler.js.map +0 -1
- package/dist/transports/http.d.ts +0 -86
- package/dist/transports/http.d.ts.map +0 -1
- package/dist/transports/http.js +0 -137
- package/dist/transports/http.js.map +0 -1
- package/dist/transports/postmessage.d.ts +0 -159
- package/dist/transports/postmessage.d.ts.map +0 -1
- package/dist/transports/postmessage.js +0 -207
- package/dist/transports/postmessage.js.map +0 -1
- package/dist/transports/workshop.d.ts +0 -173
- package/dist/transports/workshop.d.ts.map +0 -1
- package/dist/transports/workshop.js +0 -307
- package/dist/transports/workshop.js.map +0 -1
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module server/from-license
|
|
3
|
-
* @description License-driven server SDK factory.
|
|
4
|
-
*
|
|
5
|
-
* `createServerSDK.fromLicense({ product })` is the recommended production entry point
|
|
6
|
-
* for product backends. It replaces the manual environment-variable wiring required by
|
|
7
|
-
* `createSDK()` with a single boot-time call to Portal:
|
|
8
|
-
*
|
|
9
|
-
* ```
|
|
10
|
-
* SOULCRAFT_LICENSE_KEY=SC-XXXX-XXXX-XXXX ← only secret needed
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* At startup the factory:
|
|
14
|
-
* 1. Reads `SOULCRAFT_LICENSE_KEY` from the environment.
|
|
15
|
-
* 2. POSTs `POST https://soulcraft.com/api/license/activate` with the key and product name.
|
|
16
|
-
* 3. Receives a config bundle (`anthropicApiKey`, `portalCreditSecret`, `hallSecret`, etc.).
|
|
17
|
-
* 4. Injects the bundle values into `process.env` so all SDK module factories pick them
|
|
18
|
-
* up automatically — no factory API changes required.
|
|
19
|
-
* 5. Writes the bundle to `.soulcraft-config.json` in the working directory for offline
|
|
20
|
-
* restarts (e.g. if Portal is temporarily unreachable after a product restart).
|
|
21
|
-
* 6. Schedules a background re-validation every 4 hours to pick up rotated secrets.
|
|
22
|
-
*
|
|
23
|
-
* The returned `ServerSDK` object is long-lived (one per process). Its `createSDK(brain)`
|
|
24
|
-
* method returns a per-request `SoulcraftSDK`, identical to calling `createSDK({ brain })`
|
|
25
|
-
* directly, but with all secrets already wired from the bundle.
|
|
26
|
-
*
|
|
27
|
-
* ## Backward compatibility
|
|
28
|
-
*
|
|
29
|
-
* `createSDK()` is unchanged — products that manage their own env vars continue to work
|
|
30
|
-
* exactly as before. `fromLicense()` is additive.
|
|
31
|
-
*
|
|
32
|
-
* ## Offline / degraded start
|
|
33
|
-
*
|
|
34
|
-
* If Portal is unreachable on first boot, the factory falls back to a cached bundle from
|
|
35
|
-
* `.soulcraft-config.json`. If no cache exists either, it throws — a product cannot start
|
|
36
|
-
* in production without a resolved config bundle.
|
|
37
|
-
*
|
|
38
|
-
* @example Academy startup
|
|
39
|
-
* ```typescript
|
|
40
|
-
* import { createServerSDK, BrainyInstancePool } from '@soulcraft/sdk/server'
|
|
41
|
-
*
|
|
42
|
-
* const server = await createServerSDK.fromLicense({ product: 'academy' })
|
|
43
|
-
*
|
|
44
|
-
* const pool = new BrainyInstancePool({
|
|
45
|
-
* storage: 'mmap-filesystem',
|
|
46
|
-
* dataPath: '/mnt/brainy-data',
|
|
47
|
-
* strategy: 'per-user',
|
|
48
|
-
* })
|
|
49
|
-
*
|
|
50
|
-
* // Per-request:
|
|
51
|
-
* app.get('/api/brainy', requireAuth, async (c) => {
|
|
52
|
-
* const brain = await pool.forUser(user.emailHash, 'main')
|
|
53
|
-
* const sdk = server.createSDK(brain)
|
|
54
|
-
* const items = await sdk.brainy.find({ query: 'candle inventory' })
|
|
55
|
-
* return c.json({ items })
|
|
56
|
-
* })
|
|
57
|
-
*
|
|
58
|
-
* // On shutdown:
|
|
59
|
-
* process.on('SIGTERM', () => server.shutdown())
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
63
|
-
import { resolve } from 'node:path';
|
|
64
|
-
import { createSDK } from './create-sdk.js';
|
|
65
|
-
import { createHallModule } from './hall-handlers.js';
|
|
66
|
-
import { SDK_VERSION } from '../version.js';
|
|
67
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
-
// Constants
|
|
69
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
-
/** Portal license activation endpoint. */
|
|
71
|
-
const ACTIVATE_URL = 'https://soulcraft.com/api/license/activate';
|
|
72
|
-
/** Re-validation interval: 4 hours. */
|
|
73
|
-
const REVALIDATE_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
74
|
-
/**
|
|
75
|
-
* Returns the path to the cached config bundle.
|
|
76
|
-
*
|
|
77
|
-
* Resolved lazily at call-time so the path is always relative to the working
|
|
78
|
-
* directory at the time of the call, not at module load time.
|
|
79
|
-
*/
|
|
80
|
-
function getCachePath() {
|
|
81
|
-
return resolve(process.cwd(), '.soulcraft-config.json');
|
|
82
|
-
}
|
|
83
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
-
// Internal helpers
|
|
85
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
-
/**
|
|
87
|
-
* Call the Portal `licenseActivate` endpoint and return the full response.
|
|
88
|
-
*
|
|
89
|
-
* @throws {Error} If the network call fails or Portal returns a non-200 status.
|
|
90
|
-
*/
|
|
91
|
-
async function _activate(key, product) {
|
|
92
|
-
const response = await fetch(ACTIVATE_URL, {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
headers: { 'Content-Type': 'application/json' },
|
|
95
|
-
body: JSON.stringify({ key, product, sdkVersion: SDK_VERSION }),
|
|
96
|
-
signal: AbortSignal.timeout(15_000),
|
|
97
|
-
});
|
|
98
|
-
if (response.status === 403) {
|
|
99
|
-
const body = (await response.json().catch(() => ({})));
|
|
100
|
-
throw new Error(`License key rejected by Portal: ${body.reason ?? 'key_not_found'}. ` +
|
|
101
|
-
`Check SOULCRAFT_LICENSE_KEY is correct and the license has not expired.`);
|
|
102
|
-
}
|
|
103
|
-
if (!response.ok) {
|
|
104
|
-
throw new Error(`Portal license activation failed (HTTP ${response.status}). ` +
|
|
105
|
-
`The service may be temporarily unavailable.`);
|
|
106
|
-
}
|
|
107
|
-
return response.json();
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Read the cached bundle from `.soulcraft-config.json`.
|
|
111
|
-
* Returns `undefined` if the file does not exist or cannot be parsed.
|
|
112
|
-
*/
|
|
113
|
-
function _readCache() {
|
|
114
|
-
try {
|
|
115
|
-
const raw = readFileSync(getCachePath(), 'utf8');
|
|
116
|
-
return JSON.parse(raw);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Write the activation response to `.soulcraft-config.json`.
|
|
124
|
-
* Failures are non-fatal — if the disk is read-only, the cache is simply not written.
|
|
125
|
-
*/
|
|
126
|
-
function _writeCache(product, response) {
|
|
127
|
-
try {
|
|
128
|
-
const bundle = {
|
|
129
|
-
activatedAt: new Date().toISOString(),
|
|
130
|
-
product,
|
|
131
|
-
response,
|
|
132
|
-
};
|
|
133
|
-
writeFileSync(getCachePath(), JSON.stringify(bundle, null, 2), 'utf8');
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// Non-fatal — read-only FS or permission error.
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Inject config bundle values into `process.env`.
|
|
141
|
-
*
|
|
142
|
-
* Only sets variables that are present in the bundle and not already set by the host
|
|
143
|
-
* environment. Host-set values take precedence, allowing per-deployment overrides.
|
|
144
|
-
*/
|
|
145
|
-
function _injectEnv(config) {
|
|
146
|
-
const mappings = [
|
|
147
|
-
['anthropicApiKey', 'ANTHROPIC_API_KEY'],
|
|
148
|
-
['portalCreditSecret', 'PORTAL_CREDIT_SECRET'],
|
|
149
|
-
['hallUrl', 'HALL_URL'],
|
|
150
|
-
['stripeSecretKey', 'STRIPE_SECRET_KEY'],
|
|
151
|
-
['stripeWebhookSecret', 'STRIPE_WEBHOOK_SECRET'],
|
|
152
|
-
['workshopDeploySecret', 'WORKSHOP_DEPLOY_SECRET'],
|
|
153
|
-
['academyPublishSecret', 'ACADEMY_PUBLISH_SECRET'],
|
|
154
|
-
['venueRpcSecret', 'VENUE_RPC_SECRET'],
|
|
155
|
-
['postmarkToken', 'POSTMARK_SERVER_TOKEN'],
|
|
156
|
-
['postmarkFromEmail', 'POSTMARK_FROM_EMAIL'],
|
|
157
|
-
['twilioSid', 'TWILIO_ACCOUNT_SID'],
|
|
158
|
-
['twilioToken', 'TWILIO_AUTH_TOKEN'],
|
|
159
|
-
['twilioFromNumber', 'TWILIO_FROM_NUMBER'],
|
|
160
|
-
];
|
|
161
|
-
for (const [configKey, envKey] of mappings) {
|
|
162
|
-
const value = config[configKey];
|
|
163
|
-
if (value && !process.env[envKey]) {
|
|
164
|
-
process.env[envKey] = value;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Write a Cortex license key to `.soulcraft.json` in the working directory.
|
|
170
|
-
*
|
|
171
|
-
* Uses an atomic tmp-then-rename write so a concurrent Cortex process never
|
|
172
|
-
* reads a partial file. Must be called BEFORE `createSDK(brain)` — Cortex
|
|
173
|
-
* reads `.soulcraft.json` during Brainy plugin initialization.
|
|
174
|
-
*
|
|
175
|
-
* Failures are non-fatal: if the filesystem is read-only or permissions are
|
|
176
|
-
* missing, Cortex falls back to its own activation code exchange.
|
|
177
|
-
*
|
|
178
|
-
* @param cortexLicenseKey - The Cortex Ed25519 JWT (`sc_cortex_...`) to persist.
|
|
179
|
-
*/
|
|
180
|
-
function _writeSoulcraftJson(cortexLicenseKey) {
|
|
181
|
-
try {
|
|
182
|
-
const targetPath = resolve(process.cwd(), '.soulcraft.json');
|
|
183
|
-
const tmpPath = targetPath + '.tmp';
|
|
184
|
-
writeFileSync(tmpPath, JSON.stringify({ cortex: cortexLicenseKey }, null, 2) + '\n', 'utf8');
|
|
185
|
-
renameSync(tmpPath, targetPath);
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
// Non-fatal — Cortex exchanges its own activation code independently.
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
-
// fromLicense factory
|
|
193
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
-
/**
|
|
195
|
-
* Activate the SDK from a Soulcraft license key.
|
|
196
|
-
*
|
|
197
|
-
* Calls Portal's `licenseActivate` endpoint, injects the returned config bundle
|
|
198
|
-
* into `process.env`, and returns a `ServerSDK` ready for per-request use.
|
|
199
|
-
*
|
|
200
|
-
* Falls back to a cached bundle (`.soulcraft-config.json`) if Portal is unreachable.
|
|
201
|
-
* Throws if neither Portal nor a valid cache is available.
|
|
202
|
-
*
|
|
203
|
-
* @param options - Product name and optional license key override.
|
|
204
|
-
* @returns A `ServerSDK` instance, fully wired from the license bundle.
|
|
205
|
-
*
|
|
206
|
-
* @throws {Error} If the license key is invalid, expired, or Portal is unreachable with no cache.
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```typescript
|
|
210
|
-
* const server = await createServerSDK.fromLicense({ product: 'academy' })
|
|
211
|
-
* const pool = new BrainyInstancePool({ storage: 'mmap-filesystem', dataPath: '/mnt/brainy-data', strategy: 'per-user' })
|
|
212
|
-
*
|
|
213
|
-
* app.use(requireAuth)
|
|
214
|
-
* app.get('/api/brainy', async (c) => {
|
|
215
|
-
* const brain = await pool.forUser(c.get('user').emailHash, 'main')
|
|
216
|
-
* const sdk = server.createSDK(brain)
|
|
217
|
-
* return c.json(await sdk.brainy.find({ query: c.req.query('q') ?? '' }))
|
|
218
|
-
* })
|
|
219
|
-
*
|
|
220
|
-
* process.on('SIGTERM', () => server.shutdown())
|
|
221
|
-
* ```
|
|
222
|
-
*/
|
|
223
|
-
async function fromLicense(options) {
|
|
224
|
-
const { product } = options;
|
|
225
|
-
const key = options.licenseKey ?? process.env['SOULCRAFT_LICENSE_KEY'];
|
|
226
|
-
if (!key) {
|
|
227
|
-
throw new Error('SOULCRAFT_LICENSE_KEY is not set. ' +
|
|
228
|
-
'Set this environment variable to your Soulcraft license key, ' +
|
|
229
|
-
'or pass licenseKey explicitly to fromLicense().');
|
|
230
|
-
}
|
|
231
|
-
// ── Activate ──────────────────────────────────────────────────────────────
|
|
232
|
-
let activateResponse;
|
|
233
|
-
try {
|
|
234
|
-
activateResponse = await _activate(key, product);
|
|
235
|
-
_writeCache(product, activateResponse);
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
// Portal unreachable — try the cached bundle.
|
|
239
|
-
const cached = _readCache();
|
|
240
|
-
if (cached?.product === product && cached.response.valid) {
|
|
241
|
-
console.warn(`[SDK] Portal unreachable (${err instanceof Error ? err.message : String(err)}). ` +
|
|
242
|
-
`Using cached config from ${cached.activatedAt}.`);
|
|
243
|
-
activateResponse = cached.response;
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
throw new Error(`Failed to activate SDK license and no valid cache found. ` +
|
|
247
|
-
`Original error: ${err instanceof Error ? err.message : String(err)}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (!activateResponse.valid) {
|
|
251
|
-
throw new Error(`License key is not valid: ${activateResponse.reason ?? 'unknown reason'}. ` +
|
|
252
|
-
`Check that your license has not expired.`);
|
|
253
|
-
}
|
|
254
|
-
const config = activateResponse.config;
|
|
255
|
-
const tier = activateResponse.tier ?? 'free';
|
|
256
|
-
let venueOrgConfig = activateResponse.venueOrgConfig ?? {};
|
|
257
|
-
// ── Inject env ────────────────────────────────────────────────────────────
|
|
258
|
-
_injectEnv(config);
|
|
259
|
-
// ── Write Cortex license to .soulcraft.json ───────────────────────────────
|
|
260
|
-
// Must run before createSDK(brain) — Cortex reads .soulcraft.json at Brainy
|
|
261
|
-
// plugin initialization time, not at first use.
|
|
262
|
-
if (config.cortexLicenseKey) {
|
|
263
|
-
_writeSoulcraftJson(config.cortexLicenseKey);
|
|
264
|
-
}
|
|
265
|
-
// ── Wire Hall ─────────────────────────────────────────────────────────────
|
|
266
|
-
let hall;
|
|
267
|
-
if (config.hallSecret && config.hallUrl) {
|
|
268
|
-
hall = createHallModule({
|
|
269
|
-
url: config.hallUrl,
|
|
270
|
-
productName: product,
|
|
271
|
-
secret: config.hallSecret,
|
|
272
|
-
});
|
|
273
|
-
await hall.connect();
|
|
274
|
-
}
|
|
275
|
-
// ── Background re-validation ──────────────────────────────────────────────
|
|
276
|
-
let _revalidateTimer;
|
|
277
|
-
async function _revalidate() {
|
|
278
|
-
try {
|
|
279
|
-
const fresh = await _activate(key, product);
|
|
280
|
-
_writeCache(product, fresh);
|
|
281
|
-
_injectEnv(fresh.config);
|
|
282
|
-
if (fresh.config.cortexLicenseKey) {
|
|
283
|
-
_writeSoulcraftJson(fresh.config.cortexLicenseKey);
|
|
284
|
-
}
|
|
285
|
-
venueOrgConfig = fresh.venueOrgConfig ?? {};
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// Non-fatal — the in-memory injected values remain valid until next attempt.
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
_revalidateTimer = setInterval(() => { void _revalidate(); }, REVALIDATE_INTERVAL_MS);
|
|
292
|
-
if (_revalidateTimer.unref)
|
|
293
|
-
_revalidateTimer.unref();
|
|
294
|
-
// ── Return ServerSDK ──────────────────────────────────────────────────────
|
|
295
|
-
return {
|
|
296
|
-
createSDK(brain) {
|
|
297
|
-
return createSDK({ brain });
|
|
298
|
-
},
|
|
299
|
-
get hall() {
|
|
300
|
-
return hall;
|
|
301
|
-
},
|
|
302
|
-
get config() {
|
|
303
|
-
return config;
|
|
304
|
-
},
|
|
305
|
-
get tier() {
|
|
306
|
-
return tier;
|
|
307
|
-
},
|
|
308
|
-
get venueOrgConfig() {
|
|
309
|
-
return venueOrgConfig;
|
|
310
|
-
},
|
|
311
|
-
shutdown() {
|
|
312
|
-
if (_revalidateTimer) {
|
|
313
|
-
clearInterval(_revalidateTimer);
|
|
314
|
-
_revalidateTimer = undefined;
|
|
315
|
-
}
|
|
316
|
-
if (hall) {
|
|
317
|
-
void hall.close();
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
323
|
-
// createServerSDK namespace
|
|
324
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
325
|
-
/**
|
|
326
|
-
* Server SDK factory namespace.
|
|
327
|
-
*
|
|
328
|
-
* The primary entry point is `createServerSDK.fromLicense({ product })`.
|
|
329
|
-
* For lower-level control (custom Brainy instance, no license key), use
|
|
330
|
-
* `createSDK({ brain })` directly.
|
|
331
|
-
*
|
|
332
|
-
* @example
|
|
333
|
-
* ```typescript
|
|
334
|
-
* import { createServerSDK, BrainyInstancePool } from '@soulcraft/sdk/server'
|
|
335
|
-
*
|
|
336
|
-
* const server = await createServerSDK.fromLicense({ product: 'workshop' })
|
|
337
|
-
* const pool = new BrainyInstancePool({ storage: 'mmap-filesystem', dataPath: '/mnt/data', strategy: 'per-user' })
|
|
338
|
-
*
|
|
339
|
-
* app.get('/api/brainy', requireAuth, async (c) => {
|
|
340
|
-
* const brain = await pool.forUser(c.get('user').emailHash, 'main')
|
|
341
|
-
* const sdk = server.createSDK(brain)
|
|
342
|
-
* return c.json(await sdk.brainy.find({ query: c.req.query('q') ?? '' }))
|
|
343
|
-
* })
|
|
344
|
-
* ```
|
|
345
|
-
*/
|
|
346
|
-
export const createServerSDK = {
|
|
347
|
-
fromLicense,
|
|
348
|
-
};
|
|
349
|
-
//# sourceMappingURL=from-license.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"from-license.js","sourceRoot":"","sources":["../../src/server/from-license.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module server/handlers
|
|
3
|
-
* @description HTTP RPC and WebSocket handler factories for mounting Brainy server endpoints.
|
|
4
|
-
*
|
|
5
|
-
* Products mount exactly two routes:
|
|
6
|
-
* - `POST /api/brainy/rpc` — handled by {@link createBrainyHandler}
|
|
7
|
-
* - `GET /api/brainy/ws` — WebSocket upgrade handled by {@link createBrainyWsHandler}
|
|
8
|
-
*
|
|
9
|
-
* Both factories are framework-agnostic — they accept and return `Request` / `Response`
|
|
10
|
-
* objects (Fetch API), making them compatible with SvelteKit, Hono, and raw Bun.serve.
|
|
11
|
-
*
|
|
12
|
-
* ## HTTP RPC handler
|
|
13
|
-
*
|
|
14
|
-
* Accepts `{ method: string, args: unknown[] }` as a JSON body and returns
|
|
15
|
-
* `{ result: unknown }` or `{ error: { code, message } }`. All authentication and
|
|
16
|
-
* authorization is delegated to the caller-supplied callbacks.
|
|
17
|
-
*
|
|
18
|
-
* ## WebSocket handler
|
|
19
|
-
*
|
|
20
|
-
* Accepts binary MessagePack frames. See wire protocol in `transports/ws.ts`.
|
|
21
|
-
* The `broadcastChange()` method on the returned handler pushes `BrainyChangeEvent`
|
|
22
|
-
* objects to connected clients.
|
|
23
|
-
*
|
|
24
|
-
* @example SvelteKit route mounting the HTTP handler
|
|
25
|
-
* ```typescript
|
|
26
|
-
* // src/routes/api/brainy/rpc/+server.ts
|
|
27
|
-
* import { createBrainyHandler } from '@soulcraft/sdk/server'
|
|
28
|
-
* import { pool } from '$lib/server/sdk'
|
|
29
|
-
* import { verifySession } from '$lib/server/auth'
|
|
30
|
-
*
|
|
31
|
-
* const handler = createBrainyHandler({
|
|
32
|
-
* resolveBrain: async (req) => {
|
|
33
|
-
* const userId = req.headers.get('x-user-id') ?? ''
|
|
34
|
-
* const workspaceId = req.headers.get('x-workspace-id') ?? ''
|
|
35
|
-
* return pool.forUser(userId, workspaceId)
|
|
36
|
-
* },
|
|
37
|
-
* authenticate: async (req) => verifySession(req.headers.get('cookie') ?? ''),
|
|
38
|
-
* })
|
|
39
|
-
*
|
|
40
|
-
* export const POST: RequestHandler = ({ request }) => handler(request)
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
|
-
* @example Bun.serve WebSocket upgrade
|
|
44
|
-
* ```typescript
|
|
45
|
-
* import { createBrainyWsHandler } from '@soulcraft/sdk/server'
|
|
46
|
-
* import { pool } from './sdk'
|
|
47
|
-
* import { verifyCapabilityToken } from '@soulcraft/sdk'
|
|
48
|
-
*
|
|
49
|
-
* const wsHandler = createBrainyWsHandler({
|
|
50
|
-
* resolveBrain: (scope) => pool.forTenant(scope),
|
|
51
|
-
* authenticate: (token, scope) => verifyCapabilityToken(token, process.env.VENUE_RPC_SECRET!),
|
|
52
|
-
* })
|
|
53
|
-
*
|
|
54
|
-
* Bun.serve({
|
|
55
|
-
* async fetch(req, server) {
|
|
56
|
-
* if (req.url.endsWith('/api/brainy/ws')) {
|
|
57
|
-
* const session = await wsHandler.handleUpgrade(req)
|
|
58
|
-
* if (!session) return new Response('Unauthorized', { status: 401 })
|
|
59
|
-
* server.upgrade(req, { data: session })
|
|
60
|
-
* return undefined
|
|
61
|
-
* }
|
|
62
|
-
* return new Response('Not found', { status: 404 })
|
|
63
|
-
* },
|
|
64
|
-
* websocket: {
|
|
65
|
-
* async message(ws, data) {
|
|
66
|
-
* await wsHandler.handleMessage(ws.data, data as ArrayBuffer, (msg) => ws.send(msg))
|
|
67
|
-
* },
|
|
68
|
-
* },
|
|
69
|
-
* })
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
import type { Brainy } from '@soulcraft/brainy';
|
|
73
|
-
import { LocalTransport } from '../transports/local.js';
|
|
74
|
-
import type { BrainyChangeEvent } from '../modules/brainy/events.js';
|
|
75
|
-
import { YDocManager } from '../modules/brainy/ydoc-manager.js';
|
|
76
|
-
/**
|
|
77
|
-
* Configuration for {@link createBrainyHandler}.
|
|
78
|
-
*/
|
|
79
|
-
export interface BrainyHandlerConfig {
|
|
80
|
-
/**
|
|
81
|
-
* Resolves the Brainy instance to dispatch this request against.
|
|
82
|
-
*
|
|
83
|
-
* Called on every request. Use request headers (org slug, workspace ID, etc.)
|
|
84
|
-
* to select the correct instance from the pool.
|
|
85
|
-
*
|
|
86
|
-
* @param request - The incoming HTTP Request.
|
|
87
|
-
* @returns The Brainy instance for this request's scope.
|
|
88
|
-
*/
|
|
89
|
-
resolveBrain: (request: Request) => Promise<Brainy>;
|
|
90
|
-
/**
|
|
91
|
-
* Authenticates the incoming request.
|
|
92
|
-
*
|
|
93
|
-
* Return any truthy value (claims, session object, etc.) on success.
|
|
94
|
-
* Return `null` or `undefined` to reject the request with HTTP 401.
|
|
95
|
-
*
|
|
96
|
-
* @param request - The incoming HTTP Request.
|
|
97
|
-
* @returns An auth context on success, or `null` to reject.
|
|
98
|
-
*/
|
|
99
|
-
authenticate: (request: Request) => Promise<unknown | null>;
|
|
100
|
-
/**
|
|
101
|
-
* Optional per-call authorization check.
|
|
102
|
-
*
|
|
103
|
-
* Called after `authenticate()` succeeds. Return `false` or `null` to
|
|
104
|
-
* reject with HTTP 403.
|
|
105
|
-
*
|
|
106
|
-
* @param method - Dot-separated Brainy method name.
|
|
107
|
-
* @param args - Positional arguments from the request body.
|
|
108
|
-
* @param auth - The auth context returned by `authenticate()`.
|
|
109
|
-
* @returns `true` to allow, `false` / `null` to deny.
|
|
110
|
-
*/
|
|
111
|
-
authorize?: (method: string, args: unknown[], auth: unknown) => boolean | null | Promise<boolean | null>;
|
|
112
|
-
/**
|
|
113
|
-
* Methods that are blocked for all callers regardless of auth.
|
|
114
|
-
*
|
|
115
|
-
* @default []
|
|
116
|
-
*/
|
|
117
|
-
blockedMethods?: string[];
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Creates a Fetch-API-compatible HTTP handler for Brainy RPC calls.
|
|
121
|
-
*
|
|
122
|
-
* Mount at `POST /api/brainy/rpc` in your product's router.
|
|
123
|
-
*
|
|
124
|
-
* @param config - Handler configuration.
|
|
125
|
-
* @returns `(request: Request) => Promise<Response>`
|
|
126
|
-
*/
|
|
127
|
-
export declare function createBrainyHandler(config: BrainyHandlerConfig): (request: Request) => Promise<Response>;
|
|
128
|
-
/**
|
|
129
|
-
* Configuration for {@link createBrainyWsHandler}.
|
|
130
|
-
*/
|
|
131
|
-
export interface BrainyWsHandlerConfig {
|
|
132
|
-
/**
|
|
133
|
-
* Resolves the Brainy instance for the given scope (e.g. tenant slug).
|
|
134
|
-
*
|
|
135
|
-
* @param scope - The `?scope=` query param from the WebSocket URL.
|
|
136
|
-
* @returns The Brainy instance for this scope.
|
|
137
|
-
*/
|
|
138
|
-
resolveBrain: (scope: string) => Promise<Brainy>;
|
|
139
|
-
/**
|
|
140
|
-
* Authenticates the WebSocket upgrade request.
|
|
141
|
-
*
|
|
142
|
-
* The `token` is extracted from `Authorization: Bearer <token>` (Bun/Node) or
|
|
143
|
-
* `?token=` query param (browser). Return `null` to reject with close code 4001.
|
|
144
|
-
*
|
|
145
|
-
* @param token - The bearer token from the header or query string.
|
|
146
|
-
* @param scope - The requested scope from the URL.
|
|
147
|
-
* @returns An auth context on success, or `null` to reject.
|
|
148
|
-
*/
|
|
149
|
-
authenticate: (token: string, scope: string) => Promise<unknown | null>;
|
|
150
|
-
/**
|
|
151
|
-
* Optional per-call authorization check for Brainy RPC methods.
|
|
152
|
-
*
|
|
153
|
-
* @param method - Dot-separated Brainy method name.
|
|
154
|
-
* @param args - Positional arguments.
|
|
155
|
-
* @param auth - The auth context from `authenticate()`.
|
|
156
|
-
* @returns `true` to allow, `false` / `null` to deny.
|
|
157
|
-
*/
|
|
158
|
-
authorize?: (method: string, args: unknown[], auth: unknown) => boolean | null | Promise<boolean | null>;
|
|
159
|
-
/** Methods blocked for all callers. @default [] */
|
|
160
|
-
blockedMethods?: string[];
|
|
161
|
-
/**
|
|
162
|
-
* Optional Y.js document manager for real-time collaborative editing.
|
|
163
|
-
*
|
|
164
|
-
* When provided, the handler routes `y-sync`, `y-update`, and `y-awareness`
|
|
165
|
-
* messages through the `YDocManager` instead of the Brainy RPC dispatcher.
|
|
166
|
-
* Y.js update broadcasts are sent to all other peers via the `broadcast` callback
|
|
167
|
-
* on the session — the product server must populate this callback when a peer
|
|
168
|
-
* connects (see {@link WsSession.broadcast}).
|
|
169
|
-
*/
|
|
170
|
-
ydocManager?: YDocManager;
|
|
171
|
-
}
|
|
172
|
-
/** Per-connection session state stored on the WebSocket data field. */
|
|
173
|
-
export interface WsSession {
|
|
174
|
-
/** The tenant scope extracted from `?scope=`. */
|
|
175
|
-
scope: string;
|
|
176
|
-
/** The auth context returned by `authenticate()`. */
|
|
177
|
-
auth: unknown;
|
|
178
|
-
/** The resolved Brainy instance for this scope. */
|
|
179
|
-
brain: Brainy;
|
|
180
|
-
/** Local transport wrapping the Brainy instance for this session. */
|
|
181
|
-
transport: LocalTransport;
|
|
182
|
-
/**
|
|
183
|
-
* Unique peer ID for this connection. Used by `YDocManager` to track per-file
|
|
184
|
-
* peer counts so it knows when the last peer has disconnected.
|
|
185
|
-
*/
|
|
186
|
-
peerId: string;
|
|
187
|
-
/**
|
|
188
|
-
* Broadcast callback set by the product server after upgrade.
|
|
189
|
-
*
|
|
190
|
-
* Called by the handler to fan out `y-update` and `y-awareness` messages to all
|
|
191
|
-
* other peers connected to this scope. The product server owns the connection map
|
|
192
|
-
* (keyed by scope) and provides this callback so the SDK doesn't need to manage
|
|
193
|
-
* WebSocket handles directly.
|
|
194
|
-
*
|
|
195
|
-
* @param msg - Encoded MessagePack frame to send to all OTHER peers in this scope.
|
|
196
|
-
* @param excludePeerId - The peer that sent the message — must be excluded from broadcast.
|
|
197
|
-
*/
|
|
198
|
-
broadcast: (msg: Uint8Array, excludePeerId: string) => void;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* The object returned by {@link createBrainyWsHandler}.
|
|
202
|
-
*/
|
|
203
|
-
export interface BrainyWsHandler {
|
|
204
|
-
/**
|
|
205
|
-
* Handles a WebSocket upgrade request.
|
|
206
|
-
*
|
|
207
|
-
* Extracts the token and scope from the request, authenticates, and resolves
|
|
208
|
-
* the Brainy instance. Returns `null` if authentication fails (caller should
|
|
209
|
-
* respond with 401 and not proceed with the upgrade).
|
|
210
|
-
*
|
|
211
|
-
* After a successful upgrade the product server must set `session.broadcast` to
|
|
212
|
-
* a function that sends a `Uint8Array` to all other peers in the same scope.
|
|
213
|
-
*
|
|
214
|
-
* @param request - The HTTP upgrade Request.
|
|
215
|
-
* @returns Session state on success, or `null` on auth failure.
|
|
216
|
-
*/
|
|
217
|
-
handleUpgrade(request: Request): Promise<WsSession | null>;
|
|
218
|
-
/**
|
|
219
|
-
* Processes an incoming WebSocket message.
|
|
220
|
-
*
|
|
221
|
-
* Decodes the MessagePack frame and routes it to either:
|
|
222
|
-
* - The Brainy RPC dispatcher (frames with an `id` field), or
|
|
223
|
-
* - The `YDocManager` (frames with `type: 'y-sync' | 'y-update' | 'y-awareness'`).
|
|
224
|
-
*
|
|
225
|
-
* Y.js update and awareness frames are also broadcast to other peers via
|
|
226
|
-
* `session.broadcast`.
|
|
227
|
-
*
|
|
228
|
-
* @param session - The session state from `handleUpgrade()`.
|
|
229
|
-
* @param data - Raw binary message frame.
|
|
230
|
-
* @param send - Callback to send a binary response to THIS client only.
|
|
231
|
-
*/
|
|
232
|
-
handleMessage(session: WsSession, data: ArrayBuffer | Uint8Array, send: (msg: Uint8Array) => void): Promise<void>;
|
|
233
|
-
/**
|
|
234
|
-
* Called when a peer's WebSocket connection closes.
|
|
235
|
-
*
|
|
236
|
-
* Notifies the `YDocManager` so it can flush and discard Y.Docs that no longer
|
|
237
|
-
* have any active peers.
|
|
238
|
-
*
|
|
239
|
-
* @param session - The session state of the disconnecting peer.
|
|
240
|
-
*/
|
|
241
|
-
handleClose(session: WsSession): Promise<void>;
|
|
242
|
-
/**
|
|
243
|
-
* Broadcasts a Brainy change event to a connected client.
|
|
244
|
-
*
|
|
245
|
-
* @param event - The change event to broadcast.
|
|
246
|
-
* @param send - Callback to send the encoded event to the target client.
|
|
247
|
-
*/
|
|
248
|
-
broadcastChange(event: BrainyChangeEvent, send: (msg: Uint8Array) => void): void;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Creates a server-side WebSocket handler for bidirectional Brainy RPC, real-time
|
|
252
|
-
* change push, and optional Y.js collaborative document editing.
|
|
253
|
-
*
|
|
254
|
-
* ## Y.js collaborative editing
|
|
255
|
-
*
|
|
256
|
-
* Pass a `ydocManager` in the config to enable co-editing. The handler will
|
|
257
|
-
* automatically route `y-sync`, `y-update`, and `y-awareness` messages through the
|
|
258
|
-
* manager. The product server must:
|
|
259
|
-
*
|
|
260
|
-
* 1. Maintain a `Map<scope, Map<peerId, sendFn>>` of connected peers.
|
|
261
|
-
* 2. Set `session.broadcast` after `handleUpgrade()` to fan out Y.js frames.
|
|
262
|
-
* 3. Call `handleClose(session)` when a peer disconnects.
|
|
263
|
-
*
|
|
264
|
-
* @example Workshop server wiring with Y.js
|
|
265
|
-
* ```typescript
|
|
266
|
-
* import { createBrainyWsHandler, YDocManager } from '@soulcraft/sdk/server'
|
|
267
|
-
*
|
|
268
|
-
* const ydocManager = new YDocManager()
|
|
269
|
-
* const wsHandler = createBrainyWsHandler({ resolveBrain, authenticate, ydocManager })
|
|
270
|
-
*
|
|
271
|
-
* // Map<scope, Map<peerId, send>>
|
|
272
|
-
* const peers = new Map<string, Map<string, (msg: Uint8Array) => void>>()
|
|
273
|
-
*
|
|
274
|
-
* Bun.serve({
|
|
275
|
-
* async fetch(req, server) {
|
|
276
|
-
* if (new URL(req.url).pathname === '/api/brainy/ws') {
|
|
277
|
-
* const session = await wsHandler.handleUpgrade(req)
|
|
278
|
-
* if (!session) return new Response('Unauthorized', { status: 401 })
|
|
279
|
-
* // Register peer in broadcast map
|
|
280
|
-
* if (!peers.has(session.scope)) peers.set(session.scope, new Map())
|
|
281
|
-
* server.upgrade(req, { data: session })
|
|
282
|
-
* return undefined
|
|
283
|
-
* }
|
|
284
|
-
* },
|
|
285
|
-
* websocket: {
|
|
286
|
-
* open(ws) {
|
|
287
|
-
* const session = ws.data as WsSession
|
|
288
|
-
* peers.get(session.scope)!.set(session.peerId, (msg) => ws.send(msg))
|
|
289
|
-
* session.broadcast = (msg, excludeId) => {
|
|
290
|
-
* for (const [id, send] of peers.get(session.scope) ?? []) {
|
|
291
|
-
* if (id !== excludeId) send(msg)
|
|
292
|
-
* }
|
|
293
|
-
* }
|
|
294
|
-
* ws.send(encode({ type: 'ready', scope: session.scope }))
|
|
295
|
-
* },
|
|
296
|
-
* async message(ws, data) {
|
|
297
|
-
* await wsHandler.handleMessage(ws.data, data as ArrayBuffer, (msg) => ws.send(msg))
|
|
298
|
-
* },
|
|
299
|
-
* async close(ws) {
|
|
300
|
-
* const session = ws.data as WsSession
|
|
301
|
-
* peers.get(session.scope)?.delete(session.peerId)
|
|
302
|
-
* await wsHandler.handleClose(session)
|
|
303
|
-
* },
|
|
304
|
-
* },
|
|
305
|
-
* })
|
|
306
|
-
* ```
|
|
307
|
-
*
|
|
308
|
-
* @param config - Handler configuration.
|
|
309
|
-
* @returns A {@link BrainyWsHandler} instance.
|
|
310
|
-
*/
|
|
311
|
-
export declare function createBrainyWsHandler(config: BrainyWsHandlerConfig): BrainyWsHandler;
|
|
312
|
-
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/server/handlers.ts"],"names":[],"mappings":"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"}
|