bazis 0.102.0 → 0.104.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.
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.102.0" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.104.0" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -5,12 +5,18 @@ import { reader, requireValue } from "../connectorConfig";
5
5
  import { CodexAppServerClient } from "./codex/CodexAppServerClient";
6
6
  import type { CodexClient, CodexConfigShape } from "./codex/contracts";
7
7
 
8
- export const CODEX_APP_SERVER: InjectionToken<CodexClient> = createToken<CodexClient>("CodexClient");
8
+ /**
9
+ * Codex App Server client of the `codexAppServerConnect()` connector,
10
+ * injected by type like the other infra clients:
11
+ * `constructor(private readonly codex: ICodex)`.
12
+ */
13
+ export interface ICodex extends CodexClient {}
14
+ export const ICodex = createToken<ICodex>("ICodex") as InjectionToken<CodexClient>;
9
15
 
10
16
  /** App Server is a full executor; it does not implement the raw AgentModelProvider port. */
11
17
  export function codexAppServerConnect(config: AppConfig<CodexConfigShape>): InfraConnector<CodexClient> {
12
18
  return {
13
- token: CODEX_APP_SERVER, config, phase: 0,
19
+ token: ICodex, config, phase: 0,
14
20
  create(configs) {
15
21
  const view = reader(config, configs);
16
22
  return new CodexAppServerClient({
@@ -7,7 +7,7 @@ export { Infra, infraModule, type InfraManifest } from "./Infra";
7
7
  export { InfraError, errorMessage, type ConnectionDescription, type InfraConnector } from "./InfraConnector";
8
8
  export { InfraLifecycle } from "./InfraLifecycle";
9
9
  export { reader, requireValue, settingNames, type ConfigReader } from "./connectorConfig";
10
- export { codexAppServerConnect, CODEX_APP_SERVER } from "./connectors/codex";
10
+ export { codexAppServerConnect, ICodex } from "./connectors/codex";
11
11
  export { CodexError, RU_CODEX_MESSAGES, type CodexErrorCode, CODEX_MODEL_ID, CODEX_REASONING_EFFORT, type CodexClient, type CodexConfigShape, type CodexLogin, type CodexLoginMethod, type CodexModel, type CodexStatus, type CodexRunInput, type CodexChatMessage, type CodexTool, type CodexToolCall, type CodexToolResult } from "./connectors/codex/contracts";
12
12
 
13
13
  export { postgres, IPostgres, postgresConnectionOptions, type PostgresConfigShape, type PostgresSslMode, type PostgresConnectorOptions } from "./connectors/postgres";
@@ -1,8 +1,9 @@
1
1
  import { DI, HOSTED_SERVICE, type InjectionToken, type ModuleOwnedContributionSnapshot, type ProviderDefinition, type ProviderDependencyList, type ServiceResolver } from "../di";
2
2
  import { getGeneratedClassDeps } from "../di/module/autoDeps";
3
3
  import { resolveDependencyList } from "../di/internal/resolveDependencyList";
4
- import { HEALTH_CHECK, StartupReport } from "../kernel";
4
+ import { ConfigRegistry, HEALTH_CHECK, StartupReport } from "../kernel";
5
5
  import { DbContextOptions, OrmError, type DatabaseProvider, type DbContext } from "../../library/orm";
6
+ import { bindEncryptedColumns } from "./encryptedWith";
6
7
  import { OrmLifecycle } from "./OrmLifecycle";
7
8
  import { ownedStoreLifecycleFor, ownedStoreLifecycleHealthFor } from "./OrmOwnedStoreLifecycle";
8
9
  import { effectiveOrmModuleConfig, ownedStoreRegistrations } from "./ownedStoreContributions";
@@ -22,13 +23,22 @@ type OrmBuildConfig<TContext extends DbContext> = OrmModuleConfig<TContext> & {
22
23
  function createContextOptions<TContext extends DbContext>(
23
24
  config: OrmBuildConfig<TContext>,
24
25
  provider: DatabaseProvider,
26
+ serviceProvider: unknown,
25
27
  ): DbContextOptions {
26
- return new DbContextOptions({
28
+ return bindKeys(new DbContextOptions({
27
29
  provider,
28
30
  entities: config.entities ?? [],
29
31
  validateOnSave: config.validateOnSave,
30
32
  executionStrategy: config.executionStrategy,
31
- });
33
+ }), serviceProvider);
34
+ }
35
+
36
+ /** Binds the keys of `encryptedWith` columns from the application configuration. */
37
+ function bindKeys(options: DbContextOptions, serviceProvider: unknown): DbContextOptions {
38
+ const services = serviceProvider as { has?(token: unknown): boolean; resolve?(token: unknown): unknown };
39
+ const registry = services.has?.(ConfigRegistry) ? services.resolve?.(ConfigRegistry) as ConfigRegistry : undefined;
40
+ bindEncryptedColumns(options, registry);
41
+ return options;
32
42
  }
33
43
 
34
44
  const contextDependencies = new WeakMap<object, ProviderDependencyList>();
@@ -103,7 +113,7 @@ export function buildOrmModuleProviders<TContext extends DbContext>(
103
113
  DI.scoped(
104
114
  DI.factoryProviderWithResolver(config.context, [token, SERVICE_PROVIDER] as const, (resolver, provider: DatabaseProvider, serviceProvider) => {
105
115
  const current = effectiveConfigFor(serviceProvider, registrationIdentity, config);
106
- return constructContext(current.context, createContextOptions(current, provider), resolver);
116
+ return constructContext(current.context, createContextOptions(current, provider, serviceProvider), resolver);
107
117
  }),
108
118
  ),
109
119
  DI.singleton(
@@ -119,7 +129,7 @@ export function buildOrmModuleProviders<TContext extends DbContext>(
119
129
  });
120
130
  return ownedStoreLifecycleFor(container, provider, records);
121
131
  }
122
- return createOrmLifecycle(current, createContextOptions(current, provider), false, serviceProvider);
132
+ return createOrmLifecycle(current, createContextOptions(current, provider, serviceProvider), false, serviceProvider);
123
133
  }),
124
134
  ),
125
135
  ];
@@ -137,12 +147,12 @@ export function buildOrmModuleProviders<TContext extends DbContext>(
137
147
  DI.scoped(DI.factoryProviderWithResolver(config.context, [SERVICE_PROVIDER], (resolver, serviceProvider) => {
138
148
  const current = effectiveConfigFor(serviceProvider, registrationIdentity, config);
139
149
  const context = current === config ? config.context : current.context;
140
- return constructContext(context, current === config && optionsAtModuleCall !== undefined ? optionsAtModuleCall : createContextOptions(current, provider), resolver);
150
+ return constructContext(context, current === config && optionsAtModuleCall !== undefined ? bindKeys(optionsAtModuleCall, serviceProvider) : createContextOptions(current, provider, serviceProvider), resolver);
141
151
  })),
142
152
  // Standalone mode: the context owns its provider and closes it.
143
153
  DI.singleton(DI.factoryProvider(HOSTED_SERVICE, [SERVICE_PROVIDER], (serviceProvider) => {
144
154
  const current = effectiveConfigFor(serviceProvider, registrationIdentity, config);
145
- return createOrmLifecycle(current, current === config && optionsAtModuleCall !== undefined ? optionsAtModuleCall : createContextOptions(current, provider), true, serviceProvider);
155
+ return createOrmLifecycle(current, current === config && optionsAtModuleCall !== undefined ? bindKeys(optionsAtModuleCall, serviceProvider) : createContextOptions(current, provider, serviceProvider), true, serviceProvider);
146
156
  })),
147
157
  ];
148
158
  return providers;
@@ -0,0 +1,85 @@
1
+ import { createHash } from "node:crypto";
2
+ import { Secret, type ConfigDefinition, type ConfigRegistry } from "../kernel";
3
+ import { OrmError, type DbContextOptions, type ValueConverter } from "../../library/orm";
4
+ import { aesGcmConverter } from "../../library/orm/Metadata/ValueConverter";
5
+
6
+ interface PendingKey {
7
+ readonly config: ConfigDefinition<object>;
8
+ readonly field: string;
9
+ /** The secret once bound, the configuration it came from, and its fingerprint for a second binding. */
10
+ secret?: string;
11
+ registry?: ConfigRegistry;
12
+ fingerprint?: string;
13
+ }
14
+
15
+ const pending = new WeakMap<object, PendingKey>();
16
+
17
+ /**
18
+ * AES-256-GCM encryption of a string column with a key from the application
19
+ * configuration. The key is read when the ORM starts — after every
20
+ * configuration source, secret files included, is loaded — so a missing key
21
+ * stops the start instead of failing on the first request:
22
+ *
23
+ * ```ts
24
+ * export const cryptoConfig = defineConfig("crypto", { default: { phoneKey: secret("dev-…") }, production: { phoneKey: secret() } });
25
+ *
26
+ * @HasConversion(encryptedWith(cryptoConfig, "phoneKey"))
27
+ * @Column({ type: "text" })
28
+ * phone = "";
29
+ * ```
30
+ *
31
+ * The secret must be random (`openssl rand -base64 32`): it is hashed with
32
+ * SHA-256, not stretched like a password. Equal values are stored differently,
33
+ * so the column cannot be compared or sorted in SQL.
34
+ */
35
+ export function encryptedWith<T extends object>(config: ConfigDefinition<T>, field: keyof T & string): ValueConverter<string, string> {
36
+ const binding: PendingKey = { config: config as ConfigDefinition<object>, field };
37
+ const converter = aesGcmConverter(() => {
38
+ if (binding.secret === undefined) {
39
+ throw new OrmError(`encryptedWith(${label(binding)}): the key is not bound yet; the column is used outside a started application.`);
40
+ }
41
+ return binding.secret;
42
+ });
43
+ pending.set(converter, binding);
44
+ return converter;
45
+ }
46
+
47
+ /**
48
+ * Binds the keys of `encryptedWith` columns of a context to the application
49
+ * configuration. Called by the ORM providers at startup; binding the same
50
+ * column again with another key (two applications in one process) is refused.
51
+ */
52
+ export function bindEncryptedColumns(options: DbContextOptions, registry: ConfigRegistry | undefined): void {
53
+ for (const entity of options.model.entities) {
54
+ for (const property of entity.properties) {
55
+ const binding = property.converter === undefined ? undefined : pending.get(property.converter);
56
+ if (binding === undefined || (registry !== undefined && binding.registry === registry)) continue;
57
+ const where = `${entity.name}.${property.propertyName}: encryptedWith(${label(binding)})`;
58
+ if (registry === undefined) throw new OrmError(`${where}: the application configuration is not available.`);
59
+ let view: { get(key: string): unknown };
60
+ try {
61
+ view = registry.get(binding.config) as { get(key: string): unknown };
62
+ } catch {
63
+ throw new OrmError(`${where}: the config "${prefixOf(binding)}" is not declared in the application; add it to \`config\` of the module that owns ${entity.name} or to runApp({ config }).`);
64
+ }
65
+ const value = view.get(binding.field);
66
+ const secret = value instanceof Secret ? value.reveal() : value;
67
+ if (typeof secret !== "string" || secret.length === 0) throw new OrmError(`${where}: the key is empty; set a random secret (openssl rand -base64 32).`);
68
+ const fingerprint = createHash("sha256").update(secret).digest("hex");
69
+ if (binding.fingerprint !== undefined && binding.fingerprint !== fingerprint) {
70
+ throw new OrmError(`${where}: the column is already bound to another key in this process.`);
71
+ }
72
+ binding.secret = secret;
73
+ binding.registry = registry;
74
+ binding.fingerprint = fingerprint;
75
+ }
76
+ }
77
+ }
78
+
79
+ function prefixOf(binding: PendingKey): string {
80
+ return binding.config.token.description.replace(/^Config:/, "");
81
+ }
82
+
83
+ function label(binding: PendingKey): string {
84
+ return `${prefixOf(binding)}.${binding.field}`;
85
+ }
package/core/orm/index.ts CHANGED
@@ -11,6 +11,7 @@ export { IRepository, repositoryFor } from "./repository";
11
11
  export { registerRepositories } from "./registerRepositories";
12
12
  export { paginate, type PageResult } from "./listQuery";
13
13
  export { databaseErrorStatus } from "./databaseErrorStatus";
14
+ export { encryptedWith } from "./encryptedWith";
14
15
 
15
16
  import { registerModuleMetadataExpander, type BazisModuleRef } from "../di";
16
17
  import { registerModuleDescriber } from "../kernel/report/moduleTree";
@@ -184,6 +184,15 @@ export class ModelBuilder {
184
184
  if (indexNames.has(index.name)) throw new ModelBuildError(`Entity "${ctor.name}": duplicate index name "${index.name}".`);
185
185
  indexNames.add(index.name);
186
186
  }
187
+ // Encryption with a random nonce stores equal values differently: such a
188
+ // column cannot identify a row or be unique.
189
+ for (const property of properties) {
190
+ if (property.converter?.comparable !== false) continue;
191
+ if (property.isKey) throw new ModelBuildError(`Entity "${ctor.name}": key "${property.propertyName}" cannot be encrypted: equal values are stored differently.`);
192
+ if (indexes.some((index) => index.unique && index.columns.includes(property.columnName))) {
193
+ throw new ModelBuildError(`Entity "${ctor.name}": a unique index on encrypted "${property.propertyName}" cannot work: equal values are stored differently. Put it on a keyed hash (HMAC) column.`);
194
+ }
195
+ }
187
196
  return model;
188
197
  }
189
198
 
@@ -7,40 +7,63 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:
7
7
  export interface ValueConverter<TModel = unknown, TProvider = unknown> {
8
8
  toProvider(value: TModel): TProvider;
9
9
  fromProvider(value: TProvider): TModel;
10
+ /**
11
+ * `false`: equal values are stored differently (encryption with a random
12
+ * nonce), so a condition or an ordering on the column cannot work in SQL;
13
+ * the query fails instead of silently matching nothing.
14
+ */
15
+ readonly comparable?: boolean;
16
+ }
17
+
18
+ /** Thrown by an encrypted column that cannot be read: another key, or the stored value was changed. */
19
+ export class DecryptionError extends Error {
20
+ constructor() {
21
+ super("cannot decrypt the value: it was encrypted with another key, or the stored value was changed");
22
+ this.name = "DecryptionError";
23
+ }
24
+ }
25
+
26
+ /**
27
+ * AES-256-GCM string encryption with a key that is read on first use.
28
+ * The database stores base64(nonce|ciphertext|tag); the key is the SHA-256 of
29
+ * the secret, so the secret must be random (32 bytes), not a password.
30
+ */
31
+ export function aesGcmConverter(secret: () => string): ValueConverter<string, string> {
32
+ let key: Buffer | undefined;
33
+ const keyOf = (): Buffer => (key ??= createHash("sha256").update(secret()).digest());
34
+ return {
35
+ comparable: false,
36
+ toProvider(plaintext: string): string {
37
+ const iv = randomBytes(12);
38
+ const cipher = createCipheriv("aes-256-gcm", keyOf(), iv);
39
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
40
+ return Buffer.concat([iv, encrypted, cipher.getAuthTag()]).toString("base64");
41
+ },
42
+ fromProvider(stored: string): string {
43
+ // Before 0.104.0 an empty string was stored as is.
44
+ if (stored === "") return "";
45
+ const buf = Buffer.from(stored, "base64");
46
+ if (buf.length < 28) throw new DecryptionError();
47
+ const decipher = createDecipheriv("aes-256-gcm", keyOf(), buf.subarray(0, 12));
48
+ decipher.setAuthTag(buf.subarray(buf.length - 16));
49
+ try {
50
+ return Buffer.concat([decipher.update(buf.subarray(12, buf.length - 16)), decipher.final()]).toString("utf8");
51
+ } catch {
52
+ throw new DecryptionError();
53
+ }
54
+ },
55
+ };
10
56
  }
11
57
 
12
58
  /** Built-in converters (no external dependencies: Node/Bun `crypto`). */
13
59
  export const ValueConverters = {
14
60
  /**
15
- * AES-256-GCM at-rest string encryption. The database stores base64(nonce|ciphertext|tag).
16
- * `secret` is the application password/key (derived through SHA-256).
61
+ * AES-256-GCM at-rest string encryption with a key given in code, for the ORM
62
+ * without an application. In an application take the key from its
63
+ * configuration: `encryptedWith(cryptoConfig, "phoneKey")` from `bazis/core/orm`.
17
64
  */
18
65
  encrypted(secret: string): ValueConverter<string, string> {
19
- const key = createHash("sha256").update(secret).digest();
20
- return {
21
- toProvider(plaintext: string): string {
22
- if (plaintext === "") {
23
- return "";
24
- }
25
- const iv = randomBytes(12);
26
- const cipher = createCipheriv("aes-256-gcm", key, iv);
27
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
28
- const tag = cipher.getAuthTag();
29
- return Buffer.concat([iv, encrypted, tag]).toString("base64");
30
- },
31
- fromProvider(stored: string): string {
32
- if (stored === "") {
33
- return "";
34
- }
35
- const buf = Buffer.from(stored, "base64");
36
- const iv = buf.subarray(0, 12);
37
- const tag = buf.subarray(buf.length - 16);
38
- const ciphertext = buf.subarray(12, buf.length - 16);
39
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
40
- decipher.setAuthTag(tag);
41
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
42
- },
43
- };
66
+ return aesGcmConverter(() => secret);
44
67
  },
45
68
 
46
69
  /** JSON <-> string through serialization (for custom structures on top of a text column). */
@@ -1,3 +1,4 @@
1
+ import { OrmError } from "../errors";
1
2
  import type { PropertyModel } from "../Metadata/types";
2
3
  import type { SqlDialect, SqlParam } from "./types";
3
4
 
@@ -7,8 +8,26 @@ export function encodeProperty(property: PropertyModel, value: unknown, dialect:
7
8
  return dialect.encode(modelValue, property.type);
8
9
  }
9
10
 
10
- /** Decodes a column value: dialect -> converter. */
11
- export function decodeProperty(property: PropertyModel, value: unknown, dialect: SqlDialect): unknown {
11
+ /** Decodes a column value: dialect -> converter. A converter failure names the entity and the property. */
12
+ export function decodeProperty(property: PropertyModel, value: unknown, dialect: SqlDialect, owner?: string): unknown {
12
13
  const providerValue = dialect.decode(value, property.type);
13
- return property.converter ? property.converter.fromProvider(providerValue) : providerValue;
14
+ if (!property.converter) return providerValue;
15
+ try {
16
+ return property.converter.fromProvider(providerValue);
17
+ } catch (error) {
18
+ const reason = error instanceof Error ? error.message : String(error);
19
+ throw new OrmError(`Cannot read ${owner ?? "entity"}.${property.propertyName}: ${reason}`, { cause: error });
20
+ }
21
+ }
22
+
23
+ /**
24
+ * A condition or an ordering on a column whose converter stores equal values
25
+ * differently (encryption with a random nonce) cannot work in SQL.
26
+ */
27
+ export function assertComparable(property: PropertyModel, owner: string): void {
28
+ if (property.converter?.comparable !== false) return;
29
+ throw new OrmError(
30
+ `${owner}.${property.propertyName} is encrypted with a random nonce: it cannot be compared or sorted in SQL. ` +
31
+ `To find rows by it, store a separate column with a keyed hash of the value (HMAC) and compare that.`,
32
+ );
14
33
  }
@@ -2,7 +2,7 @@ import { OrmError } from "../errors";
2
2
  import { renderPostgresOnConflictDoNothing, renderPostgresSkipLocked } from "../Providers/PostgresDialect";
3
3
  import type { EntityModel, PropertyModel } from "../Metadata/types";
4
4
  import type { SqlDialect, SqlParam } from "../Providers/types";
5
- import { encodeProperty } from "../Providers/propertyConversion";
5
+ import { assertComparable, encodeProperty } from "../Providers/propertyConversion";
6
6
  import type { Condition } from "./conditions";
7
7
  import type { QueryPlan } from "./QueryPlan";
8
8
 
@@ -121,7 +121,9 @@ export class SqlTranslator {
121
121
  return "";
122
122
  }
123
123
  const parts = plan.orders.map((order) => {
124
- const column = this.requireProperty(order.property).columnName;
124
+ const property = this.requireProperty(order.property);
125
+ assertComparable(property, this.model.name);
126
+ const column = property.columnName;
125
127
  return `${this.dialect.quoteId(column)}${order.descending ? " DESC" : " ASC"}`;
126
128
  });
127
129
  return ` ORDER BY ${parts.join(", ")}`;
@@ -144,6 +146,7 @@ export class SqlTranslator {
144
146
  switch (condition.kind) {
145
147
  case "compare": {
146
148
  const property = this.requireProperty(condition.property);
149
+ assertComparable(property, this.model.name);
147
150
  const placeholder = this.dialect.parameter(params.length);
148
151
  params.push(encode(property, condition.value));
149
152
  // startsWith/endsWith/contains escape wildcards with `\`; this works
@@ -153,6 +156,7 @@ export class SqlTranslator {
153
156
  }
154
157
  case "in": {
155
158
  const property = this.requireProperty(condition.property);
159
+ assertComparable(property, this.model.name);
156
160
  if (condition.values.length === 0) {
157
161
  return "0 = 1"; // an empty IN is always false
158
162
  }
@@ -167,7 +171,7 @@ export class SqlTranslator {
167
171
  if (condition.properties.length === 0 || condition.values.length === 0) return "0 = 1";
168
172
  return `(${condition.values.map((tuple) => {
169
173
  if (tuple.length !== condition.properties.length) throw new OrmError("Tuple predicate component count mismatch.");
170
- return `(${tuple.map((value, index) => { const property = this.requireProperty(condition.properties[index]!); const placeholder = this.dialect.parameter(params.length); params.push(encode(property, value)); return `${this.dialect.quoteId(property.columnName)} = ${placeholder}`; }).join(" AND ")})`;
174
+ return `(${tuple.map((value, index) => { const property = this.requireProperty(condition.properties[index]!); assertComparable(property, this.model.name); const placeholder = this.dialect.parameter(params.length); params.push(encode(property, value)); return `${this.dialect.quoteId(property.columnName)} = ${placeholder}`; }).join(" AND ")})`;
171
175
  }).join(" OR ")})`;
172
176
  }
173
177
  case "null": {
@@ -11,7 +11,7 @@ export function materialize<T extends object>(model: EntityModel, row: Row, dial
11
11
  const entity = new model.ctor() as Record<string, unknown>;
12
12
  for (const property of model.properties) {
13
13
  if (property.columnName in row) {
14
- entity[property.propertyName] = decodeProperty(property, row[property.columnName], dialect);
14
+ entity[property.propertyName] = decodeProperty(property, row[property.columnName], dialect, model.name);
15
15
  }
16
16
  }
17
17
  return entity as T;
@@ -32,7 +32,7 @@ export function materializeProjection(
32
32
  }
33
33
  const column = alias in row ? alias : property.columnName;
34
34
  if (column in row) {
35
- result[alias] = decodeProperty(property, row[column], dialect);
35
+ result[alias] = decodeProperty(property, row[column], dialect, model.name);
36
36
  }
37
37
  }
38
38
  return result;
@@ -45,7 +45,7 @@ export {
45
45
  type ScalarLogicalFieldType,
46
46
  } from "./Metadata/DynamicModelBuilder";
47
47
  export { pluralize } from "./Metadata/conventions";
48
- export { ValueConverters, type ValueConverter } from "./Metadata/ValueConverter";
48
+ export { DecryptionError, ValueConverters, type ValueConverter } from "./Metadata/ValueConverter";
49
49
  export type {
50
50
  ColumnOptionsType,
51
51
  ColumnType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.102.0",
3
+ "version": "0.104.0",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",