@getstrata/starter 2.0.6 → 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 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
- const database = appDatabaseName(projectName);
2747
- const role = GENERATED_POSTGRES_APP_ROLE;
2748
- const password = GENERATED_POSTGRES_APP_PASSWORD;
2749
- return `-- Application login role. FORCE RLS applies because this role is NOSUPERUSER and NOBYPASSRLS.
2750
- -- Superuser remains for CREATE ROLE / CREATE DATABASE / GRANT / migrate.
2751
- -- This script is repeatable on an already-existing volume (not only docker-entrypoint-initdb.d).
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.6",
3067
- "@getstrata/cli": "^2.0.6",
3068
- "@getstrata/core": "^2.0.6"
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.6",
16
- "@getstrata/cli": "^2.0.6",
17
- "@getstrata/core": "^2.0.6",
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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/starter",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Interactive create-strata wizard. Choose each layer; one database engine.",
5
5
  "type": "module",
6
6
  "license": "MIT",