@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 +9 -0
- package/README.md +3 -3
- package/dist/core/contracts/authUserDirectory.d.ts +4 -0
- package/dist/core/tenant/tenancyConfig.d.ts +4 -2
- package/dist/entries/audit/exportAuditLogs.js +22 -0
- package/dist/entries/auth/scimAuthMiddleware.js +11 -2
- package/dist/entries/jobs/exportAuditLogsJob.js +22 -0
- package/dist/entries/tenant/databaseTenantContext.js +22 -0
- package/dist/entries/tenant/tenancyConfig.js +11 -1
- package/dist/entries/tenant/tenantDatabaseScope.js +11 -2
- package/dist/framework/public-api.d.ts +1 -1
- package/dist/index.js +49 -20
- package/package.json +1 -1
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)
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1012
|
-
|
|
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
|
-
|
|
1023
|
+
boundConnectionState().connection = connection;
|
|
1016
1024
|
}
|
|
1017
1025
|
function getBoundDatabaseConnection() {
|
|
1018
|
-
return
|
|
1026
|
+
return boundConnectionState().connection;
|
|
1019
1027
|
}
|
|
1020
1028
|
function resetBoundDatabaseConnection() {
|
|
1021
|
-
|
|
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
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1080
|
+
const holder = defaultConnectionState();
|
|
1081
|
+
holder.pool = connection;
|
|
1082
|
+
holder.query = createDatabaseQueryProxy(connection);
|
|
1069
1083
|
}
|
|
1070
1084
|
function getDefaultDatabasePool() {
|
|
1071
|
-
|
|
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
|
|
1089
|
+
return pool;
|
|
1075
1090
|
}
|
|
1076
1091
|
function getDefaultDatabaseQuery() {
|
|
1077
|
-
|
|
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
|
|
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
|
-
|
|
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,
|