@stratum-hq/lib 0.2.0 → 0.2.3
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/crypto.d.ts +3 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +23 -5
- package/dist/crypto.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/migrate.d.ts +8 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +61 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/001_init.sql +136 -0
- package/dist/migrations/002_schema_isolation.sql +6 -0
- package/dist/migrations/003_db_isolation.sql +12 -0
- package/dist/migrations/004_webhooks.sql +53 -0
- package/dist/migrations/005_audit_logs.sql +21 -0
- package/dist/migrations/006_api_key_scopes.sql +2 -0
- package/dist/migrations/007_sensitive_config.sql +1 -0
- package/dist/migrations/008_api_key_management.sql +7 -0
- package/dist/migrations/009_consent.sql +18 -0
- package/dist/migrations/010_multi_region.sql +19 -0
- package/dist/migrations/011_demo_bootstrap.sql +5 -0
- package/dist/migrations/012_roles.sql +18 -0
- package/dist/migrations/013_api_key_hmac.sql +4 -0
- package/dist/migrations/014_sort_order.sql +5 -0
- package/dist/migrations/015_region_url_encryption.sql +5 -0
- package/dist/migrations/016_migration_alias.sql +8 -0
- package/dist/migrations/migrations/001_init.sql +145 -0
- package/dist/migrations/migrations/002_schema_isolation.sql +6 -0
- package/dist/migrations/migrations/003_db_isolation.sql +12 -0
- package/dist/migrations/migrations/004_webhooks.sql +53 -0
- package/dist/migrations/migrations/005_audit_logs.sql +21 -0
- package/dist/migrations/migrations/006_api_key_scopes.sql +2 -0
- package/dist/migrations/migrations/007_sensitive_config.sql +1 -0
- package/dist/migrations/migrations/008_api_key_management.sql +7 -0
- package/dist/migrations/migrations/009_consent.sql +18 -0
- package/dist/migrations/migrations/010_multi_region.sql +19 -0
- package/dist/migrations/migrations/011_demo_bootstrap.sql +5 -0
- package/dist/migrations/migrations/012_roles.sql +18 -0
- package/dist/migrations/migrations/013_api_key_hmac.sql +4 -0
- package/dist/migrations/migrations/014_sort_order.sql +5 -0
- package/dist/migrations/migrations/015_region_url_encryption.sql +5 -0
- package/dist/migrations/migrations/016_migration_alias.sql +8 -0
- package/dist/migrations/migrations/017_abac_policies.sql +16 -0
- package/dist/services/__tests__/abac-service.test.d.ts +2 -0
- package/dist/services/__tests__/abac-service.test.d.ts.map +1 -0
- package/dist/services/__tests__/abac-service.test.js +108 -0
- package/dist/services/__tests__/abac-service.test.js.map +1 -0
- package/dist/services/__tests__/retention-service.test.js +7 -4
- package/dist/services/__tests__/retention-service.test.js.map +1 -1
- package/dist/services/abac-service.d.ts +41 -0
- package/dist/services/abac-service.d.ts.map +1 -0
- package/dist/services/abac-service.js +268 -0
- package/dist/services/abac-service.js.map +1 -0
- package/dist/services/config-service.d.ts.map +1 -1
- package/dist/services/config-service.js +8 -5
- package/dist/services/config-service.js.map +1 -1
- package/dist/services/event-service.d.ts.map +1 -1
- package/dist/services/event-service.js +1 -0
- package/dist/services/event-service.js.map +1 -1
- package/dist/services/key-rotation-service.d.ts +5 -5
- package/dist/services/key-rotation-service.d.ts.map +1 -1
- package/dist/services/key-rotation-service.js +42 -29
- package/dist/services/key-rotation-service.js.map +1 -1
- package/dist/services/retention-service.d.ts.map +1 -1
- package/dist/services/retention-service.js +3 -1
- package/dist/services/retention-service.js.map +1 -1
- package/dist/services/tenant-service.js +4 -4
- package/dist/services/tenant-service.js.map +1 -1
- package/dist/services/webhook-service.js +1 -1
- package/dist/stratum.d.ts +35 -1
- package/dist/stratum.d.ts.map +1 -1
- package/dist/stratum.js +170 -0
- package/dist/stratum.js.map +1 -1
- package/package.json +2 -2
package/dist/crypto.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Encrypts a value. Returns versioned format: v1:iv:authTag:ciphertext (all hex) */
|
|
2
2
|
export declare function encrypt(plaintext: string): string;
|
|
3
|
-
/** Decrypts a versioned encrypted value. Supports v1 format and legacy (no version prefix).
|
|
3
|
+
/** Decrypts a versioned encrypted value. Supports v1 format and legacy (no version prefix).
|
|
4
|
+
* Falls back to STRATUM_ENCRYPTION_KEY_PREVIOUS if primary key decryption fails,
|
|
5
|
+
* enabling dual-key reads during rolling deployments. */
|
|
4
6
|
export declare function decrypt(encrypted: string): string;
|
|
5
7
|
/** Re-encrypts a value with a new key. Used for key rotation. Safe under concurrency — does not mutate process.env. */
|
|
6
8
|
export declare function reEncrypt(encrypted: string, oldKeyMaterial: string, newKeyMaterial: string): string;
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AA6DA,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;yDAEyD;AACzD,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAcjD;AAED,uHAAuH;AACvH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAGnG"}
|
package/dist/crypto.js
CHANGED
|
@@ -11,9 +11,11 @@ const ALGORITHM = "aes-256-gcm";
|
|
|
11
11
|
const IV_LENGTH = 12;
|
|
12
12
|
const AUTH_TAG_LENGTH = 16;
|
|
13
13
|
const CURRENT_KEY_VERSION = "v1";
|
|
14
|
+
const HKDF_SALT = process.env.STRATUM_HKDF_SALT
|
|
15
|
+
? Buffer.from(process.env.STRATUM_HKDF_SALT, "hex")
|
|
16
|
+
: Buffer.alloc(32, 0);
|
|
14
17
|
function hkdfDeriveKey(keyMaterial, info = "stratum-aes-key") {
|
|
15
|
-
|
|
16
|
-
return Buffer.from(node_crypto_1.default.hkdfSync("sha256", Buffer.from(keyMaterial, "utf8"), salt, info, 32));
|
|
18
|
+
return Buffer.from(node_crypto_1.default.hkdfSync("sha256", Buffer.from(keyMaterial, "utf8"), HKDF_SALT, info, 32));
|
|
17
19
|
}
|
|
18
20
|
function getEncryptionKey() {
|
|
19
21
|
const envKey = process.env.STRATUM_ENCRYPTION_KEY ?? process.env.WEBHOOK_ENCRYPTION_KEY;
|
|
@@ -27,7 +29,7 @@ function getEncryptionKey() {
|
|
|
27
29
|
return hkdfDeriveKey("stratum-dev-key");
|
|
28
30
|
}
|
|
29
31
|
function deriveKey(keyMaterial) {
|
|
30
|
-
return hkdfDeriveKey(keyMaterial, "stratum-
|
|
32
|
+
return hkdfDeriveKey(keyMaterial, "stratum-aes-key");
|
|
31
33
|
}
|
|
32
34
|
function encryptWithKey(plaintext, key) {
|
|
33
35
|
const iv = node_crypto_1.default.randomBytes(IV_LENGTH);
|
|
@@ -61,9 +63,25 @@ function decryptWithKey(encrypted, key) {
|
|
|
61
63
|
function encrypt(plaintext) {
|
|
62
64
|
return encryptWithKey(plaintext, getEncryptionKey());
|
|
63
65
|
}
|
|
64
|
-
/** Decrypts a versioned encrypted value. Supports v1 format and legacy (no version prefix).
|
|
66
|
+
/** Decrypts a versioned encrypted value. Supports v1 format and legacy (no version prefix).
|
|
67
|
+
* Falls back to STRATUM_ENCRYPTION_KEY_PREVIOUS if primary key decryption fails,
|
|
68
|
+
* enabling dual-key reads during rolling deployments. */
|
|
65
69
|
function decrypt(encrypted) {
|
|
66
|
-
|
|
70
|
+
try {
|
|
71
|
+
return decryptWithKey(encrypted, getEncryptionKey());
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const previousKey = process.env.STRATUM_ENCRYPTION_KEY_PREVIOUS;
|
|
75
|
+
if (previousKey) {
|
|
76
|
+
try {
|
|
77
|
+
return decryptWithKey(encrypted, hkdfDeriveKey(previousKey));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Both keys failed — throw the original error
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
67
85
|
}
|
|
68
86
|
/** Re-encrypts a value with a new key. Used for key rotation. Safe under concurrency — does not mutate process.env. */
|
|
69
87
|
function reEncrypt(encrypted, oldKeyMaterial, newKeyMaterial) {
|
package/dist/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Stratum, type StratumOptions } from "./stratum.js";
|
|
2
|
+
export { migrate, type MigrateOptions } from "./migrate.js";
|
|
2
3
|
export { withClient, withTransaction } from "./pool-helpers.js";
|
|
3
4
|
export { traced, getTracer, isTracingEnabled } from "./telemetry.js";
|
|
4
5
|
export type { TenantNode, TenantContext, CreateTenantInput, UpdateTenantInput, MoveTenantInput, PaginationInput, PaginatedResult, ConfigEntry, SetConfigInput, ResolvedConfigEntry, ResolvedConfig, PermissionPolicy, CreatePermissionInput, UpdatePermissionInput, ResolvedPermission, } from "@stratum-hq/core";
|
|
@@ -8,4 +9,6 @@ export type { KeyRotationResult } from "./services/key-rotation-service.js";
|
|
|
8
9
|
export type { DeliveryStats } from "./services/event-service.js";
|
|
9
10
|
export type { Role, CreateRoleInput, UpdateRoleInput } from "./services/role-service.js";
|
|
10
11
|
export type { AuditContext, AuditEntry, AuditLogQuery } from "@stratum-hq/core";
|
|
12
|
+
export { defaultLogger, noopLogger } from "./logger.js";
|
|
13
|
+
export type { StratumLogger } from "./logger.js";
|
|
11
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGrE,YAAY,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,eAAe,EACf,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACvH,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGzF,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGrE,YAAY,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,eAAe,EACf,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACvH,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,YAAY,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGzF,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isTracingEnabled = exports.getTracer = exports.traced = exports.withTransaction = exports.withClient = exports.Stratum = void 0;
|
|
3
|
+
exports.noopLogger = exports.defaultLogger = exports.isTracingEnabled = exports.getTracer = exports.traced = exports.withTransaction = exports.withClient = exports.migrate = exports.Stratum = void 0;
|
|
4
4
|
var stratum_js_1 = require("./stratum.js");
|
|
5
5
|
Object.defineProperty(exports, "Stratum", { enumerable: true, get: function () { return stratum_js_1.Stratum; } });
|
|
6
|
+
var migrate_js_1 = require("./migrate.js");
|
|
7
|
+
Object.defineProperty(exports, "migrate", { enumerable: true, get: function () { return migrate_js_1.migrate; } });
|
|
6
8
|
var pool_helpers_js_1 = require("./pool-helpers.js");
|
|
7
9
|
Object.defineProperty(exports, "withClient", { enumerable: true, get: function () { return pool_helpers_js_1.withClient; } });
|
|
8
10
|
Object.defineProperty(exports, "withTransaction", { enumerable: true, get: function () { return pool_helpers_js_1.withTransaction; } });
|
|
@@ -10,4 +12,7 @@ var telemetry_js_1 = require("./telemetry.js");
|
|
|
10
12
|
Object.defineProperty(exports, "traced", { enumerable: true, get: function () { return telemetry_js_1.traced; } });
|
|
11
13
|
Object.defineProperty(exports, "getTracer", { enumerable: true, get: function () { return telemetry_js_1.getTracer; } });
|
|
12
14
|
Object.defineProperty(exports, "isTracingEnabled", { enumerable: true, get: function () { return telemetry_js_1.isTracingEnabled; } });
|
|
15
|
+
var logger_js_1 = require("./logger.js");
|
|
16
|
+
Object.defineProperty(exports, "defaultLogger", { enumerable: true, get: function () { return logger_js_1.defaultLogger; } });
|
|
17
|
+
Object.defineProperty(exports, "noopLogger", { enumerable: true, get: function () { return logger_js_1.noopLogger; } });
|
|
13
18
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA4D;AAAnD,qGAAA,OAAO,OAAA;AAChB,qDAAgE;AAAvD,6GAAA,UAAU,OAAA;AAAE,kHAAA,eAAe,OAAA;AACpC,+CAAqE;AAA5D,sGAAA,MAAM,OAAA;AAAE,yGAAA,SAAS,OAAA;AAAE,gHAAA,gBAAgB,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA4D;AAAnD,qGAAA,OAAO,OAAA;AAChB,2CAA4D;AAAnD,qGAAA,OAAO,OAAA;AAChB,qDAAgE;AAAvD,6GAAA,UAAU,OAAA;AAAE,kHAAA,eAAe,OAAA;AACpC,+CAAqE;AAA5D,sGAAA,MAAM,OAAA;AAAE,yGAAA,SAAS,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AA8B5C,yCAAwD;AAA/C,0GAAA,aAAa,OAAA;AAAE,uGAAA,UAAU,OAAA"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structured logger for Stratum.
|
|
3
|
+
* Outputs JSON lines when NODE_ENV=production, human-readable otherwise.
|
|
4
|
+
* Can be replaced by passing a custom logger to Stratum constructor.
|
|
5
|
+
*/
|
|
6
|
+
export interface StratumLogger {
|
|
7
|
+
info(msg: string, ctx?: Record<string, unknown>): void;
|
|
8
|
+
warn(msg: string, ctx?: Record<string, unknown>): void;
|
|
9
|
+
error(msg: string, ctx?: Record<string, unknown>): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const defaultLogger: StratumLogger;
|
|
12
|
+
/** No-op logger for testing or when logging is disabled. */
|
|
13
|
+
export declare const noopLogger: StratumLogger;
|
|
14
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzD;AAqBD,eAAO,MAAM,aAAa,EAAE,aAU3B,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,UAAU,EAAE,aAIxB,CAAC"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal structured logger for Stratum.
|
|
4
|
+
* Outputs JSON lines when NODE_ENV=production, human-readable otherwise.
|
|
5
|
+
* Can be replaced by passing a custom logger to Stratum constructor.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noopLogger = exports.defaultLogger = void 0;
|
|
9
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
10
|
+
function formatLog(level, msg, ctx) {
|
|
11
|
+
if (isProduction) {
|
|
12
|
+
return JSON.stringify({
|
|
13
|
+
ts: new Date().toISOString(),
|
|
14
|
+
level,
|
|
15
|
+
msg,
|
|
16
|
+
...ctx,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const ctxStr = ctx ? ` ${JSON.stringify(ctx)}` : "";
|
|
20
|
+
return `[stratum:${level}] ${msg}${ctxStr}`;
|
|
21
|
+
}
|
|
22
|
+
exports.defaultLogger = {
|
|
23
|
+
info(msg, ctx) {
|
|
24
|
+
console.log(formatLog("info", msg, ctx));
|
|
25
|
+
},
|
|
26
|
+
warn(msg, ctx) {
|
|
27
|
+
console.warn(formatLog("warn", msg, ctx));
|
|
28
|
+
},
|
|
29
|
+
error(msg, ctx) {
|
|
30
|
+
console.error(formatLog("error", msg, ctx));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
/** No-op logger for testing or when logging is disabled. */
|
|
34
|
+
exports.noopLogger = {
|
|
35
|
+
info() { },
|
|
36
|
+
warn() { },
|
|
37
|
+
error() { },
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAQH,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;AAE3D,SAAS,SAAS,CAChB,KAAa,EACb,GAAW,EACX,GAA6B;IAE7B,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,SAAS,CAAC;YACpB,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,KAAK;YACL,GAAG;YACH,GAAG,GAAG;SACP,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACpD,OAAO,YAAY,KAAK,KAAK,GAAG,GAAG,MAAM,EAAE,CAAC;AAC9C,CAAC;AAEY,QAAA,aAAa,GAAkB;IAC1C,IAAI,CAAC,GAAG,EAAE,GAAG;QACX,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,CAAC,GAAG,EAAE,GAAG;QACX,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,GAAG,EAAE,GAAG;QACZ,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC9C,CAAC;CACF,CAAC;AAEF,4DAA4D;AAC/C,QAAA,UAAU,GAAkB;IACvC,IAAI,KAAI,CAAC;IACT,IAAI,KAAI,CAAC;IACT,KAAK,KAAI,CAAC;CACX,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
export interface MigrateOptions {
|
|
3
|
+
pool: pg.Pool;
|
|
4
|
+
/** When true, SET stratum.enforce_rls = 'on' before running migrations (hard-fail on BYPASSRLS). */
|
|
5
|
+
enforceRls?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function migrate(options: MigrateOptions): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC;IACd,oGAAoG;IACpG,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA2DpE"}
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.migrate = migrate;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
async function migrate(options) {
|
|
10
|
+
const { pool, enforceRls } = options;
|
|
11
|
+
// Create migrations tracking table
|
|
12
|
+
await pool.query(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
14
|
+
id SERIAL PRIMARY KEY,
|
|
15
|
+
name TEXT NOT NULL UNIQUE,
|
|
16
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
17
|
+
)
|
|
18
|
+
`);
|
|
19
|
+
// Get migration files
|
|
20
|
+
const migrationsDir = node_path_1.default.resolve(__dirname, "migrations");
|
|
21
|
+
if (!node_fs_1.default.existsSync(migrationsDir)) {
|
|
22
|
+
throw new Error(`Migration files not found at ${migrationsDir}. Ensure the package was built with 'npm run build'.`);
|
|
23
|
+
}
|
|
24
|
+
const files = node_fs_1.default
|
|
25
|
+
.readdirSync(migrationsDir)
|
|
26
|
+
.filter((f) => f.endsWith(".sql"))
|
|
27
|
+
.sort();
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const client = await pool.connect();
|
|
30
|
+
try {
|
|
31
|
+
await client.query("BEGIN");
|
|
32
|
+
// Acquire advisory lock to prevent concurrent migrations
|
|
33
|
+
await client.query("SELECT pg_advisory_xact_lock(8675309)");
|
|
34
|
+
// Re-check if already applied (after lock, to prevent TOCTOU race)
|
|
35
|
+
const { rows } = await client.query("SELECT 1 FROM _migrations WHERE name = $1", [file]);
|
|
36
|
+
if (rows.length > 0) {
|
|
37
|
+
await client.query("COMMIT");
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Set RLS enforcement mode if requested
|
|
41
|
+
if (enforceRls) {
|
|
42
|
+
await client.query("SET LOCAL stratum.enforce_rls = 'on'");
|
|
43
|
+
}
|
|
44
|
+
const sql = node_fs_1.default.readFileSync(node_path_1.default.join(migrationsDir, file), "utf-8");
|
|
45
|
+
await client.query(sql);
|
|
46
|
+
await client.query("INSERT INTO _migrations (name) VALUES ($1)", [file]);
|
|
47
|
+
await client.query("COMMIT");
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
try {
|
|
51
|
+
await client.query("ROLLBACK");
|
|
52
|
+
}
|
|
53
|
+
catch { /* ignore rollback failure */ }
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
client.release();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../src/migrate.ts"],"names":[],"mappings":";;;;;AAUA,0BA2DC;AArED,sDAAyB;AACzB,0DAA6B;AAStB,KAAK,UAAU,OAAO,CAAC,OAAuB;IACnD,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IAErC,mCAAmC;IACnC,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;GAMhB,CAAC,CAAC;IAEH,sBAAsB;IACtB,MAAM,aAAa,GAAG,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC5D,IAAI,CAAC,iBAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CACb,gCAAgC,aAAa,sDAAsD,CACpG,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,iBAAE;SACb,WAAW,CAAC,aAAa,CAAC;SAC1B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SACjC,IAAI,EAAE,CAAC;IAEV,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAE5B,yDAAyD;YACzD,MAAM,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;YAE5D,mEAAmE;YACnE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC,2CAA2C,EAC3C,CAAC,IAAI,CAAC,CACP,CAAC;YACF,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC7B,SAAS;YACX,CAAC;YAED,wCAAwC;YACxC,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,MAAM,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;YAC7D,CAAC;YAED,MAAM,GAAG,GAAG,iBAAE,CAAC,YAAY,CAAC,mBAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;YACrE,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxB,MAAM,MAAM,CAAC,KAAK,CAAC,4CAA4C,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;YACzE,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC;gBAAC,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,6BAA6B,CAAC,CAAC;YAC/E,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
-- Extensions
|
|
2
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
3
|
+
CREATE EXTENSION IF NOT EXISTS "ltree";
|
|
4
|
+
|
|
5
|
+
-- Warn if application role has BYPASSRLS privilege.
|
|
6
|
+
-- In production, set stratum.enforce_rls = 'on' to make this a hard error.
|
|
7
|
+
DO $$ BEGIN
|
|
8
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = current_user AND rolbypassrls) THEN
|
|
9
|
+
IF current_setting('stratum.enforce_rls', true) = 'on' THEN
|
|
10
|
+
RAISE EXCEPTION 'SECURITY: Application role "%" has BYPASSRLS privilege. Create a dedicated role without BYPASSRLS, or set stratum.enforce_rls=off for development.', current_user;
|
|
11
|
+
ELSE
|
|
12
|
+
RAISE WARNING 'Role "%" has BYPASSRLS privilege. This is fine for development, but production should use a role without BYPASSRLS. Set stratum.enforce_rls=on to enforce.', current_user;
|
|
13
|
+
END IF;
|
|
14
|
+
END IF;
|
|
15
|
+
END $$;
|
|
16
|
+
|
|
17
|
+
-- Tenant nodes table
|
|
18
|
+
CREATE TABLE IF NOT EXISTS tenants (
|
|
19
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
20
|
+
parent_id UUID REFERENCES tenants(id) ON DELETE RESTRICT,
|
|
21
|
+
ancestry_path TEXT NOT NULL,
|
|
22
|
+
ancestry_ltree ltree,
|
|
23
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
slug TEXT NOT NULL UNIQUE
|
|
26
|
+
CHECK (slug ~ '^[a-z][a-z0-9_]{0,62}$'),
|
|
27
|
+
config JSONB NOT NULL DEFAULT '{}',
|
|
28
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
29
|
+
isolation_strategy TEXT NOT NULL DEFAULT 'SHARED_RLS'
|
|
30
|
+
CHECK (isolation_strategy IN ('SHARED_RLS')),
|
|
31
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
32
|
+
CHECK (status IN ('active', 'archived')),
|
|
33
|
+
deleted_at TIMESTAMPTZ,
|
|
34
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
35
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Auto-update updated_at function
|
|
39
|
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
40
|
+
RETURNS TRIGGER AS $$
|
|
41
|
+
BEGIN
|
|
42
|
+
NEW.updated_at = now();
|
|
43
|
+
RETURN NEW;
|
|
44
|
+
END;
|
|
45
|
+
$$ language 'plpgsql';
|
|
46
|
+
|
|
47
|
+
DROP TRIGGER IF EXISTS update_tenants_updated_at ON tenants;
|
|
48
|
+
CREATE TRIGGER update_tenants_updated_at
|
|
49
|
+
BEFORE UPDATE ON tenants
|
|
50
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
51
|
+
|
|
52
|
+
-- Trigger to maintain ancestry_ltree from parent's ltree + own slug
|
|
53
|
+
CREATE OR REPLACE FUNCTION maintain_ancestry_ltree()
|
|
54
|
+
RETURNS TRIGGER AS $$
|
|
55
|
+
BEGIN
|
|
56
|
+
IF NEW.parent_id IS NULL THEN
|
|
57
|
+
NEW.ancestry_ltree = NEW.slug::ltree;
|
|
58
|
+
ELSE
|
|
59
|
+
SELECT ancestry_ltree || NEW.slug::ltree INTO NEW.ancestry_ltree
|
|
60
|
+
FROM tenants WHERE id = NEW.parent_id;
|
|
61
|
+
END IF;
|
|
62
|
+
RETURN NEW;
|
|
63
|
+
END;
|
|
64
|
+
$$ language 'plpgsql';
|
|
65
|
+
|
|
66
|
+
DROP TRIGGER IF EXISTS maintain_tenant_ancestry_ltree ON tenants;
|
|
67
|
+
CREATE TRIGGER maintain_tenant_ancestry_ltree
|
|
68
|
+
BEFORE INSERT OR UPDATE OF parent_id, slug ON tenants
|
|
69
|
+
FOR EACH ROW EXECUTE FUNCTION maintain_ancestry_ltree();
|
|
70
|
+
|
|
71
|
+
-- Indexes for tree queries
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_parent ON tenants(parent_id);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_ancestry ON tenants USING GIST (ancestry_ltree);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_depth ON tenants(depth);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_slug ON tenants(slug);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_status ON tenants(status) WHERE status = 'active';
|
|
77
|
+
|
|
78
|
+
-- Permission policies table
|
|
79
|
+
CREATE TABLE IF NOT EXISTS permission_policies (
|
|
80
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
81
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE RESTRICT,
|
|
82
|
+
key TEXT NOT NULL,
|
|
83
|
+
value JSONB NOT NULL DEFAULT 'true',
|
|
84
|
+
mode TEXT NOT NULL DEFAULT 'INHERITED'
|
|
85
|
+
CHECK (mode IN ('LOCKED', 'INHERITED', 'DELEGATED')),
|
|
86
|
+
revocation_mode TEXT NOT NULL DEFAULT 'CASCADE'
|
|
87
|
+
CHECK (revocation_mode IN ('CASCADE', 'SOFT', 'PERMANENT')),
|
|
88
|
+
source_tenant_id UUID NOT NULL REFERENCES tenants(id),
|
|
89
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
90
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
91
|
+
UNIQUE(tenant_id, key)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_permission_source ON permission_policies(source_tenant_id);
|
|
95
|
+
|
|
96
|
+
DROP TRIGGER IF EXISTS update_permission_policies_updated_at ON permission_policies;
|
|
97
|
+
CREATE TRIGGER update_permission_policies_updated_at
|
|
98
|
+
BEFORE UPDATE ON permission_policies
|
|
99
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
100
|
+
|
|
101
|
+
-- Config entries table
|
|
102
|
+
CREATE TABLE IF NOT EXISTS config_entries (
|
|
103
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
104
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
105
|
+
key TEXT NOT NULL,
|
|
106
|
+
value JSONB NOT NULL,
|
|
107
|
+
inherited BOOLEAN NOT NULL DEFAULT false,
|
|
108
|
+
source_tenant_id UUID NOT NULL REFERENCES tenants(id),
|
|
109
|
+
locked BOOLEAN NOT NULL DEFAULT false,
|
|
110
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
111
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
112
|
+
UNIQUE(tenant_id, key)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
DROP TRIGGER IF EXISTS update_config_entries_updated_at ON config_entries;
|
|
116
|
+
CREATE TRIGGER update_config_entries_updated_at
|
|
117
|
+
BEFORE UPDATE ON config_entries
|
|
118
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
119
|
+
|
|
120
|
+
-- Config entries uses ON DELETE CASCADE (unlike permission_policies which uses RESTRICT)
|
|
121
|
+
-- because config values are safe to delete with a tenant — they carry no semantic guarantees
|
|
122
|
+
-- like PERMANENT revocation mode. If soft-delete is used (v1 default), CASCADE never fires.
|
|
123
|
+
|
|
124
|
+
-- API keys table (for control plane auth)
|
|
125
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
126
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
127
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
128
|
+
key_hash TEXT NOT NULL,
|
|
129
|
+
key_prefix VARCHAR(16),
|
|
130
|
+
name TEXT,
|
|
131
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
132
|
+
last_used_at TIMESTAMPTZ,
|
|
133
|
+
revoked_at TIMESTAMPTZ
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys (key_hash);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- Migration 002: Allow SCHEMA_PER_TENANT in isolation_strategy check constraint
|
|
2
|
+
-- Extends the tenants table to support schema-per-tenant isolation (v1.1).
|
|
3
|
+
|
|
4
|
+
ALTER TABLE tenants DROP CONSTRAINT IF EXISTS tenants_isolation_strategy_check;
|
|
5
|
+
ALTER TABLE tenants ADD CONSTRAINT tenants_isolation_strategy_check
|
|
6
|
+
CHECK (isolation_strategy IN ('SHARED_RLS', 'SCHEMA_PER_TENANT'));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Migration 003: DB_PER_TENANT isolation support
|
|
2
|
+
-- Expands the isolation_strategy constraint and adds connection_config column.
|
|
3
|
+
|
|
4
|
+
-- Allow DB_PER_TENANT in addition to existing strategies.
|
|
5
|
+
-- Drop the old constraint first (name from 001_init.sql), then re-add with all three values.
|
|
6
|
+
ALTER TABLE tenants DROP CONSTRAINT IF EXISTS tenants_isolation_strategy_check;
|
|
7
|
+
ALTER TABLE tenants ADD CONSTRAINT tenants_isolation_strategy_check
|
|
8
|
+
CHECK (isolation_strategy IN ('SHARED_RLS', 'SCHEMA_PER_TENANT', 'DB_PER_TENANT'));
|
|
9
|
+
|
|
10
|
+
-- Optional JSONB column to store per-tenant database connection overrides
|
|
11
|
+
-- (e.g. custom host/port for fully isolated DB_PER_TENANT deployments).
|
|
12
|
+
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS connection_config JSONB DEFAULT NULL;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- Webhook registrations
|
|
2
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
3
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
4
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
5
|
+
url TEXT NOT NULL,
|
|
6
|
+
secret_hash TEXT NOT NULL,
|
|
7
|
+
events TEXT[] NOT NULL DEFAULT '{}',
|
|
8
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
9
|
+
description TEXT,
|
|
10
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
11
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_tenant ON webhooks(tenant_id);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active) WHERE active = true;
|
|
16
|
+
|
|
17
|
+
DROP TRIGGER IF EXISTS update_webhooks_updated_at ON webhooks;
|
|
18
|
+
CREATE TRIGGER update_webhooks_updated_at
|
|
19
|
+
BEFORE UPDATE ON webhooks
|
|
20
|
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
21
|
+
|
|
22
|
+
-- Webhook events log
|
|
23
|
+
CREATE TABLE IF NOT EXISTS webhook_events (
|
|
24
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
25
|
+
type TEXT NOT NULL,
|
|
26
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
27
|
+
data JSONB NOT NULL DEFAULT '{}',
|
|
28
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_events_tenant ON webhook_events(tenant_id);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_events_type ON webhook_events(type);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_webhook_events_created ON webhook_events(created_at);
|
|
34
|
+
|
|
35
|
+
-- Webhook delivery tracking
|
|
36
|
+
CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
|
37
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
38
|
+
webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
|
|
39
|
+
event_id UUID NOT NULL REFERENCES webhook_events(id) ON DELETE CASCADE,
|
|
40
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
41
|
+
CHECK (status IN ('pending', 'success', 'failed')),
|
|
42
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
next_retry_at TIMESTAMPTZ,
|
|
44
|
+
last_error TEXT,
|
|
45
|
+
response_code INTEGER,
|
|
46
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
47
|
+
completed_at TIMESTAMPTZ
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_webhook ON webhook_deliveries(webhook_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_event ON webhook_deliveries(event_id);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_status ON webhook_deliveries(status) WHERE status = 'pending';
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_retry ON webhook_deliveries(next_retry_at) WHERE status = 'pending';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
2
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
3
|
+
actor_id TEXT NOT NULL,
|
|
4
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('api_key', 'jwt', 'system')),
|
|
5
|
+
action TEXT NOT NULL,
|
|
6
|
+
resource_type TEXT NOT NULL,
|
|
7
|
+
resource_id TEXT,
|
|
8
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
|
|
9
|
+
source_ip INET,
|
|
10
|
+
request_id TEXT,
|
|
11
|
+
before_state JSONB,
|
|
12
|
+
after_state JSONB,
|
|
13
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
14
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant ON audit_logs(tenant_id);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_logs(created_at);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_logs(actor_id);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_audit_resource ON audit_logs(resource_type, resource_id);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE config_entries ADD COLUMN IF NOT EXISTS sensitive BOOLEAN NOT NULL DEFAULT false;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Add expiration and rate limit columns to api_keys
|
|
2
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
|
|
3
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS rate_limit_max INTEGER;
|
|
4
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS rate_limit_window TEXT;
|
|
5
|
+
|
|
6
|
+
CREATE INDEX idx_api_keys_expires ON api_keys(expires_at) WHERE expires_at IS NOT NULL;
|
|
7
|
+
CREATE INDEX idx_api_keys_last_used ON api_keys(last_used_at);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS consent_records (
|
|
2
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
4
|
+
subject_id TEXT NOT NULL,
|
|
5
|
+
purpose TEXT NOT NULL,
|
|
6
|
+
granted BOOLEAN NOT NULL DEFAULT true,
|
|
7
|
+
granted_at TIMESTAMPTZ DEFAULT now(),
|
|
8
|
+
revoked_at TIMESTAMPTZ,
|
|
9
|
+
expires_at TIMESTAMPTZ,
|
|
10
|
+
metadata JSONB DEFAULT '{}',
|
|
11
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
13
|
+
UNIQUE(tenant_id, subject_id, purpose)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_consent_tenant ON consent_records(tenant_id);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_consent_subject ON consent_records(subject_id);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_consent_purpose ON consent_records(purpose);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS regions (
|
|
2
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
display_name TEXT NOT NULL,
|
|
4
|
+
slug TEXT NOT NULL UNIQUE,
|
|
5
|
+
control_plane_url TEXT,
|
|
6
|
+
database_url TEXT,
|
|
7
|
+
is_primary BOOLEAN NOT NULL DEFAULT false,
|
|
8
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'draining', 'inactive')),
|
|
9
|
+
metadata JSONB DEFAULT '{}',
|
|
10
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
11
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_regions_slug ON regions(slug);
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_regions_status ON regions(status);
|
|
16
|
+
|
|
17
|
+
-- Add region_id to tenants (nullable for backward compat)
|
|
18
|
+
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS region_id UUID REFERENCES regions(id);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_tenants_region ON tenants(region_id);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Migration 011: Demo bootstrap
|
|
2
|
+
-- REMOVED: Demo API key seeding moved to packages/demo/api/src/seed.ts
|
|
3
|
+
-- The original migration inserted a hardcoded API key (sk_live_demo_key)
|
|
4
|
+
-- with global admin access, which is a security risk in production.
|
|
5
|
+
-- Demo data is now seeded via the demo seed script only.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- 012: Role-Based Access Control (RBAC)
|
|
2
|
+
-- Roles define named collections of scopes that can be assigned to API keys.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS roles (
|
|
5
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
6
|
+
name TEXT NOT NULL UNIQUE,
|
|
7
|
+
description TEXT,
|
|
8
|
+
scopes TEXT[] NOT NULL DEFAULT ARRAY['read'],
|
|
9
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
10
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
11
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
-- Index for tenant-scoped role lookups
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_roles_tenant_id ON roles (tenant_id);
|
|
16
|
+
|
|
17
|
+
-- Add role_id column to api_keys
|
|
18
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS role_id UUID REFERENCES roles(id) ON DELETE SET NULL;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Add hash_version column to distinguish SHA-256 (legacy) from HMAC-SHA256 hashes.
|
|
2
|
+
-- 1 = SHA-256 (legacy), 2 = HMAC-SHA256 (current).
|
|
3
|
+
-- Existing keys default to version 1; new keys will be created with version 2.
|
|
4
|
+
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS hash_version SMALLINT NOT NULL DEFAULT 1;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Encrypt database_url column in regions table
|
|
2
|
+
-- Existing plaintext URLs will need to be migrated via the key rotation service
|
|
3
|
+
ALTER TABLE regions RENAME COLUMN database_url TO database_url_encrypted;
|
|
4
|
+
-- Add a comment to remind developers this column is encrypted
|
|
5
|
+
COMMENT ON COLUMN regions.database_url_encrypted IS 'AES-256-GCM encrypted PostgreSQL connection string. Use encrypt()/decrypt() from @stratum-hq/lib.';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Handle migration renaming: 002_sort_order.sql → 014_sort_order.sql
|
|
2
|
+
-- If an existing deployment already applied 002_sort_order.sql, record the
|
|
3
|
+
-- new name so the migration runner doesn't try to re-apply it.
|
|
4
|
+
INSERT INTO _migrations (name, applied_at)
|
|
5
|
+
SELECT '014_sort_order.sql', applied_at
|
|
6
|
+
FROM _migrations
|
|
7
|
+
WHERE name = '002_sort_order.sql'
|
|
8
|
+
ON CONFLICT DO NOTHING;
|