@getstrata/starter 2.0.5 → 2.0.7
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/cli.js +158 -129
- package/dist/templates/package.json +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2697,6 +2697,155 @@ export default authProvider;
|
|
|
2697
2697
|
`;
|
|
2698
2698
|
}
|
|
2699
2699
|
|
|
2700
|
+
// ../../src/core/runtime/appEnv.ts
|
|
2701
|
+
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
2702
|
+
|
|
2703
|
+
// ../../src/core/tenant/postgresAppRole.ts
|
|
2704
|
+
var POSTGRES_APP_ROLE = "strata_app";
|
|
2705
|
+
var POSTGRES_APP_ROLE_PASSWORD = "dev-strata-app-change-me";
|
|
2706
|
+
function assertSafeSqlIdentifier(identifier, label) {
|
|
2707
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
2708
|
+
throw new Error(`Refusing unsafe Postgres ${label}: ${identifier}`);
|
|
2709
|
+
}
|
|
2710
|
+
return identifier;
|
|
2711
|
+
}
|
|
2712
|
+
function quoteSqlLiteral(value) {
|
|
2713
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
2714
|
+
}
|
|
2715
|
+
function postgresAppRoleCreateSql(options = {}) {
|
|
2716
|
+
const role = assertSafeSqlIdentifier(options.role ?? POSTGRES_APP_ROLE, "role name");
|
|
2717
|
+
const password = quoteSqlLiteral(options.password ?? POSTGRES_APP_ROLE_PASSWORD);
|
|
2718
|
+
return `DO $$
|
|
2719
|
+
BEGIN
|
|
2720
|
+
-- pg_authid is shared across databases. Database-local advisory locks cannot
|
|
2721
|
+
-- serialize a cluster-global role. This lock permits readers and lasts until commit.
|
|
2722
|
+
LOCK TABLE pg_catalog.pg_authid IN SHARE ROW EXCLUSIVE MODE;
|
|
2723
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
|
|
2724
|
+
CREATE ROLE ${role} LOGIN PASSWORD ${password}
|
|
2725
|
+
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
|
2726
|
+
ELSE
|
|
2727
|
+
ALTER ROLE ${role} WITH LOGIN PASSWORD ${password}
|
|
2728
|
+
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
|
2729
|
+
END IF;
|
|
2730
|
+
END
|
|
2731
|
+
$$`;
|
|
2732
|
+
}
|
|
2733
|
+
function postgresAppRoleSql(options) {
|
|
2734
|
+
const database = assertSafeSqlIdentifier(options.database, "database name");
|
|
2735
|
+
const role = assertSafeSqlIdentifier(options.role ?? POSTGRES_APP_ROLE, "role name");
|
|
2736
|
+
return `-- Application login role. FORCE RLS applies because this role is NOSUPERUSER and NOBYPASSRLS.
|
|
2737
|
+
-- Superuser remains for CREATE ROLE / CREATE DATABASE / GRANT / migrate / migrate:fresh DROP.
|
|
2738
|
+
-- This script is repeatable on an already-existing volume (not only docker-entrypoint-initdb.d).
|
|
2739
|
+
${postgresAppRoleCreateSql(options)};
|
|
2740
|
+
|
|
2741
|
+
GRANT CONNECT ON DATABASE ${database} TO ${role};
|
|
2742
|
+
GRANT USAGE, CREATE ON SCHEMA public TO ${role};
|
|
2743
|
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${role};
|
|
2744
|
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${role};
|
|
2745
|
+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO ${role};
|
|
2746
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${role};
|
|
2747
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${role};
|
|
2748
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO ${role};
|
|
2749
|
+
`;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// ../../src/core/tenant/enableTenantRls.ts
|
|
2753
|
+
function assertSafeIdentifier(identifier) {
|
|
2754
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
2755
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
2756
|
+
}
|
|
2757
|
+
return identifier;
|
|
2758
|
+
}
|
|
2759
|
+
var RLS_HELPER_SQL = `
|
|
2760
|
+
CREATE OR REPLACE FUNCTION app_bypass_rls()
|
|
2761
|
+
RETURNS BOOLEAN AS $$
|
|
2762
|
+
BEGIN
|
|
2763
|
+
RETURN COALESCE(current_setting('app.bypass_rls', true), 'false') = 'true';
|
|
2764
|
+
EXCEPTION
|
|
2765
|
+
WHEN others THEN
|
|
2766
|
+
RETURN FALSE;
|
|
2767
|
+
END;
|
|
2768
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
2769
|
+
|
|
2770
|
+
CREATE OR REPLACE FUNCTION app_bypass_identifier()
|
|
2771
|
+
RETURNS TEXT AS $$
|
|
2772
|
+
BEGIN
|
|
2773
|
+
RETURN NULLIF(current_setting('app.bypass_identifier', true), '');
|
|
2774
|
+
EXCEPTION
|
|
2775
|
+
WHEN others THEN
|
|
2776
|
+
RETURN NULL;
|
|
2777
|
+
END;
|
|
2778
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
2779
|
+
|
|
2780
|
+
CREATE OR REPLACE FUNCTION app_current_tenant_id()
|
|
2781
|
+
RETURNS INTEGER AS $$
|
|
2782
|
+
BEGIN
|
|
2783
|
+
RETURN NULLIF(current_setting('app.tenant_id', true), '')::INTEGER;
|
|
2784
|
+
EXCEPTION
|
|
2785
|
+
WHEN others THEN
|
|
2786
|
+
RETURN NULL;
|
|
2787
|
+
END;
|
|
2788
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
2789
|
+
`;
|
|
2790
|
+
function tenantIsolationPolicy(table, predicate) {
|
|
2791
|
+
return `
|
|
2792
|
+
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
2793
|
+
ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;
|
|
2794
|
+
DROP POLICY IF EXISTS tenant_isolation ON ${table};
|
|
2795
|
+
CREATE POLICY tenant_isolation ON ${table}
|
|
2796
|
+
USING (
|
|
2797
|
+
app_bypass_rls()
|
|
2798
|
+
OR ${predicate}
|
|
2799
|
+
)
|
|
2800
|
+
WITH CHECK (
|
|
2801
|
+
app_bypass_rls()
|
|
2802
|
+
OR ${predicate}
|
|
2803
|
+
);
|
|
2804
|
+
`;
|
|
2805
|
+
}
|
|
2806
|
+
function enableTenantRlsSql(tableName) {
|
|
2807
|
+
const table = assertSafeIdentifier(tableName);
|
|
2808
|
+
return tenantIsolationPolicy(table, `tenant_id = app_current_tenant_id()`);
|
|
2809
|
+
}
|
|
2810
|
+
function enableUsersRlsSql() {
|
|
2811
|
+
return tenantIsolationPolicy("users", `tenant_id = app_current_tenant_id()
|
|
2812
|
+
OR (
|
|
2813
|
+
app_bypass_identifier() IS NOT NULL
|
|
2814
|
+
AND (
|
|
2815
|
+
id::text = app_bypass_identifier()
|
|
2816
|
+
OR email = app_bypass_identifier()
|
|
2817
|
+
)
|
|
2818
|
+
)`);
|
|
2819
|
+
}
|
|
2820
|
+
function userOwnedIdentifierPredicate(table, userIdColumn) {
|
|
2821
|
+
const userIdMatch = `${table}.${userIdColumn}::text = app_bypass_identifier()`;
|
|
2822
|
+
if (table === "sessions") {
|
|
2823
|
+
return `(app_bypass_identifier() IS NOT NULL AND (${table}.id::text = app_bypass_identifier() OR ${userIdMatch}))`;
|
|
2824
|
+
}
|
|
2825
|
+
if (table === "api_tokens" || table === "auth_one_time_tokens") {
|
|
2826
|
+
return `(app_bypass_identifier() IS NOT NULL AND (${table}.token_hash = app_bypass_identifier() OR ${userIdMatch}))`;
|
|
2827
|
+
}
|
|
2828
|
+
return `(app_bypass_identifier() IS NOT NULL AND ${userIdMatch})`;
|
|
2829
|
+
}
|
|
2830
|
+
function enableUserOwnedRlsSql(tableName, userIdColumn = "user_id") {
|
|
2831
|
+
const table = assertSafeIdentifier(tableName);
|
|
2832
|
+
const column = assertSafeIdentifier(userIdColumn);
|
|
2833
|
+
return tenantIsolationPolicy(table, `EXISTS (
|
|
2834
|
+
SELECT 1 FROM users u
|
|
2835
|
+
WHERE u.id = ${table}.${column}
|
|
2836
|
+
AND u.tenant_id = app_current_tenant_id()
|
|
2837
|
+
)
|
|
2838
|
+
OR ${userOwnedIdentifierPredicate(table, column)}`);
|
|
2839
|
+
}
|
|
2840
|
+
function generatedRlsBootstrapSql(tables, userOwnedTables = []) {
|
|
2841
|
+
return [
|
|
2842
|
+
RLS_HELPER_SQL,
|
|
2843
|
+
...tables.map((table) => table === "users" ? enableUsersRlsSql() : enableTenantRlsSql(table)),
|
|
2844
|
+
...userOwnedTables.map((table) => enableUserOwnedRlsSql(table))
|
|
2845
|
+
].join(`
|
|
2846
|
+
`);
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2700
2849
|
// src/renderEnv.ts
|
|
2701
2850
|
function envFlag(value) {
|
|
2702
2851
|
return value ? "true" : "false";
|
|
@@ -2743,32 +2892,12 @@ function renderPostgresAppRoleInitSql(projectName, layers) {
|
|
|
2743
2892
|
if (layers.database !== "postgres") {
|
|
2744
2893
|
return null;
|
|
2745
2894
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
DO $$
|
|
2753
|
-
BEGIN
|
|
2754
|
-
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
|
|
2755
|
-
CREATE ROLE ${role} LOGIN PASSWORD '${password}'
|
|
2756
|
-
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
|
2757
|
-
ELSE
|
|
2758
|
-
ALTER ROLE ${role} WITH LOGIN PASSWORD '${password}'
|
|
2759
|
-
NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
|
2760
|
-
END IF;
|
|
2761
|
-
END
|
|
2762
|
-
$$;
|
|
2763
|
-
|
|
2764
|
-
GRANT CONNECT ON DATABASE ${database} TO ${role};
|
|
2765
|
-
GRANT USAGE, CREATE ON SCHEMA public TO ${role};
|
|
2766
|
-
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${role};
|
|
2767
|
-
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${role};
|
|
2768
|
-
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO ${role};
|
|
2769
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${role};
|
|
2770
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${role};
|
|
2771
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO ${role};
|
|
2895
|
+
return `BEGIN;
|
|
2896
|
+
${postgresAppRoleSql({
|
|
2897
|
+
database: appDatabaseName(projectName),
|
|
2898
|
+
role: GENERATED_POSTGRES_APP_ROLE,
|
|
2899
|
+
password: GENERATED_POSTGRES_APP_PASSWORD
|
|
2900
|
+
})}COMMIT;
|
|
2772
2901
|
`;
|
|
2773
2902
|
}
|
|
2774
2903
|
function renderEnvExample(projectName, layers) {
|
|
@@ -3063,9 +3192,9 @@ function renderPackageJson(projectName, options = {}) {
|
|
|
3063
3192
|
"@getstrata/cli": "workspace:*",
|
|
3064
3193
|
"@getstrata/core": "workspace:*"
|
|
3065
3194
|
} : {
|
|
3066
|
-
"@getstrata/bootstrap": "^2.0.
|
|
3067
|
-
"@getstrata/cli": "^2.0.
|
|
3068
|
-
"@getstrata/core": "^2.0.
|
|
3195
|
+
"@getstrata/bootstrap": "^2.0.7",
|
|
3196
|
+
"@getstrata/cli": "^2.0.7",
|
|
3197
|
+
"@getstrata/core": "^2.0.7"
|
|
3069
3198
|
};
|
|
3070
3199
|
coreDeps.eta = "^4.6.0";
|
|
3071
3200
|
if (options.layers?.database === "mysql") {
|
|
@@ -3413,106 +3542,6 @@ ${authUsesCookie(layers.auth) ? "- Set `SESSION_SECRET` to 32+ characters.\n" :
|
|
|
3413
3542
|
`;
|
|
3414
3543
|
}
|
|
3415
3544
|
|
|
3416
|
-
// ../../src/core/runtime/appEnv.ts
|
|
3417
|
-
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
3418
|
-
|
|
3419
|
-
// ../../src/core/tenant/enableTenantRls.ts
|
|
3420
|
-
function assertSafeIdentifier(identifier) {
|
|
3421
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
3422
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
3423
|
-
}
|
|
3424
|
-
return identifier;
|
|
3425
|
-
}
|
|
3426
|
-
var RLS_HELPER_SQL = `
|
|
3427
|
-
CREATE OR REPLACE FUNCTION app_bypass_rls()
|
|
3428
|
-
RETURNS BOOLEAN AS $$
|
|
3429
|
-
BEGIN
|
|
3430
|
-
RETURN COALESCE(current_setting('app.bypass_rls', true), 'false') = 'true';
|
|
3431
|
-
EXCEPTION
|
|
3432
|
-
WHEN others THEN
|
|
3433
|
-
RETURN FALSE;
|
|
3434
|
-
END;
|
|
3435
|
-
$$ LANGUAGE plpgsql STABLE;
|
|
3436
|
-
|
|
3437
|
-
CREATE OR REPLACE FUNCTION app_bypass_identifier()
|
|
3438
|
-
RETURNS TEXT AS $$
|
|
3439
|
-
BEGIN
|
|
3440
|
-
RETURN NULLIF(current_setting('app.bypass_identifier', true), '');
|
|
3441
|
-
EXCEPTION
|
|
3442
|
-
WHEN others THEN
|
|
3443
|
-
RETURN NULL;
|
|
3444
|
-
END;
|
|
3445
|
-
$$ LANGUAGE plpgsql STABLE;
|
|
3446
|
-
|
|
3447
|
-
CREATE OR REPLACE FUNCTION app_current_tenant_id()
|
|
3448
|
-
RETURNS INTEGER AS $$
|
|
3449
|
-
BEGIN
|
|
3450
|
-
RETURN NULLIF(current_setting('app.tenant_id', true), '')::INTEGER;
|
|
3451
|
-
EXCEPTION
|
|
3452
|
-
WHEN others THEN
|
|
3453
|
-
RETURN NULL;
|
|
3454
|
-
END;
|
|
3455
|
-
$$ LANGUAGE plpgsql STABLE;
|
|
3456
|
-
`;
|
|
3457
|
-
function tenantIsolationPolicy(table, predicate) {
|
|
3458
|
-
return `
|
|
3459
|
-
ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
|
|
3460
|
-
ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;
|
|
3461
|
-
DROP POLICY IF EXISTS tenant_isolation ON ${table};
|
|
3462
|
-
CREATE POLICY tenant_isolation ON ${table}
|
|
3463
|
-
USING (
|
|
3464
|
-
app_bypass_rls()
|
|
3465
|
-
OR ${predicate}
|
|
3466
|
-
)
|
|
3467
|
-
WITH CHECK (
|
|
3468
|
-
app_bypass_rls()
|
|
3469
|
-
OR ${predicate}
|
|
3470
|
-
);
|
|
3471
|
-
`;
|
|
3472
|
-
}
|
|
3473
|
-
function enableTenantRlsSql(tableName) {
|
|
3474
|
-
const table = assertSafeIdentifier(tableName);
|
|
3475
|
-
return tenantIsolationPolicy(table, `tenant_id = app_current_tenant_id()`);
|
|
3476
|
-
}
|
|
3477
|
-
function enableUsersRlsSql() {
|
|
3478
|
-
return tenantIsolationPolicy("users", `tenant_id = app_current_tenant_id()
|
|
3479
|
-
OR (
|
|
3480
|
-
app_bypass_identifier() IS NOT NULL
|
|
3481
|
-
AND (
|
|
3482
|
-
id::text = app_bypass_identifier()
|
|
3483
|
-
OR email = app_bypass_identifier()
|
|
3484
|
-
)
|
|
3485
|
-
)`);
|
|
3486
|
-
}
|
|
3487
|
-
function userOwnedIdentifierPredicate(table, userIdColumn) {
|
|
3488
|
-
const userIdMatch = `${table}.${userIdColumn}::text = app_bypass_identifier()`;
|
|
3489
|
-
if (table === "sessions") {
|
|
3490
|
-
return `(app_bypass_identifier() IS NOT NULL AND (${table}.id::text = app_bypass_identifier() OR ${userIdMatch}))`;
|
|
3491
|
-
}
|
|
3492
|
-
if (table === "api_tokens" || table === "auth_one_time_tokens") {
|
|
3493
|
-
return `(app_bypass_identifier() IS NOT NULL AND (${table}.token_hash = app_bypass_identifier() OR ${userIdMatch}))`;
|
|
3494
|
-
}
|
|
3495
|
-
return `(app_bypass_identifier() IS NOT NULL AND ${userIdMatch})`;
|
|
3496
|
-
}
|
|
3497
|
-
function enableUserOwnedRlsSql(tableName, userIdColumn = "user_id") {
|
|
3498
|
-
const table = assertSafeIdentifier(tableName);
|
|
3499
|
-
const column = assertSafeIdentifier(userIdColumn);
|
|
3500
|
-
return tenantIsolationPolicy(table, `EXISTS (
|
|
3501
|
-
SELECT 1 FROM users u
|
|
3502
|
-
WHERE u.id = ${table}.${column}
|
|
3503
|
-
AND u.tenant_id = app_current_tenant_id()
|
|
3504
|
-
)
|
|
3505
|
-
OR ${userOwnedIdentifierPredicate(table, column)}`);
|
|
3506
|
-
}
|
|
3507
|
-
function generatedRlsBootstrapSql(tables, userOwnedTables = []) {
|
|
3508
|
-
return [
|
|
3509
|
-
RLS_HELPER_SQL,
|
|
3510
|
-
...tables.map((table) => table === "users" ? enableUsersRlsSql() : enableTenantRlsSql(table)),
|
|
3511
|
-
...userOwnedTables.map((table) => enableUserOwnedRlsSql(table))
|
|
3512
|
-
].join(`
|
|
3513
|
-
`);
|
|
3514
|
-
}
|
|
3515
|
-
|
|
3516
3545
|
// src/renderRuntime.ts
|
|
3517
3546
|
function needsEnsure(layers) {
|
|
3518
3547
|
return layers.database !== "sqlite";
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"check": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@getstrata/bootstrap": "^2.0.
|
|
16
|
-
"@getstrata/cli": "^2.0.
|
|
17
|
-
"@getstrata/core": "^2.0.
|
|
15
|
+
"@getstrata/bootstrap": "^2.0.7",
|
|
16
|
+
"@getstrata/cli": "^2.0.7",
|
|
17
|
+
"@getstrata/core": "^2.0.7",
|
|
18
18
|
"eta": "^4.6.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|