@stratum-hq/lib 0.7.0 → 1.1.0
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/README.md +109 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/020_usage_events.sql +55 -0
- package/dist/migrations/021_tenant_status_suspended.sql +12 -0
- package/dist/scoped-job.d.ts +57 -0
- package/dist/scoped-job.d.ts.map +1 -0
- package/dist/scoped-job.js +56 -0
- package/dist/scoped-job.js.map +1 -0
- package/dist/services/api-key-service.d.ts.map +1 -1
- package/dist/services/api-key-service.js +4 -1
- package/dist/services/api-key-service.js.map +1 -1
- package/dist/services/audit-service.d.ts +9 -1
- package/dist/services/audit-service.d.ts.map +1 -1
- package/dist/services/audit-service.js +18 -0
- package/dist/services/audit-service.js.map +1 -1
- package/dist/services/config-service.js +2 -2
- package/dist/services/config-service.js.map +1 -1
- package/dist/services/event-service.d.ts +1 -1
- package/dist/services/event-service.d.ts.map +1 -1
- package/dist/services/event-service.js +7 -6
- package/dist/services/event-service.js.map +1 -1
- package/dist/services/key-rotation-service.js +1 -1
- package/dist/services/key-rotation-service.js.map +1 -1
- package/dist/services/rate-limit-service.d.ts +135 -0
- package/dist/services/rate-limit-service.d.ts.map +1 -0
- package/dist/services/rate-limit-service.js +113 -0
- package/dist/services/rate-limit-service.js.map +1 -0
- package/dist/services/role-service.d.ts +13 -3
- package/dist/services/role-service.d.ts.map +1 -1
- package/dist/services/role-service.js +18 -5
- package/dist/services/role-service.js.map +1 -1
- package/dist/services/tenant-service.d.ts +34 -0
- package/dist/services/tenant-service.d.ts.map +1 -1
- package/dist/services/tenant-service.js +159 -11
- package/dist/services/tenant-service.js.map +1 -1
- package/dist/services/usage-service.d.ts +18 -0
- package/dist/services/usage-service.d.ts.map +1 -0
- package/dist/services/usage-service.js +80 -0
- package/dist/services/usage-service.js.map +1 -0
- package/dist/services/webhook-service.d.ts +9 -1
- package/dist/services/webhook-service.d.ts.map +1 -1
- package/dist/services/webhook-service.js +51 -0
- package/dist/services/webhook-service.js.map +1 -1
- package/dist/stratum.d.ts +51 -4
- package/dist/stratum.d.ts.map +1 -1
- package/dist/stratum.js +95 -1
- package/dist/stratum.js.map +1 -1
- package/package.json +9 -1
- package/dist/__tests__/crypto.test.d.ts +0 -2
- package/dist/__tests__/crypto.test.d.ts.map +0 -1
- package/dist/__tests__/crypto.test.js +0 -41
- package/dist/__tests__/crypto.test.js.map +0 -1
- package/dist/__tests__/migrate-schemas.test.d.ts +0 -2
- package/dist/__tests__/migrate-schemas.test.d.ts.map +0 -1
- package/dist/__tests__/migrate-schemas.test.js +0 -225
- package/dist/__tests__/migrate-schemas.test.js.map +0 -1
- package/dist/__tests__/stratum-als.test.d.ts +0 -2
- package/dist/__tests__/stratum-als.test.d.ts.map +0 -1
- package/dist/__tests__/stratum-als.test.js +0 -49
- package/dist/__tests__/stratum-als.test.js.map +0 -1
- package/dist/__tests__/webhook-signature.test.d.ts +0 -2
- package/dist/__tests__/webhook-signature.test.d.ts.map +0 -1
- package/dist/__tests__/webhook-signature.test.js +0 -103
- package/dist/__tests__/webhook-signature.test.js.map +0 -1
- package/dist/services/__tests__/abac-service.test.d.ts +0 -2
- package/dist/services/__tests__/abac-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/abac-service.test.js +0 -108
- package/dist/services/__tests__/abac-service.test.js.map +0 -1
- package/dist/services/__tests__/api-key-service.test.d.ts +0 -2
- package/dist/services/__tests__/api-key-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/api-key-service.test.js +0 -76
- package/dist/services/__tests__/api-key-service.test.js.map +0 -1
- package/dist/services/__tests__/audit-service.test.d.ts +0 -2
- package/dist/services/__tests__/audit-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/audit-service.test.js +0 -230
- package/dist/services/__tests__/audit-service.test.js.map +0 -1
- package/dist/services/__tests__/config-service.test.d.ts +0 -2
- package/dist/services/__tests__/config-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/config-service.test.js +0 -565
- package/dist/services/__tests__/config-service.test.js.map +0 -1
- package/dist/services/__tests__/consent-service.test.d.ts +0 -2
- package/dist/services/__tests__/consent-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/consent-service.test.js +0 -169
- package/dist/services/__tests__/consent-service.test.js.map +0 -1
- package/dist/services/__tests__/event-service.test.d.ts +0 -2
- package/dist/services/__tests__/event-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/event-service.test.js +0 -193
- package/dist/services/__tests__/event-service.test.js.map +0 -1
- package/dist/services/__tests__/key-rotation-service.test.d.ts +0 -2
- package/dist/services/__tests__/key-rotation-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/key-rotation-service.test.js +0 -132
- package/dist/services/__tests__/key-rotation-service.test.js.map +0 -1
- package/dist/services/__tests__/permission-service.test.d.ts +0 -2
- package/dist/services/__tests__/permission-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/permission-service.test.js +0 -480
- package/dist/services/__tests__/permission-service.test.js.map +0 -1
- package/dist/services/__tests__/retention-service.test.d.ts +0 -2
- package/dist/services/__tests__/retention-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/retention-service.test.js +0 -152
- package/dist/services/__tests__/retention-service.test.js.map +0 -1
- package/dist/services/__tests__/role-service.test.d.ts +0 -2
- package/dist/services/__tests__/role-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/role-service.test.js +0 -134
- package/dist/services/__tests__/role-service.test.js.map +0 -1
- package/dist/services/__tests__/tenant-service.test.d.ts +0 -2
- package/dist/services/__tests__/tenant-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/tenant-service.test.js +0 -548
- package/dist/services/__tests__/tenant-service.test.js.map +0 -1
- package/dist/services/__tests__/test-helpers.d.ts +0 -9
- package/dist/services/__tests__/test-helpers.d.ts.map +0 -1
- package/dist/services/__tests__/test-helpers.js +0 -19
- package/dist/services/__tests__/test-helpers.js.map +0 -1
- package/dist/services/__tests__/webhook-service.test.d.ts +0 -2
- package/dist/services/__tests__/webhook-service.test.d.ts.map +0 -1
- package/dist/services/__tests__/webhook-service.test.js +0 -184
- package/dist/services/__tests__/webhook-service.test.js.map +0 -1
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ The `Stratum` instance covers the full tenant lifecycle:
|
|
|
46
46
|
- **API keys & roles** — `createApiKey`, `validateApiKey`, `rotateApiKey`, `createRole`, `assignRoleToKey`
|
|
47
47
|
- **Webhooks & audit** — `createWebhook`, `testWebhook`, `queryAuditLogs`, `listFailedDeliveries`
|
|
48
48
|
- **GDPR & regions** — `exportTenantData`, `purgeTenant`, `grantConsent`, `createRegion`, `migrateRegion`
|
|
49
|
+
- **Usage metering** — `recordUsage`, `aggregateUsage` (see [docs/usage-metering.md](../../docs/usage-metering.md))
|
|
49
50
|
|
|
50
51
|
Low-level pool helpers are also exported:
|
|
51
52
|
|
|
@@ -57,6 +58,114 @@ await withTransaction(pool, async (client) => {
|
|
|
57
58
|
});
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
## Background jobs
|
|
62
|
+
|
|
63
|
+
`runScopedJob` runs a unit of background work bound to a single tenant. It
|
|
64
|
+
establishes the tenant context in two layers for the whole duration of the job
|
|
65
|
+
and tears both down when the job settles:
|
|
66
|
+
|
|
67
|
+
- **AsyncLocalStorage** — the tenant is placed in the SDK's ALS store, so code
|
|
68
|
+
inside the job reads it through `Stratum.currentTenantId()` /
|
|
69
|
+
`Stratum.currentTenantContext()` exactly as a request handler would. Each job
|
|
70
|
+
gets its own store, so concurrent jobs cannot observe each other's tenant.
|
|
71
|
+
- **Postgres row-level security** — the job runs through the data-plane
|
|
72
|
+
`withTenantContext` (`@stratum-hq/db-adapters`), which opens a transaction and
|
|
73
|
+
issues `SET LOCAL app.current_tenant_id`. Every query the job makes on the
|
|
74
|
+
provided client is confined to that tenant by RLS, so it cannot read or write
|
|
75
|
+
another tenant's rows even with a missing `WHERE tenant_id` filter. `SET LOCAL`
|
|
76
|
+
resets at COMMIT / ROLLBACK, so the context cannot leak onto the next job that
|
|
77
|
+
reuses the pooled connection.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { runScopedJob } from "@stratum-hq/lib";
|
|
81
|
+
|
|
82
|
+
// `pool` must connect as a NON-superuser, NON-BYPASSRLS role, or RLS is a no-op.
|
|
83
|
+
await runScopedJob(pool, tenantId, async (client) => {
|
|
84
|
+
Stratum.currentTenantId(); // === tenantId
|
|
85
|
+
// Confined to `tenantId` by RLS — no app-layer WHERE filter needed.
|
|
86
|
+
await client.query("SELECT * FROM invoices WHERE status = 'pending'");
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Unlike `withClient` / `withTransaction` above (the control-plane path, which runs
|
|
91
|
+
under the audited `app.bypass_rls`), `runScopedJob` keeps the job **subject to**
|
|
92
|
+
tenant isolation. Use it for any tenant-scoped background work. Pass
|
|
93
|
+
`{ resolve }` to populate the full `ResolvedTenantContext` in the ALS store; by
|
|
94
|
+
default a minimal placeholder carrying only the tenant id is used. See
|
|
95
|
+
ADR 0001 (`docs/adr/0001-postgres-rls-defense-in-depth.md`).
|
|
96
|
+
|
|
97
|
+
## Rate Limiting
|
|
98
|
+
|
|
99
|
+
`RateLimiter` is a standalone, storage-agnostic per-tenant rate-limiting primitive. It is not part of the `Stratum` facade (which is bound to a single `pg.Pool`) — the point is that its storage is pluggable. It ships with a process-local in-memory store and a documented `RateLimitStore` contract you can implement over Redis, Postgres, or anything else.
|
|
100
|
+
|
|
101
|
+
> This is distinct from the HTTP-layer rate limiting in `@stratum-hq/control-plane`. Use this to embed per-tenant limits directly in an application.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { RateLimiter } from "@stratum-hq/lib";
|
|
105
|
+
|
|
106
|
+
const limiter = new RateLimiter({
|
|
107
|
+
defaultLimit: { limit: 100, windowMs: 60_000 }, // 100 requests / minute
|
|
108
|
+
limits: {
|
|
109
|
+
"tenant-vip": { limit: 1000, windowMs: 60_000 }, // static per-tenant override
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// `key` sub-scopes the limit within a tenant (per-endpoint, per-user, ...).
|
|
114
|
+
// Omit it for one tenant-wide bucket. Counters are namespaced by tenant, so
|
|
115
|
+
// one tenant's usage never affects another's.
|
|
116
|
+
const res = await limiter.checkLimit(tenantId, "api");
|
|
117
|
+
if (!res.allowed) {
|
|
118
|
+
throw new Error(`rate limited — retry in ${res.retryAfter}s`);
|
|
119
|
+
}
|
|
120
|
+
// res: { allowed, limit, remaining, resetAt, retryAfter }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
It uses a fixed-window algorithm: at most `limit` hits per `windowMs`, resetting when the window elapses.
|
|
124
|
+
|
|
125
|
+
### Effective limit resolution
|
|
126
|
+
|
|
127
|
+
For each tenant the limit is resolved in order:
|
|
128
|
+
|
|
129
|
+
1. `resolveLimit(tenantId)` if provided and it returns a value,
|
|
130
|
+
2. the static `limits[tenantId]` override,
|
|
131
|
+
3. `defaultLimit`.
|
|
132
|
+
|
|
133
|
+
`resolveLimit` is the seam for **config inheritance** — back it with `stratum.resolveConfig` to drive limits from the tenant config tree:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const limiter = new RateLimiter({
|
|
137
|
+
defaultLimit: { limit: 100, windowMs: 60_000 },
|
|
138
|
+
async resolveLimit(tenantId) {
|
|
139
|
+
const cfg = await stratum.resolveConfig(tenantId);
|
|
140
|
+
const limit = cfg.rate_limit?.value as { limit: number; windowMs: number } | undefined;
|
|
141
|
+
return limit; // undefined falls through to defaultLimit
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Storage backend contract
|
|
147
|
+
|
|
148
|
+
Pass any `RateLimitStore` as `store` (defaults to `MemoryRateLimitStore`). A store provides one atomic-per-key operation:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface RateLimitStore {
|
|
152
|
+
// Atomically increment `key`'s counter and return the new count and the
|
|
153
|
+
// window's reset time. If no live window exists (never seen, or the previous
|
|
154
|
+
// window has elapsed), begin a fresh window: count = 1, resetAt = now + windowMs.
|
|
155
|
+
// Otherwise increment and return the current window's unchanged resetAt.
|
|
156
|
+
// The read-modify-write MUST be atomic per key.
|
|
157
|
+
increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }>;
|
|
158
|
+
// Clear `key`'s counter so the next increment starts a new window.
|
|
159
|
+
reset(key: string): Promise<void>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Implementation notes:
|
|
164
|
+
|
|
165
|
+
- **Redis** — `INCR key`, then on a reply of `1`, `PEXPIRE key windowMs`; derive `resetAt` from `now + PTTL`. A short Lua script keeps it atomic and returns count + TTL in one round trip.
|
|
166
|
+
- **Postgres** — `INSERT ... ON CONFLICT (key) DO UPDATE` returning the new count, resetting the row when `reset_at` has passed.
|
|
167
|
+
- **In-memory** — `MemoryRateLimitStore`; single-process only, holds one entry per distinct `tenantId:key` pair. For high key cardinality or multi-process deployments, use a store with native TTL eviction.
|
|
168
|
+
|
|
60
169
|
## Error Handling
|
|
61
170
|
|
|
62
171
|
All errors come from `@stratum-hq/core` and extend `StratumError`:
|
package/dist/index.d.ts
CHANGED
|
@@ -2,15 +2,20 @@ export { Stratum, type StratumOptions } from "./stratum.js";
|
|
|
2
2
|
export { migrate, type MigrateOptions } from "./migrate.js";
|
|
3
3
|
export { migrateAllSchemas, type MigrateSchemasOptions, type MigrateSchemasResult, } from "./migrate-schemas.js";
|
|
4
4
|
export { withClient, withTransaction } from "./pool-helpers.js";
|
|
5
|
+
export { runScopedJob, type ScopedJobFn, type RunScopedJobOptions, } from "./scoped-job.js";
|
|
5
6
|
export { verifyWebhookSignature, signWebhookPayload, DEFAULT_WEBHOOK_TOLERANCE_SECONDS, type VerifyWebhookOptions, } from "./webhook-signature.js";
|
|
6
7
|
export { traced, getTracer, isTracingEnabled } from "./telemetry.js";
|
|
8
|
+
export { ErrorCode, StratumError, TenantNotFoundError, TenantAlreadyExistsError, TenantHasChildrenError, TenantCycleDetectedError, TenantArchivedError, TenantSuspendedError, InvalidTenantStateError, TenantContextNotFoundError, IsolationViolationError, IsolationStrategyUnsupportedError, PermissionLockedError, PermissionNotFoundError, PermissionRevocationDeniedError, ConfigLockedError, ConfigNotFoundError, ValidationError, UnauthorizedError, ForbiddenError, WebhookNotFoundError, WebhookDeliveryError, WebhookUrlValidationError, AbacPolicyNotFoundError, InvalidAbacOperatorError, AbacPolicyLockedError, } from "@stratum-hq/core";
|
|
7
9
|
export type { TenantNode, TenantContext, CreateTenantInput, UpdateTenantInput, MoveTenantInput, PaginationInput, PaginatedResult, ConfigEntry, SetConfigInput, ResolvedConfigEntry, ResolvedConfig, PermissionPolicy, CreatePermissionInput, UpdatePermissionInput, ResolvedPermission, } from "@stratum-hq/core";
|
|
10
|
+
export { RateLimiter, MemoryRateLimitStore } from "./services/rate-limit-service.js";
|
|
11
|
+
export type { RateLimit, RateLimitResult, RateLimitState, RateLimitStore, RateLimitResolver, RateLimiterOptions, } from "./services/rate-limit-service.js";
|
|
8
12
|
export type { ApiKeyRecord, CreatedApiKey, ValidatedApiKey, CreateApiKeyOptions } from "./services/api-key-service.js";
|
|
9
13
|
export type { BatchCreateResult } from "./services/tenant-service.js";
|
|
10
14
|
export type { KeyRotationResult } from "./services/key-rotation-service.js";
|
|
11
15
|
export type { DeliveryStats } from "./services/event-service.js";
|
|
12
16
|
export type { Role, CreateRoleInput, UpdateRoleInput } from "./services/role-service.js";
|
|
13
|
-
export type { AuditContext, AuditEntry, AuditLogQuery } from "@stratum-hq/core";
|
|
17
|
+
export type { AuditContext, AuditEntry, AuditLogQuery, RecordAuditEventInput } from "@stratum-hq/core";
|
|
18
|
+
export type { RecordUsageInput, UsageEvent, UsageAggregate, UsageAggregateQuery, } from "@stratum-hq/core";
|
|
14
19
|
export { defaultLogger, noopLogger } from "./logger.js";
|
|
15
20
|
export type { StratumLogger } from "./logger.js";
|
|
16
21
|
//# 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,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,iCAAiC,EACjC,KAAK,oBAAoB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,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,EACL,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,iCAAiC,EACjC,KAAK,oBAAoB,GAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAKrE,OAAO,EACL,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,wBAAwB,EACxB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,0BAA0B,EAC1B,uBAAuB,EACvB,iCAAiC,EACjC,qBAAqB,EACrB,uBAAuB,EACvB,+BAA+B,EAC/B,iBAAiB,EACjB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,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,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AACrF,YAAY,EACV,SAAS,EACT,eAAe,EACf,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAE1C,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,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAGvG,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.noopLogger = exports.defaultLogger = exports.isTracingEnabled = exports.getTracer = exports.traced = exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports.signWebhookPayload = exports.verifyWebhookSignature = exports.withTransaction = exports.withClient = exports.migrateAllSchemas = exports.migrate = exports.Stratum = void 0;
|
|
3
|
+
exports.noopLogger = exports.defaultLogger = exports.MemoryRateLimitStore = exports.RateLimiter = exports.AbacPolicyLockedError = exports.InvalidAbacOperatorError = exports.AbacPolicyNotFoundError = exports.WebhookUrlValidationError = exports.WebhookDeliveryError = exports.WebhookNotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.ValidationError = exports.ConfigNotFoundError = exports.ConfigLockedError = exports.PermissionRevocationDeniedError = exports.PermissionNotFoundError = exports.PermissionLockedError = exports.IsolationStrategyUnsupportedError = exports.IsolationViolationError = exports.TenantContextNotFoundError = exports.InvalidTenantStateError = exports.TenantSuspendedError = exports.TenantArchivedError = exports.TenantCycleDetectedError = exports.TenantHasChildrenError = exports.TenantAlreadyExistsError = exports.TenantNotFoundError = exports.StratumError = exports.ErrorCode = exports.isTracingEnabled = exports.getTracer = exports.traced = exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports.signWebhookPayload = exports.verifyWebhookSignature = exports.runScopedJob = exports.withTransaction = exports.withClient = exports.migrateAllSchemas = 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
6
|
var migrate_js_1 = require("./migrate.js");
|
|
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "migrateAllSchemas", { enumerable: true, get: fun
|
|
|
10
10
|
var pool_helpers_js_1 = require("./pool-helpers.js");
|
|
11
11
|
Object.defineProperty(exports, "withClient", { enumerable: true, get: function () { return pool_helpers_js_1.withClient; } });
|
|
12
12
|
Object.defineProperty(exports, "withTransaction", { enumerable: true, get: function () { return pool_helpers_js_1.withTransaction; } });
|
|
13
|
+
var scoped_job_js_1 = require("./scoped-job.js");
|
|
14
|
+
Object.defineProperty(exports, "runScopedJob", { enumerable: true, get: function () { return scoped_job_js_1.runScopedJob; } });
|
|
13
15
|
var webhook_signature_js_1 = require("./webhook-signature.js");
|
|
14
16
|
Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhook_signature_js_1.verifyWebhookSignature; } });
|
|
15
17
|
Object.defineProperty(exports, "signWebhookPayload", { enumerable: true, get: function () { return webhook_signature_js_1.signWebhookPayload; } });
|
|
@@ -18,6 +20,39 @@ var telemetry_js_1 = require("./telemetry.js");
|
|
|
18
20
|
Object.defineProperty(exports, "traced", { enumerable: true, get: function () { return telemetry_js_1.traced; } });
|
|
19
21
|
Object.defineProperty(exports, "getTracer", { enumerable: true, get: function () { return telemetry_js_1.getTracer; } });
|
|
20
22
|
Object.defineProperty(exports, "isTracingEnabled", { enumerable: true, get: function () { return telemetry_js_1.isTracingEnabled; } });
|
|
23
|
+
// Re-export core error classes as runtime VALUES (not `export type`) so consumers
|
|
24
|
+
// can branch with `instanceof` instead of matching error-message substrings. A
|
|
25
|
+
// type-only re-export would erase these at compile time and break instanceof.
|
|
26
|
+
var core_1 = require("@stratum-hq/core");
|
|
27
|
+
Object.defineProperty(exports, "ErrorCode", { enumerable: true, get: function () { return core_1.ErrorCode; } });
|
|
28
|
+
Object.defineProperty(exports, "StratumError", { enumerable: true, get: function () { return core_1.StratumError; } });
|
|
29
|
+
Object.defineProperty(exports, "TenantNotFoundError", { enumerable: true, get: function () { return core_1.TenantNotFoundError; } });
|
|
30
|
+
Object.defineProperty(exports, "TenantAlreadyExistsError", { enumerable: true, get: function () { return core_1.TenantAlreadyExistsError; } });
|
|
31
|
+
Object.defineProperty(exports, "TenantHasChildrenError", { enumerable: true, get: function () { return core_1.TenantHasChildrenError; } });
|
|
32
|
+
Object.defineProperty(exports, "TenantCycleDetectedError", { enumerable: true, get: function () { return core_1.TenantCycleDetectedError; } });
|
|
33
|
+
Object.defineProperty(exports, "TenantArchivedError", { enumerable: true, get: function () { return core_1.TenantArchivedError; } });
|
|
34
|
+
Object.defineProperty(exports, "TenantSuspendedError", { enumerable: true, get: function () { return core_1.TenantSuspendedError; } });
|
|
35
|
+
Object.defineProperty(exports, "InvalidTenantStateError", { enumerable: true, get: function () { return core_1.InvalidTenantStateError; } });
|
|
36
|
+
Object.defineProperty(exports, "TenantContextNotFoundError", { enumerable: true, get: function () { return core_1.TenantContextNotFoundError; } });
|
|
37
|
+
Object.defineProperty(exports, "IsolationViolationError", { enumerable: true, get: function () { return core_1.IsolationViolationError; } });
|
|
38
|
+
Object.defineProperty(exports, "IsolationStrategyUnsupportedError", { enumerable: true, get: function () { return core_1.IsolationStrategyUnsupportedError; } });
|
|
39
|
+
Object.defineProperty(exports, "PermissionLockedError", { enumerable: true, get: function () { return core_1.PermissionLockedError; } });
|
|
40
|
+
Object.defineProperty(exports, "PermissionNotFoundError", { enumerable: true, get: function () { return core_1.PermissionNotFoundError; } });
|
|
41
|
+
Object.defineProperty(exports, "PermissionRevocationDeniedError", { enumerable: true, get: function () { return core_1.PermissionRevocationDeniedError; } });
|
|
42
|
+
Object.defineProperty(exports, "ConfigLockedError", { enumerable: true, get: function () { return core_1.ConfigLockedError; } });
|
|
43
|
+
Object.defineProperty(exports, "ConfigNotFoundError", { enumerable: true, get: function () { return core_1.ConfigNotFoundError; } });
|
|
44
|
+
Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return core_1.ValidationError; } });
|
|
45
|
+
Object.defineProperty(exports, "UnauthorizedError", { enumerable: true, get: function () { return core_1.UnauthorizedError; } });
|
|
46
|
+
Object.defineProperty(exports, "ForbiddenError", { enumerable: true, get: function () { return core_1.ForbiddenError; } });
|
|
47
|
+
Object.defineProperty(exports, "WebhookNotFoundError", { enumerable: true, get: function () { return core_1.WebhookNotFoundError; } });
|
|
48
|
+
Object.defineProperty(exports, "WebhookDeliveryError", { enumerable: true, get: function () { return core_1.WebhookDeliveryError; } });
|
|
49
|
+
Object.defineProperty(exports, "WebhookUrlValidationError", { enumerable: true, get: function () { return core_1.WebhookUrlValidationError; } });
|
|
50
|
+
Object.defineProperty(exports, "AbacPolicyNotFoundError", { enumerable: true, get: function () { return core_1.AbacPolicyNotFoundError; } });
|
|
51
|
+
Object.defineProperty(exports, "InvalidAbacOperatorError", { enumerable: true, get: function () { return core_1.InvalidAbacOperatorError; } });
|
|
52
|
+
Object.defineProperty(exports, "AbacPolicyLockedError", { enumerable: true, get: function () { return core_1.AbacPolicyLockedError; } });
|
|
53
|
+
var rate_limit_service_js_1 = require("./services/rate-limit-service.js");
|
|
54
|
+
Object.defineProperty(exports, "RateLimiter", { enumerable: true, get: function () { return rate_limit_service_js_1.RateLimiter; } });
|
|
55
|
+
Object.defineProperty(exports, "MemoryRateLimitStore", { enumerable: true, get: function () { return rate_limit_service_js_1.MemoryRateLimitStore; } });
|
|
21
56
|
var logger_js_1 = require("./logger.js");
|
|
22
57
|
Object.defineProperty(exports, "defaultLogger", { enumerable: true, get: function () { return logger_js_1.defaultLogger; } });
|
|
23
58
|
Object.defineProperty(exports, "noopLogger", { enumerable: true, get: function () { return logger_js_1.noopLogger; } });
|
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,2CAA4D;AAAnD,qGAAA,OAAO,OAAA;AAChB,2DAI8B;AAH5B,uHAAA,iBAAiB,OAAA;AAInB,qDAAgE;AAAvD,6GAAA,UAAU,OAAA;AAAE,kHAAA,eAAe,OAAA;AACpC,+DAKgC;AAJ9B,8HAAA,sBAAsB,OAAA;AACtB,0HAAA,kBAAkB,OAAA;AAClB,yIAAA,iCAAiC,OAAA;AAGnC,+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,2DAI8B;AAH5B,uHAAA,iBAAiB,OAAA;AAInB,qDAAgE;AAAvD,6GAAA,UAAU,OAAA;AAAE,kHAAA,eAAe,OAAA;AACpC,iDAIyB;AAHvB,6GAAA,YAAY,OAAA;AAId,+DAKgC;AAJ9B,8HAAA,sBAAsB,OAAA;AACtB,0HAAA,kBAAkB,OAAA;AAClB,yIAAA,iCAAiC,OAAA;AAGnC,+CAAqE;AAA5D,sGAAA,MAAM,OAAA;AAAE,yGAAA,SAAS,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAE5C,kFAAkF;AAClF,+EAA+E;AAC/E,8EAA8E;AAC9E,yCA2B0B;AA1BxB,iGAAA,SAAS,OAAA;AACT,oGAAA,YAAY,OAAA;AACZ,2GAAA,mBAAmB,OAAA;AACnB,gHAAA,wBAAwB,OAAA;AACxB,8GAAA,sBAAsB,OAAA;AACtB,gHAAA,wBAAwB,OAAA;AACxB,2GAAA,mBAAmB,OAAA;AACnB,4GAAA,oBAAoB,OAAA;AACpB,+GAAA,uBAAuB,OAAA;AACvB,kHAAA,0BAA0B,OAAA;AAC1B,+GAAA,uBAAuB,OAAA;AACvB,yHAAA,iCAAiC,OAAA;AACjC,6GAAA,qBAAqB,OAAA;AACrB,+GAAA,uBAAuB,OAAA;AACvB,uHAAA,+BAA+B,OAAA;AAC/B,yGAAA,iBAAiB,OAAA;AACjB,2GAAA,mBAAmB,OAAA;AACnB,uGAAA,eAAe,OAAA;AACf,yGAAA,iBAAiB,OAAA;AACjB,sGAAA,cAAc,OAAA;AACd,4GAAA,oBAAoB,OAAA;AACpB,4GAAA,oBAAoB,OAAA;AACpB,iHAAA,yBAAyB,OAAA;AACzB,+GAAA,uBAAuB,OAAA;AACvB,gHAAA,wBAAwB,OAAA;AACxB,6GAAA,qBAAqB,OAAA;AAsBvB,0EAAqF;AAA5E,oHAAA,WAAW,OAAA;AAAE,6HAAA,oBAAoB,OAAA;AA2B1C,yCAAwD;AAA/C,0GAAA,aAAa,OAAA;AAAE,uGAAA,UAAU,OAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- Migration 020: per-tenant usage metering (FR-58).
|
|
2
|
+
--
|
|
3
|
+
-- Countable usage events (a metric plus a quantity) per tenant, aggregated per
|
|
4
|
+
-- metric over a time window for billing / quota. Adjacent to audit_logs (005)
|
|
5
|
+
-- but append-heavy and numeric. `occurred_at` is the event time that
|
|
6
|
+
-- aggregation windows filter on (caller-supplied, so a back-dated import lands
|
|
7
|
+
-- in the right window); `recorded_at` is ingest time.
|
|
8
|
+
--
|
|
9
|
+
-- `idempotency_key` makes a record idempotent per (tenant_id, metric): a
|
|
10
|
+
-- retried write with the same key is a no-op, so an at-least-once producer
|
|
11
|
+
-- cannot double count. Enforced by a partial unique index; NULL keys are
|
|
12
|
+
-- unconstrained and can repeat freely.
|
|
13
|
+
--
|
|
14
|
+
-- The table is tenant-scoped, so it gets the same row-level-security
|
|
15
|
+
-- defense-in-depth as migration 019: ENABLE + FORCE + a fail-closed
|
|
16
|
+
-- tenant_isolation policy. See docs/adr/0001-postgres-rls-defense-in-depth.md.
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
19
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
20
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
21
|
+
metric TEXT NOT NULL,
|
|
22
|
+
quantity BIGINT NOT NULL DEFAULT 1 CHECK (quantity >= 0),
|
|
23
|
+
idempotency_key TEXT,
|
|
24
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
25
|
+
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
26
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- Aggregation is always WHERE tenant_id = ? [AND metric = ?] over an
|
|
30
|
+
-- occurred_at window, grouped by metric. This composite index serves that path.
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_usage_tenant_metric_time
|
|
32
|
+
ON usage_events (tenant_id, metric, occurred_at);
|
|
33
|
+
|
|
34
|
+
-- Idempotency: at most one row per (tenant_id, metric, idempotency_key) when a
|
|
35
|
+
-- key is supplied. Partial so unkeyed events (idempotency_key IS NULL) do not
|
|
36
|
+
-- collide.
|
|
37
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_usage_idempotency
|
|
38
|
+
ON usage_events (tenant_id, metric, idempotency_key)
|
|
39
|
+
WHERE idempotency_key IS NOT NULL;
|
|
40
|
+
|
|
41
|
+
-- RLS: same fail-closed tenant_isolation shape as migration 019. With no
|
|
42
|
+
-- context set the predicate is NULL and zero rows are visible; a write with no
|
|
43
|
+
-- context is rejected.
|
|
44
|
+
ALTER TABLE usage_events ENABLE ROW LEVEL SECURITY;
|
|
45
|
+
ALTER TABLE usage_events FORCE ROW LEVEL SECURITY;
|
|
46
|
+
DROP POLICY IF EXISTS tenant_isolation ON usage_events;
|
|
47
|
+
CREATE POLICY tenant_isolation ON usage_events FOR ALL
|
|
48
|
+
USING (
|
|
49
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
50
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
51
|
+
)
|
|
52
|
+
WITH CHECK (
|
|
53
|
+
current_setting('app.bypass_rls', true) = 'on'
|
|
54
|
+
OR tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid
|
|
55
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Tenant lifecycle: add the 'suspended' state (FR-58).
|
|
2
|
+
-- The original inline CHECK on tenants.status (001_init.sql) allowed only
|
|
3
|
+
-- 'active' and 'archived'. Relax it to also permit 'suspended', the reversible
|
|
4
|
+
-- "blocks access" state introduced by suspendTenant/resumeTenant.
|
|
5
|
+
--
|
|
6
|
+
-- The inline constraint from 001_init.sql is named tenants_status_check by
|
|
7
|
+
-- Postgres. Drop it if present and re-add the widened constraint under the same
|
|
8
|
+
-- name so the schema stays stable.
|
|
9
|
+
ALTER TABLE tenants DROP CONSTRAINT IF EXISTS tenants_status_check;
|
|
10
|
+
ALTER TABLE tenants
|
|
11
|
+
ADD CONSTRAINT tenants_status_check
|
|
12
|
+
CHECK (status IN ('active', 'suspended', 'archived'));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type pg from "pg";
|
|
2
|
+
import type { ResolvedTenantContext } from "@stratum-hq/core";
|
|
3
|
+
/**
|
|
4
|
+
* The unit of work a background job runs. It receives the RLS-scoped pooled
|
|
5
|
+
* client bound to the job's tenant; every query issued on this client is
|
|
6
|
+
* confined to that tenant by Postgres row-level security.
|
|
7
|
+
*/
|
|
8
|
+
export type ScopedJobFn<T> = (client: pg.PoolClient) => Promise<T>;
|
|
9
|
+
export interface RunScopedJobOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Populate the full {@link ResolvedTenantContext} placed in the
|
|
12
|
+
* AsyncLocalStorage store for the job. Job code can then read it through
|
|
13
|
+
* `Stratum.currentTenantContext()` exactly as a request handler does.
|
|
14
|
+
*
|
|
15
|
+
* When omitted, a minimal placeholder context carrying only the tenant
|
|
16
|
+
* identity is used: `resolved_config` and `resolved_permissions` are empty.
|
|
17
|
+
* This mirrors the default in `@stratum-hq/hono`'s middleware. The isolation
|
|
18
|
+
* boundary (the RLS `app.current_tenant_id`) does NOT depend on this callback;
|
|
19
|
+
* it only enriches what in-job code can read from the ALS context.
|
|
20
|
+
*/
|
|
21
|
+
resolve?: (tenantId: string) => ResolvedTenantContext | Promise<ResolvedTenantContext>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Runs a background job bound to a single tenant.
|
|
25
|
+
*
|
|
26
|
+
* A job scoped with this helper carries its tenant context in two layers, both
|
|
27
|
+
* established before `fn` runs and both torn down when it settles:
|
|
28
|
+
*
|
|
29
|
+
* 1. AsyncLocalStorage: the tenant context is placed in the SDK's ALS store via
|
|
30
|
+
* `runWithTenantContext`, so code inside the job sees the same tenant through
|
|
31
|
+
* `Stratum.currentTenantId()` / `Stratum.currentTenantContext()` that a
|
|
32
|
+
* request handler would. Each job gets its own store; concurrent jobs cannot
|
|
33
|
+
* observe each other's tenant (async isolation), and the store is gone once
|
|
34
|
+
* the job settles.
|
|
35
|
+
* 2. Postgres row-level security: the work runs through the data-plane
|
|
36
|
+
* `withTenantContext` (packages/db-adapters), which opens a transaction and
|
|
37
|
+
* issues `SET LOCAL app.current_tenant_id`. Every query `fn` makes on the
|
|
38
|
+
* provided client is confined to that tenant by RLS, so a job cannot read or
|
|
39
|
+
* write another tenant's rows even if it forgets a `WHERE tenant_id` filter.
|
|
40
|
+
* `SET LOCAL` resets at COMMIT / ROLLBACK, so the context cannot leak onto the
|
|
41
|
+
* next job that reuses the pooled connection.
|
|
42
|
+
*
|
|
43
|
+
* This is deliberately NOT the control-plane bypass path (`withClient` /
|
|
44
|
+
* `withTransaction` in this package, which set `app.bypass_rls`). Background
|
|
45
|
+
* jobs are tenant-scoped data-plane work and must stay subject to isolation.
|
|
46
|
+
* See ADR 0001 (docs/adr/0001-postgres-rls-defense-in-depth.md).
|
|
47
|
+
*
|
|
48
|
+
* @param pool A pool whose connections use a NON-superuser, NON-BYPASSRLS
|
|
49
|
+
* role. A superuser connection bypasses RLS and the scoping is a
|
|
50
|
+
* no-op.
|
|
51
|
+
* @param tenantId The tenant the job is confined to.
|
|
52
|
+
* @param fn The job body; receives the tenant-scoped client.
|
|
53
|
+
* @param options Optionally resolve the full ALS context (see
|
|
54
|
+
* {@link RunScopedJobOptions.resolve}).
|
|
55
|
+
*/
|
|
56
|
+
export declare function runScopedJob<T>(pool: pg.Pool, tenantId: string, fn: ScopedJobFn<T>, options?: RunScopedJobOptions): Promise<T>;
|
|
57
|
+
//# sourceMappingURL=scoped-job.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-job.d.ts","sourceRoot":"","sources":["../src/scoped-job.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAIzB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAE9D;;;;GAIG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAEnE,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE,CACR,QAAQ,EAAE,MAAM,KACb,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC7D;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAClB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runScopedJob = runScopedJob;
|
|
4
|
+
const sdk_1 = require("@stratum-hq/sdk");
|
|
5
|
+
const db_adapters_1 = require("@stratum-hq/db-adapters");
|
|
6
|
+
const core_1 = require("@stratum-hq/core");
|
|
7
|
+
function placeholderContext(tenantId) {
|
|
8
|
+
return {
|
|
9
|
+
tenant_id: tenantId,
|
|
10
|
+
ancestry_path: tenantId,
|
|
11
|
+
depth: 0,
|
|
12
|
+
resolved_config: {},
|
|
13
|
+
resolved_permissions: {},
|
|
14
|
+
isolation_strategy: core_1.IsolationStrategy.SHARED_RLS,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs a background job bound to a single tenant.
|
|
19
|
+
*
|
|
20
|
+
* A job scoped with this helper carries its tenant context in two layers, both
|
|
21
|
+
* established before `fn` runs and both torn down when it settles:
|
|
22
|
+
*
|
|
23
|
+
* 1. AsyncLocalStorage: the tenant context is placed in the SDK's ALS store via
|
|
24
|
+
* `runWithTenantContext`, so code inside the job sees the same tenant through
|
|
25
|
+
* `Stratum.currentTenantId()` / `Stratum.currentTenantContext()` that a
|
|
26
|
+
* request handler would. Each job gets its own store; concurrent jobs cannot
|
|
27
|
+
* observe each other's tenant (async isolation), and the store is gone once
|
|
28
|
+
* the job settles.
|
|
29
|
+
* 2. Postgres row-level security: the work runs through the data-plane
|
|
30
|
+
* `withTenantContext` (packages/db-adapters), which opens a transaction and
|
|
31
|
+
* issues `SET LOCAL app.current_tenant_id`. Every query `fn` makes on the
|
|
32
|
+
* provided client is confined to that tenant by RLS, so a job cannot read or
|
|
33
|
+
* write another tenant's rows even if it forgets a `WHERE tenant_id` filter.
|
|
34
|
+
* `SET LOCAL` resets at COMMIT / ROLLBACK, so the context cannot leak onto the
|
|
35
|
+
* next job that reuses the pooled connection.
|
|
36
|
+
*
|
|
37
|
+
* This is deliberately NOT the control-plane bypass path (`withClient` /
|
|
38
|
+
* `withTransaction` in this package, which set `app.bypass_rls`). Background
|
|
39
|
+
* jobs are tenant-scoped data-plane work and must stay subject to isolation.
|
|
40
|
+
* See ADR 0001 (docs/adr/0001-postgres-rls-defense-in-depth.md).
|
|
41
|
+
*
|
|
42
|
+
* @param pool A pool whose connections use a NON-superuser, NON-BYPASSRLS
|
|
43
|
+
* role. A superuser connection bypasses RLS and the scoping is a
|
|
44
|
+
* no-op.
|
|
45
|
+
* @param tenantId The tenant the job is confined to.
|
|
46
|
+
* @param fn The job body; receives the tenant-scoped client.
|
|
47
|
+
* @param options Optionally resolve the full ALS context (see
|
|
48
|
+
* {@link RunScopedJobOptions.resolve}).
|
|
49
|
+
*/
|
|
50
|
+
async function runScopedJob(pool, tenantId, fn, options = {}) {
|
|
51
|
+
const context = options.resolve
|
|
52
|
+
? await options.resolve(tenantId)
|
|
53
|
+
: placeholderContext(tenantId);
|
|
54
|
+
return (0, sdk_1.runWithTenantContext)(context, () => (0, db_adapters_1.withTenantContext)(pool, tenantId, fn));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=scoped-job.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-job.js","sourceRoot":"","sources":["../src/scoped-job.ts"],"names":[],"mappings":";;AA0EA,oCAYC;AArFD,yCAAuD;AACvD,yDAA4D;AAC5D,2CAAqD;AA2BrD,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,OAAO;QACL,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,QAAQ;QACvB,KAAK,EAAE,CAAC;QACR,eAAe,EAAE,EAAE;QACnB,oBAAoB,EAAE,EAAE;QACxB,kBAAkB,EAAE,wBAAiB,CAAC,UAAU;KACjD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACI,KAAK,UAAU,YAAY,CAChC,IAAa,EACb,QAAgB,EAChB,EAAkB,EAClB,UAA+B,EAAE;IAEjC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO;QAC7B,CAAC,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;QACjC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACjC,OAAO,IAAA,0BAAoB,EAAC,OAAO,EAAE,GAAG,EAAE,CACxC,IAAA,+BAAiB,EAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,CACtC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-key-service.d.ts","sourceRoot":"","sources":["../../src/services/api-key-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"api-key-service.d.ts","sourceRoot":"","sources":["../../src/services/api-key-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,7 @@ exports.getApiKey = getApiKey;
|
|
|
13
13
|
exports.listDormantKeys = listDormantKeys;
|
|
14
14
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
15
|
const pool_helpers_js_1 = require("../pool-helpers.js");
|
|
16
|
+
const role_service_js_1 = require("./role-service.js");
|
|
16
17
|
// --- Hashing ---
|
|
17
18
|
const HASH_V1_SHA256 = 1;
|
|
18
19
|
const HASH_V2_HMAC = 2;
|
|
@@ -99,7 +100,9 @@ async function validateApiKey(pool, key) {
|
|
|
99
100
|
return {
|
|
100
101
|
tenant_id: row.tenant_id,
|
|
101
102
|
key_id: row.id,
|
|
102
|
-
|
|
103
|
+
// Resolve through the single source so an assigned role governs the
|
|
104
|
+
// key's scopes at the auth boundary, exactly as resolveKeyScopes does.
|
|
105
|
+
scopes: await (0, role_service_js_1.resolveEffectiveScopes)(pool, row.id),
|
|
103
106
|
rate_limit_max: row.rate_limit_max,
|
|
104
107
|
rate_limit_window: row.rate_limit_window,
|
|
105
108
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-key-service.js","sourceRoot":"","sources":["../../src/services/api-key-service.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"api-key-service.js","sourceRoot":"","sources":["../../src/services/api-key-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
import type { AuditContext, AuditEntry, AuditLogQuery } from "@stratum-hq/core";
|
|
2
|
+
import type { AuditContext, AuditEntry, AuditLogQuery, RecordAuditEventInput } from "@stratum-hq/core";
|
|
3
3
|
export declare function createAuditEntry(pool: pg.Pool, context: AuditContext, action: string, resourceType: string, resourceId: string | null, tenantId: string | null, beforeState?: Record<string, unknown> | null, afterState?: Record<string, unknown> | null, metadata?: Record<string, unknown>): Promise<AuditEntry>;
|
|
4
|
+
/**
|
|
5
|
+
* Append a custom audit event through the public surface. Validates the
|
|
6
|
+
* consumer-facing input and maps it onto the same createAuditEntry write path
|
|
7
|
+
* the internal services use, so a recorded event is indistinguishable from one
|
|
8
|
+
* Stratum writes itself and is queryable via queryAuditLogs. The row is stamped
|
|
9
|
+
* for input.tenantId and no other tenant.
|
|
10
|
+
*/
|
|
11
|
+
export declare function recordAuditEvent(pool: pg.Pool, input: RecordAuditEventInput): Promise<AuditEntry>;
|
|
4
12
|
export declare function queryAuditLogs(pool: pg.Pool, query: AuditLogQuery): Promise<AuditEntry[]>;
|
|
5
13
|
export declare function getAuditEntry(pool: pg.Pool, id: string): Promise<AuditEntry | null>;
|
|
6
14
|
//# sourceMappingURL=audit-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-service.d.ts","sourceRoot":"","sources":["../../src/services/audit-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"audit-service.d.ts","sourceRoot":"","sources":["../../src/services/audit-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,aAAa,EACb,qBAAqB,EACtB,MAAM,kBAAkB,CAAC;AAE1B,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EAC5C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,UAAU,CAAC,CAwBrB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAkBrB;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAkDvB;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAY5B"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createAuditEntry = createAuditEntry;
|
|
4
|
+
exports.recordAuditEvent = recordAuditEvent;
|
|
4
5
|
exports.queryAuditLogs = queryAuditLogs;
|
|
5
6
|
exports.getAuditEntry = getAuditEntry;
|
|
6
7
|
const pool_helpers_js_1 = require("../pool-helpers.js");
|
|
8
|
+
const core_1 = require("@stratum-hq/core");
|
|
7
9
|
async function createAuditEntry(pool, context, action, resourceType, resourceId, tenantId, beforeState, afterState, metadata) {
|
|
8
10
|
return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
|
|
9
11
|
const res = await client.query(`INSERT INTO audit_logs (actor_id, actor_type, action, resource_type, resource_id, tenant_id, source_ip, request_id, before_state, after_state, metadata)
|
|
@@ -26,6 +28,22 @@ async function createAuditEntry(pool, context, action, resourceType, resourceId,
|
|
|
26
28
|
return res.rows[0];
|
|
27
29
|
});
|
|
28
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Append a custom audit event through the public surface. Validates the
|
|
33
|
+
* consumer-facing input and maps it onto the same createAuditEntry write path
|
|
34
|
+
* the internal services use, so a recorded event is indistinguishable from one
|
|
35
|
+
* Stratum writes itself and is queryable via queryAuditLogs. The row is stamped
|
|
36
|
+
* for input.tenantId and no other tenant.
|
|
37
|
+
*/
|
|
38
|
+
async function recordAuditEvent(pool, input) {
|
|
39
|
+
const parsed = core_1.RecordAuditEventInputSchema.parse(input);
|
|
40
|
+
const context = {
|
|
41
|
+
actor_id: parsed.actorId,
|
|
42
|
+
actor_type: parsed.actorType,
|
|
43
|
+
source_ip: parsed.sourceIp ?? undefined,
|
|
44
|
+
};
|
|
45
|
+
return createAuditEntry(pool, context, parsed.action, parsed.resourceType, parsed.resourceId, parsed.tenantId, parsed.before ?? null, parsed.after ?? null, parsed.metadata);
|
|
46
|
+
}
|
|
29
47
|
async function queryAuditLogs(pool, query) {
|
|
30
48
|
return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
|
|
31
49
|
const conditions = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-service.js","sourceRoot":"","sources":["../../src/services/audit-service.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"audit-service.js","sourceRoot":"","sources":["../../src/services/audit-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -54,7 +54,7 @@ async function resolveConfig(pool, tenantId) {
|
|
|
54
54
|
}
|
|
55
55
|
const isCurrentTenant = currentTenantId === tenantId;
|
|
56
56
|
const resolvedValue = entry.sensitive
|
|
57
|
-
? JSON.parse((0, crypto_js_1.decrypt)(
|
|
57
|
+
? JSON.parse((0, crypto_js_1.decrypt)(entry.value))
|
|
58
58
|
: entry.value;
|
|
59
59
|
resolved.set(entry.key, {
|
|
60
60
|
key: entry.key,
|
|
@@ -229,7 +229,7 @@ async function getConfigWithInheritance(pool, tenantId) {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
const decryptEntryValue = (entry) => entry.sensitive
|
|
232
|
-
? JSON.parse((0, crypto_js_1.decrypt)(
|
|
232
|
+
? JSON.parse((0, crypto_js_1.decrypt)(entry.value))
|
|
233
233
|
: entry.value;
|
|
234
234
|
const result = {};
|
|
235
235
|
for (const [key, rec] of byKey) {
|