@exvio/os-backend-core 0.4.0

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/README.md ADDED
@@ -0,0 +1,466 @@
1
+ # @exvio/os-backend-core
2
+
3
+ Shared backend building blocks for the Wellous OS hub-and-spoke fleet.
4
+
5
+ This package plays the same governance role for backend infrastructure that
6
+ `@exvio/os-theme` plays for frontend UI: one implementation, versioned releases,
7
+ and deliberate upgrades in each consumer. It ships TypeScript source because
8
+ every current consumer runs on Bun and already type-checks with bundler module
9
+ resolution.
10
+
11
+ ## Scope law
12
+
13
+ The package owns infrastructure that has the same contract in multiple Wellous
14
+ backends. It must not own a product's routes, role model, database schema,
15
+ migrations, environment names, tenant table catalogue, or domain services.
16
+
17
+ Dependency direction is one-way:
18
+
19
+ ```text
20
+ consumer backend ---> @exvio/os-backend-core
21
+ adapters ---> db / redis / logger / config supplied by the app
22
+ ```
23
+
24
+ Core modules never import a consumer repository. Infrastructure that needs an
25
+ application service must be exposed as a factory and receive a narrow adapter.
26
+
27
+ ## v0.1 tenant API
28
+
29
+ The first release owns the shared `AsyncLocalStorage` tenant context and the
30
+ Kysely AST plugin that scopes tenant-aware queries.
31
+
32
+ ```ts
33
+ import {
34
+ TenantFilterPlugin,
35
+ bypassTenant,
36
+ tenantContext,
37
+ withTenant,
38
+ } from '@exvio/os-backend-core/tenant'
39
+
40
+ const db = new Kysely<Database>({
41
+ dialect,
42
+ plugins: [
43
+ new TenantFilterPlugin(TENANT_AWARE_TABLES, {
44
+ context: tenantContext,
45
+ // Transitional v0.1 policy. Move to `throw` after every non-HTTP entry
46
+ // point has an explicit tenant or bypass scope.
47
+ onMissingContext: 'passthrough',
48
+ }),
49
+ // Query profiler goes after the tenant filter so it sees executed SQL.
50
+ ],
51
+ })
52
+
53
+ await tenantContext.run({ tenantId: requestTenantId, bypass: false }, handleRequest)
54
+ await withTenant(jobTenantId, runBackgroundJob)
55
+ await bypassTenant(runReviewedCrossTenantOperation)
56
+ ```
57
+
58
+ The consumer remains the source of truth for `TENANT_AWARE_TABLES`. The plugin
59
+ only receives that `ReadonlySet<string>`; it does not know any product schema.
60
+ The constructor snapshots the set so later mutation cannot alter the boundary.
61
+ Entries are case-sensitive bare table names. A tenant-scoped table migration or
62
+ rename must update the consumer catalogue and its sentinel test in the same PR.
63
+
64
+ ### Security contract
65
+
66
+ - HTTP tenant middleware must wrap the entire downstream request in
67
+ `tenantContext.run(...)`.
68
+ - WebSockets, background jobs, cron, queues, workers, and fire-and-forget tasks
69
+ must use `withTenant(...)` for each tenant; HTTP context is not a substitute
70
+ for an explicit non-HTTP entry boundary. `withTenant` accepts only positive
71
+ safe-integer tenant ids.
72
+ - `bypassTenant(...)` is only for reviewed cross-tenant operations.
73
+ - No context currently preserves the fleet's existing behaviour: the plugin
74
+ passes the query through by default. Consumers must explicitly configure the
75
+ transitional policy, audit every entry point, and then switch to
76
+ `onMissingContext: 'throw'`.
77
+ - Raw `sql\`...\`` fragments are opaque to Kysely's AST transformer and must
78
+ include their own tenant predicate.
79
+ - Joined tenant tables are currently supported for INNER and LEFT joins
80
+ (including their lateral forms). RIGHT, FULL, CROSS, USING-join, and APPLY
81
+ variants fail loudly when the joined side is tenant-aware because placing a
82
+ predicate in `ON` would not safely filter their preserved side.
83
+ - `DELETE ... USING` tenant tables are scoped in the outer `WHERE`.
84
+ - A tenant-scoped `UPDATE` cannot assign `tenant_id`. Multi-table targets and
85
+ dynamic/raw update-column names fail loudly because the package cannot prove
86
+ their ownership semantics. Ownership changes require a separately reviewed
87
+ cross-tenant operation.
88
+ - `INSERT ... SELECT` and `INSERT ... DEFAULT VALUES` against a tenant-aware
89
+ table fail loudly. The only escape is a narrowly scoped `bypassTenant(...)`
90
+ operation that explicitly guarantees the projected tenant id.
91
+ - UPSERT and REPLACE variants fail loudly for tenant-aware targets. Until a
92
+ conflict key can be proven to include `tenant_id`, they can otherwise match
93
+ and mutate another tenant's row through a globally unique key.
94
+ - With an active tenant context, `MERGE` currently fails loudly for every table
95
+ because the Kysely AST cannot yet be rewritten safely.
96
+ - Compile and execute a query inside the intended context; do not compile it in
97
+ one scope and execute it in another.
98
+
99
+ All context users and `TenantFilterPlugin` must resolve the same installed
100
+ package instance. Do not mix a copied tenant context with the packaged plugin.
101
+ The optional `context` constructor argument makes this dependency explicit and
102
+ protects linked development/HMR from using a different singleton instance.
103
+
104
+ `tenantContext` is exposed for compatibility with HTTP middleware. Application
105
+ code must not call its `enterWith()` or `disable()` methods; prefer
106
+ `withTenant()`, `bypassTenant()`, and `getCurrentTenant()`.
107
+
108
+ Migration and seed commands must make their policy explicit. During the v0.1
109
+ transition they may rely on `passthrough`; before enabling strict mode, move
110
+ them to a dedicated Kysely instance without the tenant plugin or wrap each
111
+ reviewed operation in the appropriate scope.
112
+
113
+ ## v0.2 AI runtime (alpha)
114
+
115
+ The AI subpath centralizes provider transport and runtime behaviour without
116
+ owning a product's prompts, tools, tier rules, RAG, workflows, config tables, or
117
+ usage ledger.
118
+
119
+ ```ts
120
+ import { createAIClient } from '@exvio/os-backend-core/ai'
121
+ import { createDeepSeekProvider } from '@exvio/os-backend-core/ai/providers/deepseek'
122
+ import { createGeminiProvider } from '@exvio/os-backend-core/ai/providers/gemini'
123
+ import { getCurrentTenant } from '@exvio/os-backend-core/tenant'
124
+
125
+ const ai = createAIClient({
126
+ providers: new Map([
127
+ ['gemini', createGeminiProvider()],
128
+ ['deepseek', createDeepSeekProvider()],
129
+ ]),
130
+ // The app still owns DB/Redis config and must keep this tenant-isolated.
131
+ configSource: {
132
+ loadConfig: tenantId => loadAIConfig(tenantId, { fallback: false }),
133
+ },
134
+ tenantResolver: () => {
135
+ const context = getCurrentTenant()
136
+ return context && !context.bypass ? context.tenantId : undefined
137
+ },
138
+ missingTenantPolicy: 'throw',
139
+ telemetry: observeRedactedInvocation,
140
+ })
141
+
142
+ const result = await ai.generateText('Rewrite this clearly.', {
143
+ tenantId,
144
+ lane: 'text',
145
+ metadata: { feature: 'editor.rephrase' },
146
+ })
147
+
148
+ // A reliable product ledger must use the returned result, not telemetry.
149
+ await recordUsage({
150
+ tenantId,
151
+ provider: result.provider,
152
+ model: result.model,
153
+ usage: result.usage,
154
+ })
155
+ ```
156
+
157
+ The application config adapter returns explicit provider/model pairs:
158
+
159
+ ```ts
160
+ {
161
+ providers: {
162
+ gemini: { apiKey: '...' },
163
+ deepseek: {
164
+ apiKey: '...',
165
+ endpoint: 'https://api.deepseek.com/chat/completions',
166
+ },
167
+ },
168
+ lanes: {
169
+ text: { provider: 'gemini', model: 'gemini-2.5-flash' },
170
+ vision: { provider: 'gemini', model: 'gemini-2.5-flash' },
171
+ chat: { provider: 'deepseek', model: 'deepseek-v4-flash' },
172
+ embedding: { provider: 'gemini', model: 'gemini-embedding-001' },
173
+ },
174
+ }
175
+ ```
176
+
177
+ There is no model-prefix inference or silent provider fallback in the new API.
178
+ `parseAIConfig()` also applies Core's built-in model lifecycle policy to every
179
+ named lane, including custom lanes and explicit `{ provider, model }` entries.
180
+ Retired identifiers fail at config load; only an explicit forward data
181
+ migration may use `replacementForRetiredAIModelId()` to rewrite stored config.
182
+ Credentials are loaded per invocation and provider instances are not shared
183
+ between tenants. Missing tenant context fails by default. The built-in tenant
184
+ resolver intentionally ignores a `bypassTenant(...)` scope; a reviewed
185
+ cross-tenant operation must pass a positive `tenantId` on every AI call. A
186
+ positive explicit tenant must match a normal active tenant context; callers
187
+ cannot use an option to escape request tenancy. Enter a reviewed bypass scope
188
+ before making an intentional cross-tenant call. A
189
+ custom endpoint must be HTTPS, contain no URL credentials, and pass the
190
+ application's `validateEndpoint` policy before an Authorization header is
191
+ created. A provider factory's immutable official endpoint is the only endpoint
192
+ accepted without that hook. The built-in Gemini adapter additionally accepts
193
+ only Google's official API origin.
194
+
195
+ `AIClientOptions.fetch` is an injectable transport for HTTP providers such as
196
+ DeepSeek. The production Gemini adapter uses the official `@google/genai` SDK,
197
+ which cannot honor that injected transport; it fails loudly if the client is
198
+ configured with a custom `fetch` instead of silently bypassing the application's
199
+ transport policy.
200
+
201
+ Every text, content, chat, structured-output, stream, and embedding result uses
202
+ the same actual-model and token-usage envelope. A stream may retry only before
203
+ its first emitted event. Tool arguments and structured JSON fail closed rather
204
+ than becoming `{}`. Telemetry is scalar, secret-free, non-blocking, and
205
+ best-effort: a sink failure never changes a successful AI result, and telemetry
206
+ must not be used as a reliable billing or usage ledger. Persist non-stream usage
207
+ from the returned `AIResult`; for streaming, persist the `result` carried by the
208
+ terminal `finish` event.
209
+
210
+ ### Adding providers and lanes
211
+
212
+ The registry and lane names are open strings, so adding OpenAI, Anthropic,
213
+ Azure AI, an on-prem model, or a future vendor does not require changing the
214
+ consumer API. Implement an `AIProviderFactory`, declare its capabilities, add
215
+ provider conformance tests, register it in each application's config adapter,
216
+ and add an explicit `{ provider, model }` selection. Product call sites remain
217
+ on `generateText`, `generateContent`, `generateChat`, `streamChat`,
218
+ `generateObject`, or `embed`.
219
+
220
+ Multiple providers and lanes can be active for the same tenant at the same
221
+ time. For example, `text` may use DeepSeek, `vision` Gemini, `chat` a private
222
+ provider, and `embedding` a dedicated embedding model. New product-specific
223
+ lanes such as `workflow`, `classification`, or `assessment` require only a new
224
+ config entry; Core does not maintain a closed lane enum. Automatic fallback,
225
+ load balancing, or cost routing must be an explicit application policy and may
226
+ never silently change the reported provider/model.
227
+
228
+ Model/lane selection and prices are deliberately application-owned; rates are
229
+ not bundled. Use `createPriceCatalog()` with an operations-owned
230
+ version/effective date and `calculateCostUsd()`. Unknown models return `null`,
231
+ not a misleading zero-dollar value. Product prompts, personas, RAG/vector-store
232
+ policy, tool execution/authorization, and workflows also remain outside core.
233
+
234
+ ## v0.3 Changelog runtime (alpha)
235
+
236
+ The Changelog subpath centralizes immutable release-note catalogues, weighted
237
+ language negotiation, and per-subject acknowledgement state without owning an
238
+ application's content, HTTP routes, user table, or migrations.
239
+
240
+ ```ts
241
+ import {
242
+ createChangelogCatalogue,
243
+ createChangelogService,
244
+ } from '@exvio/os-backend-core/changelog'
245
+
246
+ const catalogue = createChangelogCatalogue({
247
+ entries: appReleaseNotes, // newest first; versions are opaque keys
248
+ locales: ['en', 'cn', 'bm'] as const,
249
+ defaultLocale: 'en',
250
+ languageTags: { zh: 'cn', ms: 'bm' },
251
+ maxVersionLength: 64, // must match the consumer database column
252
+ })
253
+
254
+ const changelog = createChangelogService({
255
+ catalogue,
256
+ acknowledgements: appAcknowledgementStore,
257
+ })
258
+
259
+ const state = await changelog.getState({
260
+ subjectId: authenticatedUserId,
261
+ acceptLanguage: request.headers.get('Accept-Language'),
262
+ })
263
+ ```
264
+
265
+ The first array entry is the latest release. Core never sorts versions by
266
+ SemVer, text, or display date. Every acknowledgement must name an exact known
267
+ version, so a late acknowledgement for an older screen may make a newer release
268
+ reappear but can never silently hide it. The catalogue snapshots and freezes
269
+ consumer input, requires the default-locale body, and allows older entries to
270
+ fall back when an optional translation is absent.
271
+
272
+ The acknowledgement store always receives both `tenantId` and `subjectId`.
273
+ The default resolver reads the package tenant context, ignores bypass scopes,
274
+ and fails when no tenant exists. An explicit tenant is allowed for reviewed
275
+ background or bypass work, but it may not disagree with an active non-bypass
276
+ tenant. Consumer repositories should use the supplied subject as their scope;
277
+ they must not query by user id alone.
278
+
279
+ Hono status codes, request-body limits, authentication, Kysely queries,
280
+ database migrations, release-note Markdown, and frontend sanitization remain
281
+ application-owned. HTTP adapters should project only the documented response
282
+ fields and send personalized responses with `Cache-Control: no-store, private`.
283
+
284
+ ## v0.3 Guide runtime (alpha)
285
+
286
+ The Guide subpath owns the common file-content mechanics used by every backend:
287
+ manifest validation, deterministic grouping, locale fallback, bounded Markdown
288
+ reads, frontmatter parsing, visibility policies, and removal of private
289
+ `ai-context` authoring comments.
290
+
291
+ ```ts
292
+ import path from 'node:path'
293
+ import {
294
+ createFileGuideContentSource,
295
+ createGuideService,
296
+ } from '@exvio/os-backend-core/guide'
297
+
298
+ const guides = createGuideService({
299
+ locales: ['en', 'cn'] as const,
300
+ defaultLocale: 'en',
301
+ categories: appGuideCategories,
302
+ entries: appGuideEntries,
303
+ source: createFileGuideContentSource({
304
+ root: path.resolve('src/data/guides'),
305
+ maxFileBytes: 512_000,
306
+ }),
307
+ // These are separate by design: hiding an item in a catalogue is not
308
+ // automatically authorization for its detail route.
309
+ listPolicy: (entry, context) => includeGuideInCatalogue(entry, context),
310
+ readPolicy: (entry, context) => mayReadGuide(entry, context),
311
+ })
312
+
313
+ await guides.validate() // CI/startup asset audit
314
+ const document = await guides.load({ category, slug, locale, context })
315
+ ```
316
+
317
+ Category, slug, and locale identifiers are validated before file access. The
318
+ file source requires an explicit absolute root, resolves real paths, rejects
319
+ resolved symlink escapes, and caps reads. The root must be a Git/deployment-owned
320
+ read-only asset directory; this adapter is not a sandbox for a directory that
321
+ an untrusted process can rename or rewrite concurrently. Manifest input is deeply snapshotted and
322
+ case-insensitive filesystem collisions fail at construction. Locale fallback
323
+ returns both the requested and actual resolved locale.
324
+
325
+ Frontmatter intentionally supports only `title` and `description`, handles BOM,
326
+ LF, and CRLF, and rejects duplicate or unknown fields. User-facing bodies always
327
+ remove `<!-- ai-context ... -->`; an unclosed marker removes the rest of the
328
+ document rather than leaking private authoring material. The first API does not
329
+ offer a raw or bulk AI loader.
330
+
331
+ Applications retain their Markdown, manifest metadata, role model, Hono/i18n
332
+ adapter, MCP and AI prompt adapters, and any tenant-specific overrides. Core
333
+ policies receive the app's generic context but do not know `UserRole`. Routes
334
+ must explicitly project response fields instead of serializing the returned
335
+ extensible manifest entry. Markdown-to-HTML rendering and DOM sanitization are
336
+ a frontend/theme responsibility.
337
+
338
+ ## v0.4 Auth runtime (alpha)
339
+
340
+ The first Auth slice centralizes framework-neutral login policy, atomic OAuth
341
+ transactions, non-bearer session-management handles, lifetime calculation, and
342
+ secret redaction. It does not own users, roles, Hono routes, Redis, Kysely,
343
+ provider credentials, or product authorization.
344
+
345
+ ```ts
346
+ import {
347
+ createLoginPolicy,
348
+ createOAuthBrowserExchangeManager,
349
+ createOAuthStateManager,
350
+ createSessionBearerToken,
351
+ createSessionManagementHandle,
352
+ } from '@exvio/os-backend-core/auth'
353
+
354
+ const policy = createLoginPolicy({
355
+ mode: tenantIsHubManaged ? 'hub_managed' : 'standalone',
356
+ enabled: { email: true, google: true, microsoft: false, passkey: true },
357
+ })
358
+ policy.assertAllowed({ surface: 'google', phase: 'start' })
359
+
360
+ const oneTimeStore = {
361
+ put: (key, value, ttlSeconds) => redis.set(key, value, ttlSeconds),
362
+ // This must be one Redis GETDEL command or one Lua script.
363
+ take: key => redis.getdel(key),
364
+ }
365
+ const oauthState = createOAuthStateManager({ store: oneTimeStore })
366
+ const browserExchange = createOAuthBrowserExchangeManager({ store: oneTimeStore })
367
+
368
+ const state = await oauthState.issue({
369
+ tenantId,
370
+ provider: 'google',
371
+ redirectUri,
372
+ verifier: pkceVerifier,
373
+ initiatingSessionId: browserTransactionCookie,
374
+ })
375
+
376
+ const exchangeCode = await browserExchange.issue({
377
+ tenantId,
378
+ browserTransaction: browserTransactionCookie,
379
+ provider: 'google',
380
+ userId,
381
+ // Optional revocation epoch captured before provider authentication.
382
+ sessionGeneration,
383
+ })
384
+ ```
385
+
386
+ The OAuth manager generates the state token and stores only its SHA-256-derived
387
+ key. Consumption binds the exact tenant, provider, redirect URI, and optional
388
+ initiating session, then burns the record before returning the PKCE verifier.
389
+ Missing, malformed, expired, replayed, tenant-swapped, provider-swapped, and
390
+ redirect-swapped transactions fail with a secret-free `AuthError`.
391
+
392
+ After a successful provider callback, the browser exchange manager creates a
393
+ short-lived, one-time code bound to the tenant, browser transaction, provider,
394
+ user, optional session revocation generation, and expiry. It stores only a SHA-256-derived key and atomically burns the
395
+ record before comparing the tenant and browser binding, so mismatch, replay,
396
+ and concurrent consumption fail closed. The application owns the Secure,
397
+ HttpOnly, SameSite=Lax transaction cookie, Hono exchange endpoint, final login
398
+ policy/user checks, and session creation. A callback redirects with only the
399
+ exchange code; a bearer token, PKCE verifier, OAuth state, and user projection
400
+ must never be placed in the URL.
401
+
402
+ ### Passkey alpha boundary
403
+
404
+ The Auth alpha also provides framework-neutral Passkey challenge transactions,
405
+ distributed rate-limit decisions, recent-auth evaluation, HMAC-derived WebAuthn
406
+ user handles, and signature-counter compare-and-swap rules. Registration binds
407
+ the positive tenant and user ids, initiating session, recent authentication
408
+ time, RP ID, origin, and excluded credentials. Authentication binds the
409
+ positive tenant id, optional user, RP ID, origin, and allowed credential set.
410
+ Both flows consume stored state atomically before comparison; missing, replayed,
411
+ concurrent, malformed, purpose-swapped, or context-swapped finishes fail with a
412
+ secret-free `PasskeyAuthError`.
413
+
414
+ Core supplies no Redis or SQL client. A challenge adapter must implement atomic
415
+ `take` with Redis `GETDEL` (or one equivalent Lua command), and authentication
416
+ issuance also requires atomic `putIfAbsent`. The application supplies the Redis
417
+ Lua rate-limit adapter and tenant-aware SQL counter CAS, plus Hono routes,
418
+ SimpleWebAuthn verification, RP configuration, trusted-proxy address policy,
419
+ credential persistence, recent-auth UX, security notifications, and durable
420
+ audit storage. Durable Passkey audit is not implemented by this alpha and is a
421
+ release blocker wherever policy or compliance requires it.
422
+
423
+ `createSessionManagementHandle()` derives a stable `smh_v1_...` handle for a
424
+ session-management screen. That handle cannot authenticate a request. A
425
+ consumer must still verify the authenticated owner before revocation and must
426
+ never return or log the underlying bearer. New sessions use the disjoint
427
+ `sat_v1_...` namespace from `createSessionBearerToken()`; raw random values,
428
+ `smh_v1_...` handles, and `oas_v1_...` states are not session bearers. Consumers
429
+ enforce both idle and absolute expiry with `remainingSessionTtl()` and revoke
430
+ the request's previous session after every successful authentication or
431
+ privilege-changing rotation.
432
+
433
+ The mode and surface matrix is shared, but role hierarchies are not. Each
434
+ product's action permissions, object access, workflow ownership, entitlements
435
+ and API-key/MCP scope catalogues remain application policy.
436
+
437
+ `redactAuthSecrets()` and `redactAuthStorageKey()` produce log-safe values. The
438
+ storage-key redactor bundles only generic auth namespaces (`session:`,
439
+ `oauth:state:`, `passkey:auth:`, `password:reset:` and similar); an
440
+ application's own credential-bearing key prefixes are passed on each call, for
441
+ example `redactAuthStorageKey(key, ['ticket:', 'invite:'])`. A key that matches
442
+ no prefix is returned unchanged, so an unlisted namespace is a logging leak, not
443
+ a redaction.
444
+
445
+ ## Install
446
+
447
+ The package is on the public npm registry under the `@exvio` scope. No git, ssh
448
+ or AWS credentials are needed on the consumer side, and a Docker build that runs
449
+ `bun install` needs nothing beyond registry access: no Git, no SSH client, no key
450
+ to forward.
451
+
452
+ ```powershell
453
+ bun add @exvio/os-backend-core
454
+ ```
455
+
456
+ ```json
457
+ { "dependencies": { "@exvio/os-backend-core": "^0.4.0" } }
458
+ ```
459
+
460
+ Pin exactly (`0.4.0`, no caret) where a consumer must not pick up a minor: on a
461
+ 0.x line every minor may carry a breaking change.
462
+
463
+ Published versions are immutable and every consumer upgrades explicitly. A
464
+ pre-release version (`0.5.0-alpha.1`) is published under the `next` dist-tag and
465
+ never moves `latest`. Contributor workflow, gates and the release procedure are
466
+ in the source repository's `CONTRIBUTING.md`, which is not part of the package.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@exvio/os-backend-core",
3
+ "version": "0.4.0",
4
+ "license": "UNLICENSED",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org"
8
+ },
9
+ "type": "module",
10
+ "description": "Shared backend building blocks for the Wellous OS hub-and-spoke fleet.",
11
+ "module": "./src/index.ts",
12
+ "types": "./src/index.ts",
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./tenant": "./src/tenant.ts",
16
+ "./auth": "./src/auth/index.ts",
17
+ "./changelog": "./src/changelog/index.ts",
18
+ "./guide": "./src/guide/index.ts",
19
+ "./ai": "./src/ai/index.ts",
20
+ "./ai/providers/gemini": "./src/ai/providers/gemini.ts",
21
+ "./ai/providers/deepseek": "./src/ai/providers/deepseek.ts"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "sideEffects": false,
28
+ "scripts": {
29
+ "type-check": "bun x tsc -p tsconfig.json --noEmit",
30
+ "test": "bun test",
31
+ "assert:publish-safe": "node scripts/assert-publish-safe.mjs",
32
+ "check": "bun run type-check && bun test"
33
+ },
34
+ "engines": {
35
+ "bun": ">=1.3.10"
36
+ },
37
+ "peerDependencies": {
38
+ "kysely": ">=0.28.16 <0.29.0"
39
+ },
40
+ "dependencies": {
41
+ "@google/genai": "2.17.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "^1.3.14",
45
+ "kysely": "^0.28.17",
46
+ "typescript": "^5.9.3"
47
+ }
48
+ }