@getstrata/core 0.7.3 → 0.7.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 0.7.5
4
+
5
+ - `TENANCY_DRIVER=column`: tenant ALS without Postgres `SET LOCAL` / `set_config`. `isRlsTenancy()` is the RLS-only check.
6
+ - `AuthUserRecord` may include `name` and optional MFA columns used by generated starters.
7
+
8
+ ## 0.7.4
9
+
10
+ - Default database pool and query handles live on `globalThis`, so the published `@getstrata/core` bundle and `src/core` share one pool in the same process.
11
+
3
12
  ## 0.7.3
4
13
 
5
14
  - `JsonResource.whenLoaded` returns `null` when a relation is loaded but empty. It does not call the transform, so `new PositionResource(value).toArray()` cannot crash on a missing belongsTo.
package/README.md CHANGED
@@ -23,7 +23,7 @@ import { EtaViewEngine } from "@getstrata/core/view";
23
23
 
24
24
  `.eta` files are **HTML + Eta tags** (`<% %>`, `<%= %>`, `<%~ include() %>`), not another template language. Class/attribute shorthand such as `section.section` or `a href=` fails at render time with the template name.
25
25
 
26
- **Dependency:** `eta` is a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database **engine** is your choice. HiroApp uses **Bun's built-in `Bun.sql`** (Postgres). Create and bind it with `createBunSqlPool()` / `bindBunSql()`, or call `bindDatabaseConnection()` yourself. Extra engines register with `registerNamedConnection` (`database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`).
26
+ **Dependency:** `eta` is a direct dependency of `@getstrata/core`. Apps do not need to list it separately. The database **engine** is your choice. Generated HiroApp uses **Bun's built-in `Bun.sql`** (Postgres): `createBunSqlPool()`, then `registerDefaultDatabasePool()` and `bindDatabaseConnection()`. `bindBunSql()` is a helper that registers both. Extra engines register with `registerNamedConnection` (`database/namedConnections`, `database/sqliteConnection`, `database/mysqlConnection`).
27
27
 
28
28
  `orderBy` accepts `{ column, direction }` objects or column shorthand such as `{ published_at: "desc" }`. `{ ilike }` uses the value as-is. Pass `%term%` yourself.
29
29
 
@@ -33,7 +33,7 @@ import { EtaViewEngine } from "@getstrata/core/view";
33
33
  - `createFailedJobService`, `FailedJobService.delete()`: failed job persistence and cleanup
34
34
  - `runQueueJob`, `jobRegistry`: dispatch retried jobs from admin UIs
35
35
 
36
- HiroApp uses failed-job recovery in the staff dashboard. You do not have to use the admin registry.
36
+ Core ships failed-job helpers and an optional admin resource registry. Generated HiroApp does not include an admin dashboard. You do not have to use the registry.
37
37
 
38
38
  ## Build and verify (monorepo root)
39
39
 
@@ -65,7 +65,7 @@ import type { Migration } from "@getstrata/core/database/migrations/types";
65
65
  Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
66
66
 
67
67
  1. Add `NPM_TOKEN` to GitHub repository secrets.
68
- 2. Tag a release: `git tag v0.7.3 && git push origin v0.7.3`
68
+ 2. Tag a release: `git tag v0.7.5 && git push origin v0.7.5`
69
69
  3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
70
70
 
71
71
  See [docs/PACKAGING.md](../../docs/PACKAGING.md).
@@ -1,10 +1,14 @@
1
1
  import type { AuthUser } from "../auth/authContext";
2
2
  interface AuthUserRecord {
3
3
  id: number;
4
+ name?: string | null;
4
5
  email?: string | null;
5
6
  role: string;
6
7
  email_verified_at?: Date | string | null;
7
8
  session_valid_after?: Date | string | null;
9
+ mfa_enabled?: boolean;
10
+ mfa_secret?: string | null;
11
+ mfa_recovery_codes?: string | null;
8
12
  }
9
13
  interface AuthUserDirectory {
10
14
  resolveUserFromToken(token: string): Promise<AuthUser | null>;
@@ -1,5 +1,7 @@
1
- type TenancyDriver = "rls" | "none";
1
+ type TenancyDriver = "rls" | "column" | "none";
2
2
  declare function readTenancyDriver(env?: Record<string, string | undefined>): TenancyDriver;
3
3
  declare function isTenancyEnabled(env?: Record<string, string | undefined>): boolean;
4
+ /** Postgres `SET LOCAL` / `set_config` for row-level security. SQLite and MySQL cannot do this. */
5
+ declare function isRlsTenancy(env?: Record<string, string | undefined>): boolean;
4
6
  export type { TenancyDriver };
5
- export { isTenancyEnabled, readTenancyDriver };
7
+ export { isRlsTenancy, isTenancyEnabled, readTenancyDriver };
@@ -189,7 +189,29 @@ async function safeFetch(input, init = {}, options = {}) {
189
189
 
190
190
  // ../../src/core/tenant/databaseTenantContext.ts
191
191
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
192
+
193
+ // ../../src/core/tenant/tenancyConfig.ts
194
+ function readTenancyDriver(env = process.env) {
195
+ if (env.TENANCY_DRIVER === "none") {
196
+ return "none";
197
+ }
198
+ if (env.TENANCY_DRIVER === "column") {
199
+ return "column";
200
+ }
201
+ return "rls";
202
+ }
203
+ function isTenancyEnabled(env = process.env) {
204
+ return readTenancyDriver(env) !== "none";
205
+ }
206
+ function isRlsTenancy(env = process.env) {
207
+ return readTenancyDriver(env) === "rls";
208
+ }
209
+
210
+ // ../../src/core/tenant/databaseTenantContext.ts
192
211
  async function runWithMigrationBypass(callback) {
212
+ if (!isRlsTenancy()) {
213
+ return await callback();
214
+ }
193
215
  await db`SELECT set_config('app.bypass_rls', 'true', false)`;
194
216
  try {
195
217
  return await callback();
@@ -79,11 +79,20 @@ import { repositoryConnection as db } from "@getstrata/core/database/repositoryC
79
79
 
80
80
  // ../../src/core/tenant/tenancyConfig.ts
81
81
  function readTenancyDriver(env = process.env) {
82
- return env.TENANCY_DRIVER === "none" ? "none" : "rls";
82
+ if (env.TENANCY_DRIVER === "none") {
83
+ return "none";
84
+ }
85
+ if (env.TENANCY_DRIVER === "column") {
86
+ return "column";
87
+ }
88
+ return "rls";
83
89
  }
84
90
  function isTenancyEnabled(env = process.env) {
85
91
  return readTenancyDriver(env) !== "none";
86
92
  }
93
+ function isRlsTenancy(env = process.env) {
94
+ return readTenancyDriver(env) === "rls";
95
+ }
87
96
 
88
97
  // ../../src/core/tenant/resolveTenant.ts
89
98
  async function resolveTenant(tenantId) {
@@ -123,7 +132,7 @@ async function applyTenantContextToTransaction(transaction, tenantId) {
123
132
  await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
124
133
  }
125
134
  async function runWithTenantDatabase(tenant, callback) {
126
- if (!isTenancyEnabled()) {
135
+ if (!isTenancyEnabled() || !isRlsTenancy()) {
127
136
  return await runWithTenant(tenant, callback);
128
137
  }
129
138
  if (hasActiveDatabaseConnection()) {
@@ -189,7 +189,29 @@ async function safeFetch(input, init = {}, options = {}) {
189
189
 
190
190
  // ../../src/core/tenant/databaseTenantContext.ts
191
191
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
192
+
193
+ // ../../src/core/tenant/tenancyConfig.ts
194
+ function readTenancyDriver(env = process.env) {
195
+ if (env.TENANCY_DRIVER === "none") {
196
+ return "none";
197
+ }
198
+ if (env.TENANCY_DRIVER === "column") {
199
+ return "column";
200
+ }
201
+ return "rls";
202
+ }
203
+ function isTenancyEnabled(env = process.env) {
204
+ return readTenancyDriver(env) !== "none";
205
+ }
206
+ function isRlsTenancy(env = process.env) {
207
+ return readTenancyDriver(env) === "rls";
208
+ }
209
+
210
+ // ../../src/core/tenant/databaseTenantContext.ts
192
211
  async function runWithMigrationBypass(callback) {
212
+ if (!isRlsTenancy()) {
213
+ return await callback();
214
+ }
193
215
  await db`SELECT set_config('app.bypass_rls', 'true', false)`;
194
216
  try {
195
217
  return await callback();
@@ -1,7 +1,29 @@
1
1
  // @bun
2
2
  // ../../src/core/tenant/databaseTenantContext.ts
3
3
  import { repositoryConnection as db } from "@getstrata/core/database/repositoryConnection";
4
+
5
+ // ../../src/core/tenant/tenancyConfig.ts
6
+ function readTenancyDriver(env = process.env) {
7
+ if (env.TENANCY_DRIVER === "none") {
8
+ return "none";
9
+ }
10
+ if (env.TENANCY_DRIVER === "column") {
11
+ return "column";
12
+ }
13
+ return "rls";
14
+ }
15
+ function isTenancyEnabled(env = process.env) {
16
+ return readTenancyDriver(env) !== "none";
17
+ }
18
+ function isRlsTenancy(env = process.env) {
19
+ return readTenancyDriver(env) === "rls";
20
+ }
21
+
22
+ // ../../src/core/tenant/databaseTenantContext.ts
4
23
  async function runWithMigrationBypass(callback) {
24
+ if (!isRlsTenancy()) {
25
+ return await callback();
26
+ }
5
27
  await db`SELECT set_config('app.bypass_rls', 'true', false)`;
6
28
  try {
7
29
  return await callback();
@@ -1,12 +1,22 @@
1
1
  // @bun
2
2
  // ../../src/core/tenant/tenancyConfig.ts
3
3
  function readTenancyDriver(env = process.env) {
4
- return env.TENANCY_DRIVER === "none" ? "none" : "rls";
4
+ if (env.TENANCY_DRIVER === "none") {
5
+ return "none";
6
+ }
7
+ if (env.TENANCY_DRIVER === "column") {
8
+ return "column";
9
+ }
10
+ return "rls";
5
11
  }
6
12
  function isTenancyEnabled(env = process.env) {
7
13
  return readTenancyDriver(env) !== "none";
8
14
  }
15
+ function isRlsTenancy(env = process.env) {
16
+ return readTenancyDriver(env) === "rls";
17
+ }
9
18
  export {
19
+ isRlsTenancy,
10
20
  isTenancyEnabled,
11
21
  readTenancyDriver
12
22
  };
@@ -30,11 +30,20 @@ function hasActiveDatabaseConnection() {
30
30
 
31
31
  // ../../src/core/tenant/tenancyConfig.ts
32
32
  function readTenancyDriver(env = process.env) {
33
- return env.TENANCY_DRIVER === "none" ? "none" : "rls";
33
+ if (env.TENANCY_DRIVER === "none") {
34
+ return "none";
35
+ }
36
+ if (env.TENANCY_DRIVER === "column") {
37
+ return "column";
38
+ }
39
+ return "rls";
34
40
  }
35
41
  function isTenancyEnabled(env = process.env) {
36
42
  return readTenancyDriver(env) !== "none";
37
43
  }
44
+ function isRlsTenancy(env = process.env) {
45
+ return readTenancyDriver(env) === "rls";
46
+ }
38
47
 
39
48
  // ../../src/core/tenant/tenantContext.ts
40
49
  var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
@@ -51,7 +60,7 @@ async function applyTenantContextToTransaction(transaction, tenantId) {
51
60
  await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
52
61
  }
53
62
  async function runWithTenantDatabase(tenant, callback) {
54
- if (!isTenancyEnabled()) {
63
+ if (!isTenancyEnabled() || !isRlsTenancy()) {
55
64
  return await runWithTenant(tenant, callback);
56
65
  }
57
66
  if (hasActiveDatabaseConnection()) {
@@ -126,7 +126,7 @@ export { logSecurityEvent } from "../core/security/securityEvents.ts";
126
126
  export type { StorageDriver } from "../core/storage/storage.ts";
127
127
  export { createStorageDriver, LocalStorageDriver, resetDefaultStorage, StorageManager, } from "../core/storage/storage.ts";
128
128
  export type { TenancyDriver } from "../core/tenant/tenancyConfig.ts";
129
- export { isTenancyEnabled, readTenancyDriver } from "../core/tenant/tenancyConfig.ts";
129
+ export { isRlsTenancy, isTenancyEnabled, readTenancyDriver, } from "../core/tenant/tenancyConfig.ts";
130
130
  export type { TenantContext } from "../core/tenant/tenantContext.ts";
131
131
  export { currentTenant, currentTenantId, rateLimitMultiplierForPlan, runWithTenant, } from "../core/tenant/tenantContext.ts";
132
132
  export { isInsideTenantDatabaseScope, runWithTenantDatabase, } from "../core/tenant/tenantDatabaseScope.ts";
package/dist/index.js CHANGED
@@ -1008,17 +1008,25 @@ class PolicyGate {
1008
1008
  }
1009
1009
  }
1010
1010
  // ../../src/core/database/boundConnection.ts
1011
- var boundConnectionHolder = {
1012
- connection: null
1013
- };
1011
+ var BOUND_CONNECTION_KEY = Symbol.for("@getstrata/boundDatabaseConnection");
1012
+ function boundConnectionState() {
1013
+ const globalRecord = globalThis;
1014
+ const existing = globalRecord[BOUND_CONNECTION_KEY];
1015
+ if (existing) {
1016
+ return existing;
1017
+ }
1018
+ const created = { connection: null };
1019
+ globalRecord[BOUND_CONNECTION_KEY] = created;
1020
+ return created;
1021
+ }
1014
1022
  function bindDatabaseConnection(connection) {
1015
- boundConnectionHolder.connection = connection;
1023
+ boundConnectionState().connection = connection;
1016
1024
  }
1017
1025
  function getBoundDatabaseConnection() {
1018
- return boundConnectionHolder.connection;
1026
+ return boundConnectionState().connection;
1019
1027
  }
1020
1028
  function resetBoundDatabaseConnection() {
1021
- boundConnectionHolder.connection = null;
1029
+ boundConnectionState().connection = null;
1022
1030
  }
1023
1031
  // ../../src/core/database/connectionContext.ts
1024
1032
  var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
@@ -1057,27 +1065,35 @@ function createDatabaseQueryProxy(pool) {
1057
1065
  }
1058
1066
 
1059
1067
  // ../../src/core/database/defaultConnection.ts
1060
- var defaultPool = {
1061
- connection: null
1062
- };
1063
- var defaultQuery = {
1064
- connection: null
1065
- };
1068
+ var DEFAULT_CONNECTION_KEY = Symbol.for("@getstrata/defaultDatabaseConnection");
1069
+ function defaultConnectionState() {
1070
+ const globalRecord = globalThis;
1071
+ const existing = globalRecord[DEFAULT_CONNECTION_KEY];
1072
+ if (existing) {
1073
+ return existing;
1074
+ }
1075
+ const created = { pool: null, query: null };
1076
+ globalRecord[DEFAULT_CONNECTION_KEY] = created;
1077
+ return created;
1078
+ }
1066
1079
  function registerDefaultDatabasePool(connection) {
1067
- defaultPool.connection = connection;
1068
- defaultQuery.connection = createDatabaseQueryProxy(connection);
1080
+ const holder = defaultConnectionState();
1081
+ holder.pool = connection;
1082
+ holder.query = createDatabaseQueryProxy(connection);
1069
1083
  }
1070
1084
  function getDefaultDatabasePool() {
1071
- if (!defaultPool.connection) {
1085
+ const pool = defaultConnectionState().pool;
1086
+ if (!pool) {
1072
1087
  throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
1073
1088
  }
1074
- return defaultPool.connection;
1089
+ return pool;
1075
1090
  }
1076
1091
  function getDefaultDatabaseQuery() {
1077
- if (!defaultQuery.connection) {
1092
+ const query = defaultConnectionState().query;
1093
+ if (!query) {
1078
1094
  throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
1079
1095
  }
1080
- return defaultQuery.connection;
1096
+ return query;
1081
1097
  }
1082
1098
 
1083
1099
  // ../../src/core/security/timingSafeCompare.ts
@@ -1141,11 +1157,20 @@ var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
1141
1157
 
1142
1158
  // ../../src/core/tenant/tenancyConfig.ts
1143
1159
  function readTenancyDriver(env = process.env) {
1144
- return env.TENANCY_DRIVER === "none" ? "none" : "rls";
1160
+ if (env.TENANCY_DRIVER === "none") {
1161
+ return "none";
1162
+ }
1163
+ if (env.TENANCY_DRIVER === "column") {
1164
+ return "column";
1165
+ }
1166
+ return "rls";
1145
1167
  }
1146
1168
  function isTenancyEnabled(env = process.env) {
1147
1169
  return readTenancyDriver(env) !== "none";
1148
1170
  }
1171
+ function isRlsTenancy(env = process.env) {
1172
+ return readTenancyDriver(env) === "rls";
1173
+ }
1149
1174
 
1150
1175
  // ../../src/core/tenant/resolveTenant.ts
1151
1176
  async function resolveTenant(tenantId) {
@@ -1173,7 +1198,7 @@ async function applyTenantContextToTransaction(transaction, tenantId) {
1173
1198
  await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
1174
1199
  }
1175
1200
  async function runWithTenantDatabase(tenant, callback) {
1176
- if (!isTenancyEnabled()) {
1201
+ if (!isTenancyEnabled() || !isRlsTenancy()) {
1177
1202
  return await runWithTenant(tenant, callback);
1178
1203
  }
1179
1204
  if (hasActiveDatabaseConnection()) {
@@ -8813,6 +8838,9 @@ import { createHash } from "crypto";
8813
8838
 
8814
8839
  // ../../src/core/tenant/databaseTenantContext.ts
8815
8840
  async function runWithMigrationBypass(callback) {
8841
+ if (!isRlsTenancy()) {
8842
+ return await callback();
8843
+ }
8816
8844
  await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
8817
8845
  try {
8818
8846
  return await callback();
@@ -9545,6 +9573,7 @@ export {
9545
9573
  isHttpErrorLike,
9546
9574
  isInsideTenantDatabaseScope,
9547
9575
  isPublicReadsEnabled,
9576
+ isRlsTenancy,
9548
9577
  isTenancyEnabled,
9549
9578
  jobRegistry,
9550
9579
  jsonResponse2 as jsonResponse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",