@govcore/server 0.1.2 → 0.2.1
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/index.d.ts +51 -9
- package/dist/index.js +67 -49
- package/dist/index.js.map +1 -0
- package/package.json +26 -10
- package/CHANGELOG.md +0 -39
- package/dist/index.d.ts.map +0 -1
- package/src/index.ts +0 -101
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { GovcoreDb } from '@govcore/schema';
|
|
2
|
+
import { AuditEvent } from '@govcore/audit';
|
|
3
|
+
|
|
4
|
+
/** The default `instanceRole` value that grants operator-plane access. */
|
|
5
|
+
declare const INSTANCE_ADMIN_ROLE = "instance_admin";
|
|
6
|
+
/** What an operator handler receives: the actor plus a pre-bound audit writer. */
|
|
7
|
+
interface OperatorContext {
|
|
8
|
+
userId: string;
|
|
9
|
+
instanceRole: string;
|
|
10
|
+
/** Write a platform audit row, pre-filled with the operator as actor. */
|
|
11
|
+
audit: (event: AuditEvent) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
interface CreateOperatorActionsConfig {
|
|
14
|
+
/**
|
|
15
|
+
* The privileged, RLS-bypassing pool (the same one createAuth takes as
|
|
16
|
+
* `authDb`). Operator queries run here because they are cross-org and must
|
|
17
|
+
* not be filtered by the tenant GUC.
|
|
18
|
+
*/
|
|
19
|
+
operatorDb: GovcoreDb;
|
|
20
|
+
/** Resolve the current actor's active context — typically from the auth() session. */
|
|
21
|
+
getActiveContext: () => Promise<ActiveContext | null>;
|
|
22
|
+
/** The `instanceRole` that grants operator access. Default `instance_admin`. */
|
|
23
|
+
operatorRole?: string;
|
|
24
|
+
/** Called when there is no active context. Default: throws Error('Unauthorized'). */
|
|
25
|
+
onUnauthorized?: () => never;
|
|
26
|
+
/** Called when the actor is not an operator. Default: throws Error('Forbidden'). */
|
|
27
|
+
onForbidden?: () => never;
|
|
28
|
+
}
|
|
29
|
+
type OperatorActionHandler<I, O> = (args: {
|
|
30
|
+
ctx: OperatorContext;
|
|
31
|
+
db: GovcoreDb;
|
|
32
|
+
}, input: I) => Promise<O>;
|
|
33
|
+
/**
|
|
34
|
+
* Build the app's typed `operatorAction`. Call once at app startup with the
|
|
35
|
+
* privileged pool and the session→context resolver.
|
|
36
|
+
*
|
|
37
|
+
* The handler receives the raw `operatorDb` (not a pre-opened transaction):
|
|
38
|
+
* cross-org operator mutations — e.g. `@govcore/tenancy`'s `createOrganization`
|
|
39
|
+
* / `updateUserAdministration` — open and audit their own transactions, so a
|
|
40
|
+
* forced outer transaction would only nest. Deliberately sets no org GUC.
|
|
41
|
+
*/
|
|
42
|
+
declare function createOperatorActions(config: CreateOperatorActionsConfig): <I = void, O = unknown>(handler: OperatorActionHandler<I, O>) => (input: I) => Promise<O>;
|
|
43
|
+
|
|
3
44
|
/** The trusted, server-resolved identity of the current actor. */
|
|
4
|
-
|
|
45
|
+
interface ActiveContext {
|
|
5
46
|
userId: string;
|
|
6
47
|
organizationId: string;
|
|
7
48
|
role: string;
|
|
8
49
|
instanceRole?: string | null;
|
|
9
50
|
}
|
|
10
51
|
/** What a handler receives: the active context plus a pre-bound audit writer. */
|
|
11
|
-
|
|
52
|
+
interface TenantActionContext extends ActiveContext {
|
|
12
53
|
/** Write an audit row in the *same* transaction, pre-filled with actor + org. */
|
|
13
54
|
audit: (event: AuditEvent) => Promise<void>;
|
|
14
55
|
}
|
|
15
|
-
|
|
56
|
+
interface CreateTenantActionsConfig {
|
|
16
57
|
db: GovcoreDb;
|
|
17
58
|
/** Resolve the current actor's active context — typically from the auth() session. */
|
|
18
59
|
getActiveContext: () => Promise<ActiveContext | null>;
|
|
@@ -30,11 +71,11 @@ export interface CreateTenantActionsConfig {
|
|
|
30
71
|
/** Called when the permission check fails. Default: throws Error('Forbidden'). */
|
|
31
72
|
onForbidden?: () => never;
|
|
32
73
|
}
|
|
33
|
-
|
|
74
|
+
type TenantActionHandler<I, O> = (args: {
|
|
34
75
|
ctx: TenantActionContext;
|
|
35
76
|
db: GovcoreDb;
|
|
36
77
|
}, input: I) => Promise<O>;
|
|
37
|
-
|
|
78
|
+
interface TenantActionOptions {
|
|
38
79
|
/** Permission required to run this action (checked against the active role). */
|
|
39
80
|
permission?: string;
|
|
40
81
|
}
|
|
@@ -42,5 +83,6 @@ export interface TenantActionOptions {
|
|
|
42
83
|
* Build the app's typed `tenantAction`. Call once at app startup with the app's
|
|
43
84
|
* db, session→context resolver, and rbac instance.
|
|
44
85
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
86
|
+
declare function createTenantActions(config: CreateTenantActionsConfig): <I = void, O = unknown>(options: TenantActionOptions, handler: TenantActionHandler<I, O>) => (input: I) => Promise<O>;
|
|
87
|
+
|
|
88
|
+
export { type ActiveContext, type CreateOperatorActionsConfig, type CreateTenantActionsConfig, INSTANCE_ADMIN_ROLE, type OperatorActionHandler, type OperatorContext, type TenantActionContext, type TenantActionHandler, type TenantActionOptions, createOperatorActions, createTenantActions };
|
package/dist/index.js
CHANGED
|
@@ -1,52 +1,70 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { writeAuditLog as writeAuditLog2 } from "@govcore/audit";
|
|
4
|
+
|
|
5
|
+
// src/operator.ts
|
|
6
|
+
import { writeAuditLog } from "@govcore/audit";
|
|
7
|
+
var INSTANCE_ADMIN_ROLE = "instance_admin";
|
|
8
|
+
function createOperatorActions(config) {
|
|
9
|
+
const { operatorDb, getActiveContext } = config;
|
|
10
|
+
const operatorRole = config.operatorRole ?? INSTANCE_ADMIN_ROLE;
|
|
11
|
+
return function operatorAction(handler) {
|
|
12
|
+
return async (input) => {
|
|
13
|
+
const active = await getActiveContext();
|
|
14
|
+
if (!active) {
|
|
15
|
+
if (config.onUnauthorized) return config.onUnauthorized();
|
|
16
|
+
throw new Error("Unauthorized");
|
|
17
|
+
}
|
|
18
|
+
if (active.instanceRole !== operatorRole) {
|
|
19
|
+
if (config.onForbidden) return config.onForbidden();
|
|
20
|
+
throw new Error("Forbidden");
|
|
21
|
+
}
|
|
22
|
+
const ctx = {
|
|
23
|
+
userId: active.userId,
|
|
24
|
+
instanceRole: active.instanceRole,
|
|
25
|
+
audit: (event) => writeAuditLog(operatorDb, { ...event, userId: event.userId ?? active.userId })
|
|
26
|
+
};
|
|
27
|
+
return handler({ ctx, db: operatorDb }, input);
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/index.ts
|
|
33
|
+
function createTenantActions(config) {
|
|
34
|
+
const { db, getActiveContext, rbac } = config;
|
|
35
|
+
return function tenantAction(options, handler) {
|
|
36
|
+
return async (input) => {
|
|
37
|
+
const active = await getActiveContext();
|
|
38
|
+
if (!active) {
|
|
39
|
+
if (config.onUnauthorized) return config.onUnauthorized();
|
|
40
|
+
throw new Error("Unauthorized");
|
|
41
|
+
}
|
|
42
|
+
if (options.permission) {
|
|
43
|
+
const allowed = rbac?.hasPermission(active.role, options.permission) ?? false;
|
|
44
|
+
if (!allowed) {
|
|
45
|
+
if (config.onForbidden) return config.onForbidden();
|
|
46
|
+
throw new Error("Forbidden");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return db.transaction(async (tx) => {
|
|
50
|
+
await tx.execute(sql`select set_config('app.current_org', ${active.organizationId}, true)`);
|
|
51
|
+
const txDb = tx;
|
|
52
|
+
const ctx = {
|
|
53
|
+
...active,
|
|
54
|
+
audit: (event) => writeAuditLog2(txDb, {
|
|
55
|
+
...event,
|
|
56
|
+
userId: event.userId ?? active.userId,
|
|
57
|
+
organizationId: event.organizationId ?? active.organizationId
|
|
58
|
+
})
|
|
50
59
|
};
|
|
60
|
+
return handler({ ctx, db: txDb }, input);
|
|
61
|
+
});
|
|
51
62
|
};
|
|
63
|
+
};
|
|
52
64
|
}
|
|
65
|
+
export {
|
|
66
|
+
INSTANCE_ADMIN_ROLE,
|
|
67
|
+
createOperatorActions,
|
|
68
|
+
createTenantActions
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/operator.ts"],"sourcesContent":["// @govcore/server — tenantAction, the enforced server-action seam.\n//\n// Every tenant-scoped mutation runs through here. tenantAction:\n// 1. resolves the actor's active context (never trusts caller input),\n// 2. applies a permission gate (if configured),\n// 3. opens a transaction and sets the transaction-local org GUC, so the RLS\n// policies in @govcore/schema apply to every query in the handler, and\n// 4. hands the handler an audit fn pre-bound to the actor + org.\n//\n// The app builds its own typed `tenantAction` once via createTenantActions,\n// injecting its db, its session→context resolver, and its rbac instance.\n\nimport { sql } from 'drizzle-orm'\nimport { type GovcoreDb } from '@govcore/schema'\nimport { writeAuditLog, type AuditEvent } from '@govcore/audit'\n\n/** The trusted, server-resolved identity of the current actor. */\nexport interface ActiveContext {\n userId: string\n organizationId: string\n role: string\n instanceRole?: string | null\n}\n\n/** What a handler receives: the active context plus a pre-bound audit writer. */\nexport interface TenantActionContext extends ActiveContext {\n /** Write an audit row in the *same* transaction, pre-filled with actor + org. */\n audit: (event: AuditEvent) => Promise<void>\n}\n\nexport interface CreateTenantActionsConfig {\n db: GovcoreDb\n /** Resolve the current actor's active context — typically from the auth() session. */\n getActiveContext: () => Promise<ActiveContext | null>\n /**\n * Optional permission gate — pass an `@govcore/rbac` instance directly. Method\n * syntax (not an arrow property) so its bivariant parameters accept a\n * `createRbac<Role, Permission>()` whose `hasPermission` is typed with the\n * app's role/permission *literals*, while the active role here is a `string`.\n */\n rbac?: { hasPermission(role: string, permission: string): boolean }\n /** Called when there is no active context. Default: throws Error('Unauthorized'). */\n onUnauthorized?: () => never\n /** Called when the permission check fails. Default: throws Error('Forbidden'). */\n onForbidden?: () => never\n}\n\nexport type TenantActionHandler<I, O> = (\n args: { ctx: TenantActionContext; db: GovcoreDb },\n input: I,\n) => Promise<O>\n\nexport interface TenantActionOptions {\n /** Permission required to run this action (checked against the active role). */\n permission?: string\n}\n\n/**\n * Build the app's typed `tenantAction`. Call once at app startup with the app's\n * db, session→context resolver, and rbac instance.\n */\nexport function createTenantActions(config: CreateTenantActionsConfig) {\n const { db, getActiveContext, rbac } = config\n\n return function tenantAction<I = void, O = unknown>(\n options: TenantActionOptions,\n handler: TenantActionHandler<I, O>,\n ): (input: I) => Promise<O> {\n return async (input: I): Promise<O> => {\n const active = await getActiveContext()\n if (!active) {\n if (config.onUnauthorized) return config.onUnauthorized()\n throw new Error('Unauthorized')\n }\n\n if (options.permission) {\n const allowed = rbac?.hasPermission(active.role, options.permission) ?? false\n if (!allowed) {\n if (config.onForbidden) return config.onForbidden()\n throw new Error('Forbidden')\n }\n }\n\n return db.transaction(async (tx) => {\n // Transaction-local org GUC → RLS policies bind to every query below.\n await tx.execute(sql`select set_config('app.current_org', ${active.organizationId}, true)`)\n const txDb = tx as unknown as GovcoreDb\n const ctx: TenantActionContext = {\n ...active,\n audit: (event) =>\n writeAuditLog(txDb, {\n ...event,\n userId: event.userId ?? active.userId,\n organizationId: event.organizationId ?? active.organizationId,\n }),\n }\n return handler({ ctx, db: txDb }, input)\n })\n }\n }\n}\n\nexport {\n createOperatorActions,\n INSTANCE_ADMIN_ROLE,\n type OperatorContext,\n type CreateOperatorActionsConfig,\n type OperatorActionHandler,\n} from './operator'\n","// @govcore/server — operatorAction, the operator-plane counterpart to tenantAction.\n//\n// The instance console legitimately crosses org boundaries: list every org,\n// provision a user in any org, read the platform audit. Those queries must NOT\n// run through the tenant seam — there is no single active org, and the runtime\n// pool's FORCE-RLS (keyed on the `app.current_org` GUC) would filter them to\n// nothing. So the operator plane is the mirror image of tenantAction:\n//\n// tenantAction — runtime pool · sets the org GUC · gated by an RBAC permission\n// operatorAction — privileged pool · NO org GUC (cross-org) · gated by instanceRole\n//\n// The privileged `operatorDb` is the same RLS-bypassing pool createAuth takes as\n// `authDb`. Wrapping every cross-org mutation in operatorAction is what turns a\n// consumer's ad-hoc `platformDb.select(...)` into a named, instance-admin-gated\n// seam — so the gate can't be forgotten and the plane has one definition.\n\nimport { type GovcoreDb } from '@govcore/schema'\nimport { writeAuditLog, type AuditEvent } from '@govcore/audit'\nimport type { ActiveContext } from './index'\n\n/** The default `instanceRole` value that grants operator-plane access. */\nexport const INSTANCE_ADMIN_ROLE = 'instance_admin'\n\n/** What an operator handler receives: the actor plus a pre-bound audit writer. */\nexport interface OperatorContext {\n userId: string\n instanceRole: string\n /** Write a platform audit row, pre-filled with the operator as actor. */\n audit: (event: AuditEvent) => Promise<void>\n}\n\nexport interface CreateOperatorActionsConfig {\n /**\n * The privileged, RLS-bypassing pool (the same one createAuth takes as\n * `authDb`). Operator queries run here because they are cross-org and must\n * not be filtered by the tenant GUC.\n */\n operatorDb: GovcoreDb\n /** Resolve the current actor's active context — typically from the auth() session. */\n getActiveContext: () => Promise<ActiveContext | null>\n /** The `instanceRole` that grants operator access. Default `instance_admin`. */\n operatorRole?: string\n /** Called when there is no active context. Default: throws Error('Unauthorized'). */\n onUnauthorized?: () => never\n /** Called when the actor is not an operator. Default: throws Error('Forbidden'). */\n onForbidden?: () => never\n}\n\nexport type OperatorActionHandler<I, O> = (\n args: { ctx: OperatorContext; db: GovcoreDb },\n input: I,\n) => Promise<O>\n\n/**\n * Build the app's typed `operatorAction`. Call once at app startup with the\n * privileged pool and the session→context resolver.\n *\n * The handler receives the raw `operatorDb` (not a pre-opened transaction):\n * cross-org operator mutations — e.g. `@govcore/tenancy`'s `createOrganization`\n * / `updateUserAdministration` — open and audit their own transactions, so a\n * forced outer transaction would only nest. Deliberately sets no org GUC.\n */\nexport function createOperatorActions(config: CreateOperatorActionsConfig) {\n const { operatorDb, getActiveContext } = config\n const operatorRole = config.operatorRole ?? INSTANCE_ADMIN_ROLE\n\n return function operatorAction<I = void, O = unknown>(\n handler: OperatorActionHandler<I, O>,\n ): (input: I) => Promise<O> {\n return async (input: I): Promise<O> => {\n const active = await getActiveContext()\n if (!active) {\n if (config.onUnauthorized) return config.onUnauthorized()\n throw new Error('Unauthorized')\n }\n if (active.instanceRole !== operatorRole) {\n if (config.onForbidden) return config.onForbidden()\n throw new Error('Forbidden')\n }\n\n const ctx: OperatorContext = {\n userId: active.userId,\n instanceRole: active.instanceRole,\n audit: (event) =>\n writeAuditLog(operatorDb, { ...event, userId: event.userId ?? active.userId }),\n }\n return handler({ ctx, db: operatorDb }, input)\n }\n }\n}\n"],"mappings":";AAYA,SAAS,WAAW;AAEpB,SAAS,iBAAAA,sBAAsC;;;ACG/C,SAAS,qBAAsC;AAIxC,IAAM,sBAAsB;AAyC5B,SAAS,sBAAsB,QAAqC;AACzE,QAAM,EAAE,YAAY,iBAAiB,IAAI;AACzC,QAAM,eAAe,OAAO,gBAAgB;AAE5C,SAAO,SAAS,eACd,SAC0B;AAC1B,WAAO,OAAO,UAAyB;AACrC,YAAM,SAAS,MAAM,iBAAiB;AACtC,UAAI,CAAC,QAAQ;AACX,YAAI,OAAO,eAAgB,QAAO,OAAO,eAAe;AACxD,cAAM,IAAI,MAAM,cAAc;AAAA,MAChC;AACA,UAAI,OAAO,iBAAiB,cAAc;AACxC,YAAI,OAAO,YAAa,QAAO,OAAO,YAAY;AAClD,cAAM,IAAI,MAAM,WAAW;AAAA,MAC7B;AAEA,YAAM,MAAuB;AAAA,QAC3B,QAAQ,OAAO;AAAA,QACf,cAAc,OAAO;AAAA,QACrB,OAAO,CAAC,UACN,cAAc,YAAY,EAAE,GAAG,OAAO,QAAQ,MAAM,UAAU,OAAO,OAAO,CAAC;AAAA,MACjF;AACA,aAAO,QAAQ,EAAE,KAAK,IAAI,WAAW,GAAG,KAAK;AAAA,IAC/C;AAAA,EACF;AACF;;;AD5BO,SAAS,oBAAoB,QAAmC;AACrE,QAAM,EAAE,IAAI,kBAAkB,KAAK,IAAI;AAEvC,SAAO,SAAS,aACd,SACA,SAC0B;AAC1B,WAAO,OAAO,UAAyB;AACrC,YAAM,SAAS,MAAM,iBAAiB;AACtC,UAAI,CAAC,QAAQ;AACX,YAAI,OAAO,eAAgB,QAAO,OAAO,eAAe;AACxD,cAAM,IAAI,MAAM,cAAc;AAAA,MAChC;AAEA,UAAI,QAAQ,YAAY;AACtB,cAAM,UAAU,MAAM,cAAc,OAAO,MAAM,QAAQ,UAAU,KAAK;AACxE,YAAI,CAAC,SAAS;AACZ,cAAI,OAAO,YAAa,QAAO,OAAO,YAAY;AAClD,gBAAM,IAAI,MAAM,WAAW;AAAA,QAC7B;AAAA,MACF;AAEA,aAAO,GAAG,YAAY,OAAO,OAAO;AAElC,cAAM,GAAG,QAAQ,2CAA2C,OAAO,cAAc,SAAS;AAC1F,cAAM,OAAO;AACb,cAAM,MAA2B;AAAA,UAC/B,GAAG;AAAA,UACH,OAAO,CAAC,UACNC,eAAc,MAAM;AAAA,YAClB,GAAG;AAAA,YACH,QAAQ,MAAM,UAAU,OAAO;AAAA,YAC/B,gBAAgB,MAAM,kBAAkB,OAAO;AAAA,UACjD,CAAC;AAAA,QACL;AACA,eAAO,QAAQ,EAAE,KAAK,IAAI,KAAK,GAAG,KAAK;AAAA,MACzC,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":["writeAuditLog","writeAuditLog"]}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@govcore/server",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "tenantAction — the server-action wrapper that opens the tenant transaction, sets the RLS org GUC, gates on permission, and binds audit",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"default": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"publishConfig": {
|
|
@@ -16,14 +16,30 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"drizzle-orm": "^0.45.2",
|
|
19
|
-
"@govcore/
|
|
20
|
-
"@govcore/
|
|
19
|
+
"@govcore/schema": "0.2.1",
|
|
20
|
+
"@govcore/audit": "0.1.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"typescript": "^5.5.0"
|
|
23
|
+
"typescript": "^5.5.0",
|
|
24
|
+
"tsup": "^8.3.0"
|
|
25
|
+
},
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"tsup": {
|
|
31
|
+
"entry": [
|
|
32
|
+
"src/index.ts"
|
|
33
|
+
],
|
|
34
|
+
"format": [
|
|
35
|
+
"esm"
|
|
36
|
+
],
|
|
37
|
+
"dts": true,
|
|
38
|
+
"clean": true,
|
|
39
|
+
"sourcemap": true
|
|
24
40
|
},
|
|
25
41
|
"scripts": {
|
|
26
|
-
"build": "
|
|
27
|
-
"dev": "
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch"
|
|
28
44
|
}
|
|
29
45
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# @govcore/server
|
|
2
|
-
|
|
3
|
-
## 0.1.2
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [d255afc]
|
|
8
|
-
- @govcore/schema@0.2.0
|
|
9
|
-
- @govcore/audit@0.1.1
|
|
10
|
-
|
|
11
|
-
## 0.1.1
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- f3bce48: `createTenantActions` now accepts a `createRbac()` instance for its `rbac` gate without a cast. `CreateTenantActionsConfig.rbac.hasPermission` is declared with method syntax so its bivariant parameters accept a gate typed with the app's role/permission _literals_ (what `@govcore/rbac` returns), while the active role is checked as a `string`. Removes a consumer footgun (#45).
|
|
16
|
-
|
|
17
|
-
## 0.1.0
|
|
18
|
-
|
|
19
|
-
### Minor Changes
|
|
20
|
-
|
|
21
|
-
- 8db62da: Phase 4 (part 1) — the action seam and the accessibility floor.
|
|
22
|
-
|
|
23
|
-
`@govcore/server`: `createTenantActions` → a typed `tenantAction` wrapper. It
|
|
24
|
-
resolves the actor's active context (never trusting caller input), applies an
|
|
25
|
-
optional permission gate, opens a transaction and sets the transaction-local
|
|
26
|
-
`app.current_org` GUC so the schema's RLS policies bind to every query, and hands
|
|
27
|
-
the handler an `audit` fn pre-bound to the actor + org.
|
|
28
|
-
|
|
29
|
-
`@govcore/theme`: a WCAG-AA base token set (`base.css`, light/dark, visible focus
|
|
30
|
-
ring), a Tailwind `baseTheme` preset, and `defineTheme` — which only allows
|
|
31
|
-
overriding allowlisted brand vars and sanitizes values to prevent inline-`<style>`
|
|
32
|
-
breakout (#769).
|
|
33
|
-
|
|
34
|
-
### Patch Changes
|
|
35
|
-
|
|
36
|
-
- Updated dependencies
|
|
37
|
-
- Updated dependencies [f2f3743]
|
|
38
|
-
- @govcore/audit@0.1.0
|
|
39
|
-
- @govcore/schema@0.1.0
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAE/D,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,iFAAiF;AACjF,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,iFAAiF;IACjF,KAAK,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5C;AAED,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,SAAS,CAAA;IACb,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IACrD;;;;;OAKG;IACH,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;IACnE,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,KAAK,CAAA;IAC5B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAC1B;AAED,MAAM,MAAM,mBAAmB,CAAC,CAAC,EAAE,CAAC,IAAI,CACtC,IAAI,EAAE;IAAE,GAAG,EAAE,mBAAmB,CAAC;IAAC,EAAE,EAAE,SAAS,CAAA;CAAE,EACjD,KAAK,EAAE,CAAC,KACL,OAAO,CAAC,CAAC,CAAC,CAAA;AAEf,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,IAGtC,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,OAAO,EAChD,SAAS,mBAAmB,EAC5B,SAAS,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,KACjC,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAiC5B"}
|
package/src/index.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// @govcore/server — tenantAction, the enforced server-action seam.
|
|
2
|
-
//
|
|
3
|
-
// Every tenant-scoped mutation runs through here. tenantAction:
|
|
4
|
-
// 1. resolves the actor's active context (never trusts caller input),
|
|
5
|
-
// 2. applies a permission gate (if configured),
|
|
6
|
-
// 3. opens a transaction and sets the transaction-local org GUC, so the RLS
|
|
7
|
-
// policies in @govcore/schema apply to every query in the handler, and
|
|
8
|
-
// 4. hands the handler an audit fn pre-bound to the actor + org.
|
|
9
|
-
//
|
|
10
|
-
// The app builds its own typed `tenantAction` once via createTenantActions,
|
|
11
|
-
// injecting its db, its session→context resolver, and its rbac instance.
|
|
12
|
-
|
|
13
|
-
import { sql } from 'drizzle-orm'
|
|
14
|
-
import { type GovcoreDb } from '@govcore/schema'
|
|
15
|
-
import { writeAuditLog, type AuditEvent } from '@govcore/audit'
|
|
16
|
-
|
|
17
|
-
/** The trusted, server-resolved identity of the current actor. */
|
|
18
|
-
export interface ActiveContext {
|
|
19
|
-
userId: string
|
|
20
|
-
organizationId: string
|
|
21
|
-
role: string
|
|
22
|
-
instanceRole?: string | null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** What a handler receives: the active context plus a pre-bound audit writer. */
|
|
26
|
-
export interface TenantActionContext extends ActiveContext {
|
|
27
|
-
/** Write an audit row in the *same* transaction, pre-filled with actor + org. */
|
|
28
|
-
audit: (event: AuditEvent) => Promise<void>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface CreateTenantActionsConfig {
|
|
32
|
-
db: GovcoreDb
|
|
33
|
-
/** Resolve the current actor's active context — typically from the auth() session. */
|
|
34
|
-
getActiveContext: () => Promise<ActiveContext | null>
|
|
35
|
-
/**
|
|
36
|
-
* Optional permission gate — pass an `@govcore/rbac` instance directly. Method
|
|
37
|
-
* syntax (not an arrow property) so its bivariant parameters accept a
|
|
38
|
-
* `createRbac<Role, Permission>()` whose `hasPermission` is typed with the
|
|
39
|
-
* app's role/permission *literals*, while the active role here is a `string`.
|
|
40
|
-
*/
|
|
41
|
-
rbac?: { hasPermission(role: string, permission: string): boolean }
|
|
42
|
-
/** Called when there is no active context. Default: throws Error('Unauthorized'). */
|
|
43
|
-
onUnauthorized?: () => never
|
|
44
|
-
/** Called when the permission check fails. Default: throws Error('Forbidden'). */
|
|
45
|
-
onForbidden?: () => never
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type TenantActionHandler<I, O> = (
|
|
49
|
-
args: { ctx: TenantActionContext; db: GovcoreDb },
|
|
50
|
-
input: I,
|
|
51
|
-
) => Promise<O>
|
|
52
|
-
|
|
53
|
-
export interface TenantActionOptions {
|
|
54
|
-
/** Permission required to run this action (checked against the active role). */
|
|
55
|
-
permission?: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Build the app's typed `tenantAction`. Call once at app startup with the app's
|
|
60
|
-
* db, session→context resolver, and rbac instance.
|
|
61
|
-
*/
|
|
62
|
-
export function createTenantActions(config: CreateTenantActionsConfig) {
|
|
63
|
-
const { db, getActiveContext, rbac } = config
|
|
64
|
-
|
|
65
|
-
return function tenantAction<I = void, O = unknown>(
|
|
66
|
-
options: TenantActionOptions,
|
|
67
|
-
handler: TenantActionHandler<I, O>,
|
|
68
|
-
): (input: I) => Promise<O> {
|
|
69
|
-
return async (input: I): Promise<O> => {
|
|
70
|
-
const active = await getActiveContext()
|
|
71
|
-
if (!active) {
|
|
72
|
-
if (config.onUnauthorized) return config.onUnauthorized()
|
|
73
|
-
throw new Error('Unauthorized')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (options.permission) {
|
|
77
|
-
const allowed = rbac?.hasPermission(active.role, options.permission) ?? false
|
|
78
|
-
if (!allowed) {
|
|
79
|
-
if (config.onForbidden) return config.onForbidden()
|
|
80
|
-
throw new Error('Forbidden')
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return db.transaction(async (tx) => {
|
|
85
|
-
// Transaction-local org GUC → RLS policies bind to every query below.
|
|
86
|
-
await tx.execute(sql`select set_config('app.current_org', ${active.organizationId}, true)`)
|
|
87
|
-
const txDb = tx as unknown as GovcoreDb
|
|
88
|
-
const ctx: TenantActionContext = {
|
|
89
|
-
...active,
|
|
90
|
-
audit: (event) =>
|
|
91
|
-
writeAuditLog(txDb, {
|
|
92
|
-
...event,
|
|
93
|
-
userId: event.userId ?? active.userId,
|
|
94
|
-
organizationId: event.organizationId ?? active.organizationId,
|
|
95
|
-
}),
|
|
96
|
-
}
|
|
97
|
-
return handler({ ctx, db: txDb }, input)
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|