@kindgi/authz 0.0.0-bootstrap.0 → 0.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/LICENSE +201 -0
- package/README.md +66 -2
- package/dist/check.d.ts +68 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +11 -0
- package/dist/check.js.map +1 -0
- package/dist/enqueue.d.ts +28 -0
- package/dist/enqueue.d.ts.map +1 -0
- package/dist/enqueue.js +4 -0
- package/dist/enqueue.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/pep.d.ts +46 -0
- package/dist/pep.d.ts.map +1 -0
- package/dist/pep.js +33 -0
- package/dist/pep.js.map +1 -0
- package/dist/principal.d.ts +60 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +71 -0
- package/dist/principal.js.map +1 -0
- package/dist/reviewer.d.ts +8 -0
- package/dist/reviewer.d.ts.map +1 -0
- package/dist/reviewer.js +13 -0
- package/dist/reviewer.js.map +1 -0
- package/dist/tuples.d.ts +142 -0
- package/dist/tuples.d.ts.map +1 -0
- package/dist/tuples.js +372 -0
- package/dist/tuples.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +86 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -4
- package/src/check.ts +96 -0
- package/src/enqueue.ts +42 -0
- package/src/index.ts +10 -0
- package/src/pep.ts +87 -0
- package/src/principal.ts +157 -0
- package/src/reviewer.ts +31 -0
- package/src/tuples.ts +595 -0
- package/src/types.ts +103 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
// FGA authorization primitives — public shape.
|
|
4
|
+
//
|
|
5
|
+
// Owns:
|
|
6
|
+
// - Object type enum (must match the `type X` declarations of the
|
|
7
|
+
// authorization model the deployment's FGA store runs)
|
|
8
|
+
// - Action verb enum (application-facing vocabulary for PEP callers)
|
|
9
|
+
// - Action → computed-relation mapping (PEP → FGA check translation)
|
|
10
|
+
// - Object → allowed-actions catalog (guards against category errors)
|
|
11
|
+
//
|
|
12
|
+
// Consumers:
|
|
13
|
+
// - Principal construction (`principal.ts`) builds {actor, onBehalfOf?}
|
|
14
|
+
// - PEPs check `ACTION_TO_RELATION[action]` on a resource through an
|
|
15
|
+
// `AuthzCheckBinding` (`check.ts`)
|
|
16
|
+
// - Policy executors reuse ObjectType for policy matching
|
|
17
|
+
export const OBJECT_TYPES = [
|
|
18
|
+
// Structural / boundary
|
|
19
|
+
'tenant',
|
|
20
|
+
'org',
|
|
21
|
+
'team',
|
|
22
|
+
'project',
|
|
23
|
+
'user',
|
|
24
|
+
// Content (project-scoped, user-authored)
|
|
25
|
+
'agent',
|
|
26
|
+
'flow',
|
|
27
|
+
'tool',
|
|
28
|
+
'guardrail',
|
|
29
|
+
'eval_suite',
|
|
30
|
+
'trigger',
|
|
31
|
+
'conversation',
|
|
32
|
+
// Config (polymorphic scope)
|
|
33
|
+
'secret',
|
|
34
|
+
'env',
|
|
35
|
+
'mcp_endpoint',
|
|
36
|
+
// Runtime
|
|
37
|
+
'run',
|
|
38
|
+
];
|
|
39
|
+
export const ACTIONS = [
|
|
40
|
+
'read',
|
|
41
|
+
'write',
|
|
42
|
+
'delete',
|
|
43
|
+
'admin',
|
|
44
|
+
'execute',
|
|
45
|
+
'invoke',
|
|
46
|
+
'publish',
|
|
47
|
+
'rotate',
|
|
48
|
+
'fire',
|
|
49
|
+
'cancel',
|
|
50
|
+
];
|
|
51
|
+
export const ACTION_TO_RELATION = {
|
|
52
|
+
read: 'can_read',
|
|
53
|
+
write: 'can_write',
|
|
54
|
+
delete: 'can_delete',
|
|
55
|
+
admin: 'can_admin',
|
|
56
|
+
execute: 'can_execute',
|
|
57
|
+
invoke: 'can_invoke',
|
|
58
|
+
publish: 'can_publish',
|
|
59
|
+
rotate: 'can_rotate',
|
|
60
|
+
fire: 'can_fire',
|
|
61
|
+
cancel: 'can_cancel',
|
|
62
|
+
};
|
|
63
|
+
// Which verbs are meaningful for which object types. A PEP uses this to
|
|
64
|
+
// reject e.g. `rotate` on `agent:X` before calling FGA.
|
|
65
|
+
export const OBJECT_ACTIONS = {
|
|
66
|
+
tenant: ['read', 'admin'],
|
|
67
|
+
org: ['read', 'admin'],
|
|
68
|
+
team: ['read', 'admin'],
|
|
69
|
+
project: ['read', 'write', 'admin', 'delete'],
|
|
70
|
+
user: [],
|
|
71
|
+
agent: ['read', 'write', 'delete', 'admin', 'execute', 'publish'],
|
|
72
|
+
flow: ['read', 'write', 'delete', 'admin', 'execute', 'publish'],
|
|
73
|
+
tool: ['read', 'write', 'delete', 'admin', 'invoke'],
|
|
74
|
+
guardrail: ['read', 'write', 'delete', 'admin'],
|
|
75
|
+
eval_suite: ['read', 'write', 'delete', 'admin', 'execute', 'publish'],
|
|
76
|
+
trigger: ['read', 'write', 'delete', 'admin', 'fire'],
|
|
77
|
+
conversation: ['read', 'write', 'delete', 'admin'],
|
|
78
|
+
secret: ['read', 'write', 'delete', 'admin', 'rotate'],
|
|
79
|
+
env: ['read', 'write', 'delete', 'admin'],
|
|
80
|
+
mcp_endpoint: ['read', 'write', 'delete', 'admin'],
|
|
81
|
+
run: ['read', 'cancel', 'delete'],
|
|
82
|
+
};
|
|
83
|
+
export function resourceKey(ref) {
|
|
84
|
+
return `${ref.type}:${ref.id}`;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC,+CAA+C;AAC/C,EAAE;AACF,QAAQ;AACR,oEAAoE;AACpE,2DAA2D;AAC3D,uEAAuE;AACvE,uEAAuE;AACvE,wEAAwE;AACxE,EAAE;AACF,aAAa;AACb,0EAA0E;AAC1E,uEAAuE;AACvE,uCAAuC;AACvC,4DAA4D;AAE5D,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,wBAAwB;IACxB,QAAQ;IACR,KAAK;IACL,MAAM;IACN,SAAS;IACT,MAAM;IACN,0CAA0C;IAC1C,OAAO;IACP,MAAM;IACN,MAAM;IACN,WAAW;IACX,YAAY;IACZ,SAAS;IACT,cAAc;IACd,6BAA6B;IAC7B,QAAQ;IACR,KAAK;IACL,cAAc;IACd,UAAU;IACV,KAAK;CACG,CAAC;AAIX,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,MAAM;IACN,OAAO;IACP,QAAQ;IACR,OAAO;IACP,SAAS;IACT,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;IACN,QAAQ;CACA,CAAC;AAIX,MAAM,CAAC,MAAM,kBAAkB,GAA2B;IACxD,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,WAAW;IAClB,MAAM,EAAE,YAAY;IACpB,KAAK,EAAE,WAAW;IAClB,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,aAAa;IACtB,MAAM,EAAE,YAAY;IACpB,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE,YAAY;CACrB,CAAC;AAEF,wEAAwE;AACxE,wDAAwD;AACxD,MAAM,CAAC,MAAM,cAAc,GAA0C;IACnE,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IACtB,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC;IAC7C,IAAI,EAAE,EAAE;IAER,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IACjE,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IAChE,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;IACpD,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC;IAC/C,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IACtE,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC;IACrD,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC;IAElD,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC;IACtD,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC;IACzC,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC;IAElD,GAAG,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC;CAClC,CAAC;AAOF,MAAM,UAAU,WAAW,CAAC,GAAgB;IAC1C,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;AACjC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/authz",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kindgi™ authorization shape. The public vocabulary and interfaces of Kindgi's authorization stack: Action verbs (read/write/execute/…), ObjectType enum (agent/flow/tool/…), ResourceRef, Principal (who is calling, with delegation and downscoping), the AuthzCheckBinding decision-point interface and its Decision (allow/deny + reason), FGA tuple builders and the TupleEnqueueHook outbox contract, the 403 DenyPayload shape, and reviewer roles. Types and pure functions — no runtime state. The FGA store, the check implementation and the tuple worker are supplied by the deployment.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kindgi/kindgi-sdk.git",
|
|
9
|
+
"directory": "packages/authz"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/kindgi/kindgi-sdk/tree/main/packages/authz#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kindgi/kindgi-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@kindgi/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.0.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.5",
|
|
41
|
+
"typescript": "^5.7.3",
|
|
42
|
+
"vitest": "^2.1.8"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run --typecheck --typecheck.only --passWithNoTests",
|
|
48
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/check.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// PDP Decision shape + pure `ref` constructor + AuthzCheckBinding
|
|
6
|
+
// interface.
|
|
7
|
+
//
|
|
8
|
+
// This package declares the contract only. An `AuthzCheckBinding`
|
|
9
|
+
// implementation resolves the tenant's FGA store and runs the relation
|
|
10
|
+
// checks; deployments supply one to the enforcement sites.
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
import type { Principal } from './principal.js';
|
|
14
|
+
import type { Action, ObjectType, ResourceRef } from './types.js';
|
|
15
|
+
|
|
16
|
+
export interface Decision {
|
|
17
|
+
readonly allowed: boolean;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
/**
|
|
20
|
+
* Which link in the (actor, onBehalfOf, scope) chain caused the deny.
|
|
21
|
+
* Undefined when `allowed = true`.
|
|
22
|
+
*/
|
|
23
|
+
readonly failing?: 'actor' | 'onBehalfOf' | 'scope' | 'unbootstrapped' | 'invalid-action';
|
|
24
|
+
/**
|
|
25
|
+
* Structured evidence for audit + debugging.
|
|
26
|
+
*/
|
|
27
|
+
readonly evidence: {
|
|
28
|
+
readonly action: Action;
|
|
29
|
+
readonly relation: string;
|
|
30
|
+
readonly resource: string;
|
|
31
|
+
readonly actorSubject: string;
|
|
32
|
+
readonly onBehalfOfSubject?: string;
|
|
33
|
+
readonly storeId?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convenience constructor for a `ResourceRef`. The type parameter
|
|
39
|
+
* checks `type` against `ObjectType` at the call site; the return type
|
|
40
|
+
* is plain `ResourceRef` (it does not narrow to the given type).
|
|
41
|
+
*/
|
|
42
|
+
export function ref<T extends ObjectType>(type: T, id: string): ResourceRef {
|
|
43
|
+
return { type, id };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Per-call correlation context for `AuthzCheckBinding` methods.
|
|
48
|
+
* Threaded so the emitted audit event carries the request-scoped
|
|
49
|
+
* ids that let operators trace a deny back to the HTTP request or
|
|
50
|
+
* run that produced it.
|
|
51
|
+
*/
|
|
52
|
+
export interface AuthzCheckContext {
|
|
53
|
+
/**
|
|
54
|
+
* Optional correlation id — e.g. the API request id from route
|
|
55
|
+
* middleware, or a run-scoped id. Attached to emitted audit events.
|
|
56
|
+
*/
|
|
57
|
+
readonly correlationId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Optional run id — attached to emitted audit events when the check
|
|
60
|
+
* happens inside a run. Distinct from `correlationId` because the
|
|
61
|
+
* run id survives across HTTP requests (streamed responses, replays).
|
|
62
|
+
*/
|
|
63
|
+
readonly runId?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Policy Decision Point binding. Every enforcement site that wants
|
|
68
|
+
* to check authz (for example the @kindgi/api middleware) goes
|
|
69
|
+
* through this contract. The deployment supplies the implementation.
|
|
70
|
+
*/
|
|
71
|
+
export interface AuthzCheckBinding {
|
|
72
|
+
/**
|
|
73
|
+
* Applies the principal's downscope filter first (cheap, no FGA
|
|
74
|
+
* round-trip), then calls FGA check for the actor and (if
|
|
75
|
+
* delegated) the `onBehalfOf`. Returns a `Decision` — allowed
|
|
76
|
+
* iff every check passes.
|
|
77
|
+
*/
|
|
78
|
+
check(
|
|
79
|
+
principal: Principal,
|
|
80
|
+
action: Action,
|
|
81
|
+
resource: ResourceRef,
|
|
82
|
+
ctx?: AuthzCheckContext,
|
|
83
|
+
): Promise<Decision>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* List-filtering primitive. Given a set of resources, returns
|
|
87
|
+
* `Decision`s in the same order — used by `filterByCan` at the
|
|
88
|
+
* API layer to strip rows a caller can't read.
|
|
89
|
+
*/
|
|
90
|
+
checkBatch(
|
|
91
|
+
principal: Principal,
|
|
92
|
+
action: Action,
|
|
93
|
+
resources: readonly ResourceRef[],
|
|
94
|
+
ctx?: AuthzCheckContext,
|
|
95
|
+
): Promise<readonly Decision[]>;
|
|
96
|
+
}
|
package/src/enqueue.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// Tuple-intent shape + enqueue hook contract — the interface between
|
|
6
|
+
// binding write methods and a tuple outbox.
|
|
7
|
+
//
|
|
8
|
+
// Bindings emit `TupleIntent[]` inside their write transactions; the
|
|
9
|
+
// binding implementation persists them atomically alongside the entity
|
|
10
|
+
// mutation, and a worker applies them to the FGA store. The intent
|
|
11
|
+
// shape is an FGA subject/relation/object triple + an optional
|
|
12
|
+
// idempotency key.
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
export interface TupleIntent {
|
|
16
|
+
readonly operation: 'write' | 'delete';
|
|
17
|
+
/** FGA subject, e.g. `user:alice` or `team:eng#member`. */
|
|
18
|
+
readonly user: string;
|
|
19
|
+
/** FGA relation, e.g. `admin` | `parent` | `editor`. */
|
|
20
|
+
readonly relation: string;
|
|
21
|
+
/** FGA object, e.g. `project:foo`. */
|
|
22
|
+
readonly object: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional per-tenant idempotency key. If set, the outbox rejects a
|
|
25
|
+
* duplicate enqueue (same tenantId + dedupKey) — safe to retry the
|
|
26
|
+
* caller without producing double-tuples.
|
|
27
|
+
*/
|
|
28
|
+
readonly dedupKey?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Callback that produces the tuples for an entity mutation. Bindings
|
|
33
|
+
* call this INSIDE their write transaction with the freshly-inserted
|
|
34
|
+
* (or freshly-resolved, for deletes) entity row's primary key. The
|
|
35
|
+
* binding then enqueues the returned intents in the same transaction.
|
|
36
|
+
*
|
|
37
|
+
* REQUIRED on every content + config binding's write methods. An
|
|
38
|
+
* optional hook would silently split the contract into "writes tuples"
|
|
39
|
+
* vs. "doesn't write tuples" impls — exactly the split-brain that makes
|
|
40
|
+
* PEP unreliable.
|
|
41
|
+
*/
|
|
42
|
+
export type TupleEnqueueHook = (entityRowId: string) => readonly TupleIntent[];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
export * from './principal.js';
|
|
6
|
+
export * from './pep.js';
|
|
7
|
+
export * from './tuples.js';
|
|
8
|
+
export * from './enqueue.js';
|
|
9
|
+
export * from './check.js';
|
|
10
|
+
export * from './reviewer.js';
|
package/src/pep.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// PEP-side primitives — framework-neutral helpers callable from any
|
|
6
|
+
// HTTP layer (Hono, Express, Fastify, gRPC). The Hono middleware of
|
|
7
|
+
// `@kindgi/api` lives in `packages/api/src/middleware/`.
|
|
8
|
+
//
|
|
9
|
+
// Two exports:
|
|
10
|
+
// - `principalFromToken` — TokenResolution → Principal, honoring
|
|
11
|
+
// service-account vs user semantics
|
|
12
|
+
// - `denyPayload` — canonical structured 403 body shape
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
import type { TenantId, UserId } from '@kindgi/types';
|
|
16
|
+
|
|
17
|
+
import type { Action, ObjectType } from '@kindgi/authz';
|
|
18
|
+
|
|
19
|
+
import { type Principal, serviceAccountPrincipal, userPrincipal } from './principal.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Subset of `TokenResolution` (`@kindgi/api`) that principal construction
|
|
23
|
+
* actually needs. Any bearer/session middleware can adapt its shape
|
|
24
|
+
* to this before calling `principalFromToken`.
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenLike {
|
|
27
|
+
readonly tenantId: TenantId;
|
|
28
|
+
readonly userId?: UserId;
|
|
29
|
+
/**
|
|
30
|
+
* Stable id for a capability-only bearer (service account). When
|
|
31
|
+
* the token carries no `userId`, this becomes the actor id.
|
|
32
|
+
* Deployments typically hash the token or use a token-record uuid.
|
|
33
|
+
*/
|
|
34
|
+
readonly tokenId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Map a resolved token to a Principal.
|
|
39
|
+
*
|
|
40
|
+
* Priority:
|
|
41
|
+
* 1. userId present → userPrincipal (human session)
|
|
42
|
+
* 2. tokenId present → serviceAccountPrincipal (integration key)
|
|
43
|
+
* 3. Neither → throw (unauthenticated shouldn't reach the PEP)
|
|
44
|
+
*
|
|
45
|
+
* Delegation chains (agent-on-behalf-of-user) are constructed by
|
|
46
|
+
* the caller after this returns — the token itself is always a
|
|
47
|
+
* single principal.
|
|
48
|
+
*/
|
|
49
|
+
export function principalFromToken(token: TokenLike): Principal {
|
|
50
|
+
if (token.userId !== undefined) {
|
|
51
|
+
return userPrincipal(token.userId, token.tenantId);
|
|
52
|
+
}
|
|
53
|
+
if (token.tokenId !== undefined) {
|
|
54
|
+
return serviceAccountPrincipal(token.tokenId, token.tenantId);
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
'principalFromToken: token has neither userId nor tokenId — bearer middleware should have rejected earlier',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Canonical 403 body shape. Structured so clients can distinguish
|
|
63
|
+
* "you didn't have permission" from "your input was invalid" —
|
|
64
|
+
* different remediation flows. Avoids leaking full evidence (that
|
|
65
|
+
* belongs in the audit trail); reason is a human-readable summary
|
|
66
|
+
* safe to return.
|
|
67
|
+
*/
|
|
68
|
+
export interface DenyPayload {
|
|
69
|
+
readonly code: 'permission-denied';
|
|
70
|
+
readonly action: Action;
|
|
71
|
+
readonly resource: string;
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function denyPayload(
|
|
76
|
+
action: Action,
|
|
77
|
+
resourceType: ObjectType,
|
|
78
|
+
resourceId: string,
|
|
79
|
+
reason: string,
|
|
80
|
+
): DenyPayload {
|
|
81
|
+
return {
|
|
82
|
+
code: 'permission-denied',
|
|
83
|
+
action,
|
|
84
|
+
resource: `${resourceType}:${resourceId}`,
|
|
85
|
+
reason,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/principal.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// Principal — the composite identity passed to every authz check.
|
|
6
|
+
//
|
|
7
|
+
// Composed of:
|
|
8
|
+
// actor: the direct caller (who is REALLY doing this)
|
|
9
|
+
// onBehalfOf: optional delegator (the user this action serves)
|
|
10
|
+
// scope: optional downscoping (OAuth-style permission narrowing)
|
|
11
|
+
//
|
|
12
|
+
// Delegation semantics (as in cloud service-account impersonation and
|
|
13
|
+
// OAuth 2.0 token exchange, RFC 8693): the effective permission is the
|
|
14
|
+
// INTERSECTION of actor + onBehalfOf + scope. `AuthzCheckBinding.check`
|
|
15
|
+
// (`check.ts`) enforces this: it applies the scope filter locally, then
|
|
16
|
+
// checks the actor and (if set) `onBehalfOf` against FGA, and returns
|
|
17
|
+
// `allowed` only when every link permits the action.
|
|
18
|
+
//
|
|
19
|
+
// Actor kinds:
|
|
20
|
+
// - `user` — a human user session
|
|
21
|
+
// - `agent` — a machine agent acting during a run.
|
|
22
|
+
// First-class FGA subject; agents get least-
|
|
23
|
+
// privilege grants (`machineAccessTuple` in
|
|
24
|
+
// `tuples.ts`).
|
|
25
|
+
// - `service_account` — machine identity for capability-only bearer
|
|
26
|
+
// tokens (CI/CD, integrations, deploy bots).
|
|
27
|
+
// First-class FGA subject; grants via
|
|
28
|
+
// `machineAccessTuple`. NOT tenant admin —
|
|
29
|
+
// starts with zero authority.
|
|
30
|
+
// - `system` — framework-initiated action (scheduled jobs,
|
|
31
|
+
// migrations, cleanup). Maps to the
|
|
32
|
+
// `user:system` subject, which the deployment's
|
|
33
|
+
// tenant setup grants tenant admin.
|
|
34
|
+
//
|
|
35
|
+
// Tools are not principals — they are objects that agents invoke.
|
|
36
|
+
//
|
|
37
|
+
// The intersection semantic guards against the confused-deputy
|
|
38
|
+
// problem: an agent must not be able to escalate a user's authority,
|
|
39
|
+
// and a user must not be able to escalate the agent's.
|
|
40
|
+
//
|
|
41
|
+
|
|
42
|
+
import type { TenantId } from '@kindgi/types';
|
|
43
|
+
|
|
44
|
+
import type { Action } from '@kindgi/authz';
|
|
45
|
+
|
|
46
|
+
// ============================================================
|
|
47
|
+
// TYPES
|
|
48
|
+
// ============================================================
|
|
49
|
+
|
|
50
|
+
export type PrincipalKind = 'user' | 'agent' | 'service_account' | 'system';
|
|
51
|
+
|
|
52
|
+
export interface PrincipalRef {
|
|
53
|
+
readonly kind: PrincipalKind;
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly tenantId: TenantId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DownscopedPermissions {
|
|
59
|
+
/**
|
|
60
|
+
* Whitelist. When set, only these actions are allowed regardless
|
|
61
|
+
* of what FGA says. Matches OAuth 2.0 scope-narrowing.
|
|
62
|
+
*/
|
|
63
|
+
readonly allowedActions?: readonly Action[];
|
|
64
|
+
/**
|
|
65
|
+
* Blacklist. When set, these actions are denied even if FGA allows.
|
|
66
|
+
* Applied AFTER allowedActions.
|
|
67
|
+
*/
|
|
68
|
+
readonly deniedActions?: readonly Action[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Principal {
|
|
72
|
+
readonly actor: PrincipalRef;
|
|
73
|
+
readonly onBehalfOf?: PrincipalRef;
|
|
74
|
+
readonly scope?: DownscopedPermissions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// CONSTRUCTORS — thin factories for readable call sites
|
|
79
|
+
// ============================================================
|
|
80
|
+
|
|
81
|
+
export function userPrincipal(userId: string, tenantId: TenantId): Principal {
|
|
82
|
+
return { actor: { kind: 'user', id: userId, tenantId } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function systemPrincipal(tenantId: TenantId): Principal {
|
|
86
|
+
return { actor: { kind: 'system', id: 'system', tenantId } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a principal from a service-account token. `id` is typically
|
|
91
|
+
* the token id (or a stable identifier derived from it) so grants can
|
|
92
|
+
* be attached to a specific integration.
|
|
93
|
+
*/
|
|
94
|
+
export function serviceAccountPrincipal(serviceAccountId: string, tenantId: TenantId): Principal {
|
|
95
|
+
return { actor: { kind: 'service_account', id: serviceAccountId, tenantId } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a delegated principal: `actor` acting on behalf of `onBehalfOf`.
|
|
100
|
+
* Both must be in the same tenant (checked at construction; mismatches
|
|
101
|
+
* are configuration bugs, not runtime authz denials).
|
|
102
|
+
*/
|
|
103
|
+
export function delegate(
|
|
104
|
+
actor: PrincipalRef,
|
|
105
|
+
onBehalfOf: PrincipalRef,
|
|
106
|
+
scope?: DownscopedPermissions,
|
|
107
|
+
): Principal {
|
|
108
|
+
if (actor.tenantId !== onBehalfOf.tenantId) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`delegate: cross-tenant delegation not supported (actor.tenantId=${actor.tenantId}, onBehalfOf.tenantId=${onBehalfOf.tenantId})`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
actor,
|
|
115
|
+
onBehalfOf,
|
|
116
|
+
...(scope !== undefined ? { scope } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Narrow an existing principal's permissions further. Composable with
|
|
122
|
+
* delegation — an agent-on-behalf-of-user can be additionally scoped
|
|
123
|
+
* to read-only for a specific request.
|
|
124
|
+
*/
|
|
125
|
+
export function downscope(principal: Principal, scope: DownscopedPermissions): Principal {
|
|
126
|
+
return { ...principal, scope };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================
|
|
130
|
+
// FGA MAPPING — encapsulated in one place
|
|
131
|
+
// ============================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Return the FGA subject string for a PrincipalRef. This is the ONE
|
|
135
|
+
* place that decides how principal kinds map to FGA subjects.
|
|
136
|
+
*
|
|
137
|
+
* - `user` → `user:${id}` (real user UUID)
|
|
138
|
+
* - `agent` → `agent:${id}`
|
|
139
|
+
* - `service_account` → `service_account:${id}` (machine identity
|
|
140
|
+
* with narrow grants)
|
|
141
|
+
* - `system` → `user:system` (service identity the tenant's
|
|
142
|
+
* setup grants tenant admin; with one FGA store
|
|
143
|
+
* per tenant the string never crosses tenant
|
|
144
|
+
* boundaries)
|
|
145
|
+
*/
|
|
146
|
+
export function fgaSubject(ref: PrincipalRef): string {
|
|
147
|
+
switch (ref.kind) {
|
|
148
|
+
case 'user':
|
|
149
|
+
return `user:${ref.id}`;
|
|
150
|
+
case 'agent':
|
|
151
|
+
return `agent:${ref.id}`;
|
|
152
|
+
case 'service_account':
|
|
153
|
+
return `service_account:${ref.id}`;
|
|
154
|
+
case 'system':
|
|
155
|
+
return 'user:system';
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/reviewer.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
//
|
|
5
|
+
// Reviewer role vocabulary — the seniority ranks used by HITL approval
|
|
6
|
+
// flows to gate action review. Higher rank subsumes lower: a `senior`
|
|
7
|
+
// reviewer can act on anything requiring `standard`, and `admin` on
|
|
8
|
+
// anything requiring `standard` or `senior`.
|
|
9
|
+
//
|
|
10
|
+
// Role type + rank map live in the public authz vocabulary because
|
|
11
|
+
// reviewer roles are consumed by:
|
|
12
|
+
// - HITL approval bindings
|
|
13
|
+
// - API route validators and `ReviewerBinding` (@kindgi/api)
|
|
14
|
+
// - Client response types (@kindgi/client)
|
|
15
|
+
//
|
|
16
|
+
// Concrete role assignment / storage is caller-plugged via the
|
|
17
|
+
// `ReviewerBinding` interface (declared in @kindgi/api).
|
|
18
|
+
//
|
|
19
|
+
|
|
20
|
+
export type ReviewerRole = 'standard' | 'senior' | 'admin';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Rank order — index into an ordered scale so callers can express
|
|
24
|
+
* "requires >= senior" without hard-coding string comparisons.
|
|
25
|
+
* Higher number = more senior.
|
|
26
|
+
*/
|
|
27
|
+
export const REVIEWER_ROLE_RANK: Readonly<Record<ReviewerRole, number>> = {
|
|
28
|
+
standard: 0,
|
|
29
|
+
senior: 1,
|
|
30
|
+
admin: 2,
|
|
31
|
+
};
|