@kindgi/api 0.1.4-rc.1 → 0.1.4-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-binding.d.ts +12 -2
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts.map +1 -1
- package/dist/agent-pins.js +4 -1
- package/dist/agent-pins.js.map +1 -1
- package/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +23 -4
- package/dist/app.js.map +1 -1
- package/dist/deploy-versions.d.ts +8 -5
- package/dist/deploy-versions.d.ts.map +1 -1
- package/dist/deploy-versions.js +3 -9
- package/dist/deploy-versions.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +10 -0
- package/dist/eval-case-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.d.ts +14 -3
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.js.map +1 -1
- package/dist/flow-pins.d.ts +20 -7
- package/dist/flow-pins.d.ts.map +1 -1
- package/dist/flow-pins.js +36 -11
- package/dist/flow-pins.js.map +1 -1
- package/dist/gate-policy-binding.d.ts +164 -0
- package/dist/gate-policy-binding.d.ts.map +1 -0
- package/dist/gate-policy-binding.js +12 -0
- package/dist/gate-policy-binding.js.map +1 -0
- package/dist/gate.d.ts +56 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +359 -0
- package/dist/gate.js.map +1 -0
- package/dist/handler-binding.d.ts +14 -2
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/hitl-binding.d.ts +5 -0
- package/dist/hitl-binding.d.ts.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +16 -1
- package/dist/judged-dispatcher.d.ts.map +1 -1
- package/dist/judged-dispatcher.js +45 -6
- package/dist/judged-dispatcher.js.map +1 -1
- package/dist/judgment-binding.d.ts +38 -0
- package/dist/judgment-binding.d.ts.map +1 -1
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -1
- package/dist/live-version-binding.d.ts +206 -0
- package/dist/live-version-binding.d.ts.map +1 -0
- package/dist/live-version-binding.js +4 -0
- package/dist/live-version-binding.js.map +1 -0
- package/dist/middleware/authorize.d.ts.map +1 -1
- package/dist/middleware/authorize.js +13 -2
- package/dist/middleware/authorize.js.map +1 -1
- package/dist/middleware/project-ref.d.ts +25 -0
- package/dist/middleware/project-ref.d.ts.map +1 -0
- package/dist/middleware/project-ref.js +72 -0
- package/dist/middleware/project-ref.js.map +1 -0
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -0
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts +6 -0
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +512 -22
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +37 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +709 -5
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/publish-refused.d.ts +18 -0
- package/dist/publish-refused.d.ts.map +1 -0
- package/dist/publish-refused.js +22 -0
- package/dist/publish-refused.js.map +1 -0
- package/dist/retention-binding.d.ts +29 -0
- package/dist/retention-binding.d.ts.map +1 -1
- package/dist/routes/agent-releases.d.ts +40 -0
- package/dist/routes/agent-releases.d.ts.map +1 -0
- package/dist/routes/agent-releases.js +462 -0
- package/dist/routes/agent-releases.js.map +1 -0
- package/dist/routes/agents.d.ts +7 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +31 -3
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +40 -9
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/audit.d.ts.map +1 -1
- package/dist/routes/audit.js +7 -0
- package/dist/routes/audit.js.map +1 -1
- package/dist/routes/auth.js +1 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/conversations.d.ts +0 -7
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +19 -3
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/deployments.d.ts +6 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +33 -5
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/env.d.ts.map +1 -1
- package/dist/routes/env.js +1 -0
- package/dist/routes/env.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +2 -1
- package/dist/routes/eval-comparison.d.ts.map +1 -1
- package/dist/routes/eval-comparison.js +22 -9
- package/dist/routes/eval-comparison.js.map +1 -1
- package/dist/routes/flows.d.ts +14 -7
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +10 -8
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/gate-policies.d.ts +19 -0
- package/dist/routes/gate-policies.d.ts.map +1 -0
- package/dist/routes/gate-policies.js +191 -0
- package/dist/routes/gate-policies.js.map +1 -0
- package/dist/routes/gate-policy-spec.d.ts +17 -0
- package/dist/routes/gate-policy-spec.d.ts.map +1 -0
- package/dist/routes/gate-policy-spec.js +192 -0
- package/dist/routes/gate-policy-spec.js.map +1 -0
- package/dist/routes/gate-policy-wire.d.ts +3 -0
- package/dist/routes/gate-policy-wire.d.ts.map +1 -0
- package/dist/routes/gate-policy-wire.js +18 -0
- package/dist/routes/gate-policy-wire.js.map +1 -0
- package/dist/routes/hierarchy-errors.d.ts +11 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -1
- package/dist/routes/hierarchy-errors.js +13 -0
- package/dist/routes/hierarchy-errors.js.map +1 -1
- package/dist/routes/judged-suites.js +7 -0
- package/dist/routes/judged-suites.js.map +1 -1
- package/dist/routes/judgments.d.ts +4 -1
- package/dist/routes/judgments.d.ts.map +1 -1
- package/dist/routes/judgments.js +92 -19
- package/dist/routes/judgments.js.map +1 -1
- package/dist/routes/live-scope-wire.d.ts +32 -0
- package/dist/routes/live-scope-wire.d.ts.map +1 -0
- package/dist/routes/live-scope-wire.js +71 -0
- package/dist/routes/live-scope-wire.js.map +1 -0
- package/dist/routes/policies.d.ts +6 -3
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +55 -3
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +47 -3
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/retention.d.ts.map +1 -1
- package/dist/routes/retention.js +18 -2
- package/dist/routes/retention.js.map +1 -1
- package/dist/routes/reviewers.d.ts +8 -1
- package/dist/routes/reviewers.d.ts.map +1 -1
- package/dist/routes/reviewers.js +20 -3
- package/dist/routes/reviewers.js.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +22 -13
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +2 -0
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/segments.d.ts +17 -0
- package/dist/routes/segments.d.ts.map +1 -0
- package/dist/routes/segments.js +68 -0
- package/dist/routes/segments.js.map +1 -0
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +42 -3
- package/dist/routes/teams.js.map +1 -1
- package/dist/routes/uuid-param.d.ts +11 -0
- package/dist/routes/uuid-param.d.ts.map +1 -0
- package/dist/routes/uuid-param.js +19 -0
- package/dist/routes/uuid-param.js.map +1 -0
- package/openapi.json +7008 -3624
- package/package.json +21 -21
- package/src/agent-binding.ts +12 -2
- package/src/agent-pins.ts +3 -1
- package/src/app.ts +37 -3
- package/src/deploy-versions.ts +11 -14
- package/src/errors.ts +25 -0
- package/src/eval-case-binding.ts +7 -0
- package/src/eval-run-binding.ts +24 -3
- package/src/flow-pins.ts +49 -9
- package/src/gate-policy-binding.ts +171 -0
- package/src/gate.ts +467 -0
- package/src/handler-binding.ts +14 -2
- package/src/hitl-binding.ts +5 -0
- package/src/index.ts +46 -1
- package/src/judged-dispatcher.ts +74 -6
- package/src/judgment-binding.ts +62 -0
- package/src/live-version-binding.ts +237 -0
- package/src/middleware/authorize.ts +18 -2
- package/src/middleware/project-ref.ts +88 -0
- package/src/openapi/generate.ts +5 -0
- package/src/openapi/operations.ts +619 -22
- package/src/openapi/schemas.ts +788 -5
- package/src/publish-refused.ts +31 -0
- package/src/retention-binding.ts +30 -0
- package/src/routes/agent-releases.ts +581 -0
- package/src/routes/agents.ts +43 -2
- package/src/routes/approvals.ts +58 -8
- package/src/routes/audit.ts +10 -0
- package/src/routes/auth.ts +1 -1
- package/src/routes/conversations.ts +31 -3
- package/src/routes/deployments.ts +50 -5
- package/src/routes/env.ts +1 -0
- package/src/routes/eval-comparison.ts +24 -10
- package/src/routes/flows.ts +27 -11
- package/src/routes/gate-policies.ts +226 -0
- package/src/routes/gate-policy-spec.ts +229 -0
- package/src/routes/gate-policy-wire.ts +20 -0
- package/src/routes/hierarchy-errors.ts +14 -0
- package/src/routes/judged-suites.ts +5 -0
- package/src/routes/judgments.ts +107 -23
- package/src/routes/live-scope-wire.ts +90 -0
- package/src/routes/policies.ts +61 -3
- package/src/routes/projects.ts +54 -2
- package/src/routes/retention.ts +21 -2
- package/src/routes/reviewers.ts +23 -4
- package/src/routes/runs.ts +33 -20
- package/src/routes/secrets.ts +2 -0
- package/src/routes/segments.ts +75 -0
- package/src/routes/teams.ts +53 -3
- package/src/routes/uuid-param.ts +28 -0
package/src/routes/runs.ts
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
|
-
import type { MiddlewareHandler } from 'hono';
|
|
6
5
|
|
|
7
6
|
import type { AgentId } from '@kindgi/agents';
|
|
8
7
|
import type { KernelRunRecord, ListRunsInput, RunBinding } from '@kindgi/runtime';
|
|
9
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
FlowId,
|
|
10
|
+
ListScope,
|
|
11
|
+
ProjectId,
|
|
12
|
+
RunId,
|
|
13
|
+
ScopeSegment,
|
|
14
|
+
Semver,
|
|
15
|
+
TenantId,
|
|
16
|
+
} from '@kindgi/types';
|
|
10
17
|
|
|
11
18
|
import { ref } from '@kindgi/authz';
|
|
12
19
|
|
|
@@ -20,9 +27,11 @@ import type {
|
|
|
20
27
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
21
28
|
import type { MintPublicRunTokenResult } from '../public-run-token.js';
|
|
22
29
|
import type { AppEnv } from '../types.js';
|
|
30
|
+
import { liveScopeToWire } from './live-scope-wire.js';
|
|
23
31
|
import type { DecodedCursor } from './pagination.js';
|
|
24
32
|
import { clampLimit, decodeCursor } from './pagination.js';
|
|
25
33
|
import { parseListScope } from './scope-params.js';
|
|
34
|
+
import { parseSegmentsBody } from './segments.js';
|
|
26
35
|
import {
|
|
27
36
|
formatSseFrame,
|
|
28
37
|
isTerminalWireKind,
|
|
@@ -30,6 +39,7 @@ import {
|
|
|
30
39
|
projectJournalEntry,
|
|
31
40
|
toRunProgressEvent,
|
|
32
41
|
} from './sse.js';
|
|
42
|
+
import { UUID_RE, refuseMalformedUuidParam } from './uuid-param.js';
|
|
33
43
|
|
|
34
44
|
const KERNEL_RUN_CHANNEL_PREFIX = 'kernel:run:';
|
|
35
45
|
|
|
@@ -61,23 +71,8 @@ const MAX_RUN_ANCESTRY = 16;
|
|
|
61
71
|
* `POST /:runId/cancel`, `POST /:runId/resume`, `GET /:runId/stream`
|
|
62
72
|
* (SSE), `GET /:runId/journal`.
|
|
63
73
|
*/
|
|
64
|
-
/** A run id
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A `:runId` that isn't a run id is a 400, before it reaches a query (where
|
|
69
|
-
* Postgres's uuid cast would fail it as a 500).
|
|
70
|
-
*/
|
|
71
|
-
const refuseMalformedRunId: MiddlewareHandler<AppEnv> = async (c, next) => {
|
|
72
|
-
if (RUN_ID_RE.test(c.req.param('runId') ?? '')) return next();
|
|
73
|
-
c.status(statusFor('bad-input') as never);
|
|
74
|
-
return c.json(
|
|
75
|
-
toWireError(
|
|
76
|
-
{ code: 'bad-input', message: '`runId` must be a run id (a UUID)' },
|
|
77
|
-
c.get('requestId'),
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
};
|
|
74
|
+
/** A `:runId` that isn't a run id is a 400 (`uuid-param.ts`). */
|
|
75
|
+
const refuseMalformedRunId = refuseMalformedUuidParam('runId', 'a run id');
|
|
81
76
|
|
|
82
77
|
export function runsRouter(
|
|
83
78
|
binding: RunHandlerBinding,
|
|
@@ -634,6 +629,7 @@ function invokeFromBody(
|
|
|
634
629
|
tenantId,
|
|
635
630
|
...(body.projectId !== undefined && { projectId: body.projectId }),
|
|
636
631
|
input: body.input,
|
|
632
|
+
...(body.segments !== undefined && { segments: body.segments }),
|
|
637
633
|
...(body.dryRun !== undefined && { dryRun: body.dryRun }),
|
|
638
634
|
...(body.wait !== undefined && { wait: body.wait }),
|
|
639
635
|
};
|
|
@@ -681,11 +677,19 @@ function serializeRun(
|
|
|
681
677
|
id: row.agent.id,
|
|
682
678
|
version: row.agent.version,
|
|
683
679
|
conversationId: row.agent.conversationId as unknown as string,
|
|
680
|
+
...(row.agent.via !== undefined && { via: row.agent.via }),
|
|
681
|
+
...(row.agent.liveScope !== undefined && {
|
|
682
|
+
liveScope: liveScopeToWire(row.agent.liveScope),
|
|
683
|
+
}),
|
|
684
684
|
},
|
|
685
685
|
}),
|
|
686
686
|
...(row.replayOf != null && { replayOf: row.replayOf as unknown as string }),
|
|
687
687
|
...(row.evalRunId != null && { evalRunId: row.evalRunId }),
|
|
688
688
|
...(row.versions != null && { versions: row.versions }),
|
|
689
|
+
...(row.segments !== undefined &&
|
|
690
|
+
row.segments.length > 0 && {
|
|
691
|
+
segments: row.segments.map(({ key, value }) => ({ key, value })),
|
|
692
|
+
}),
|
|
689
693
|
};
|
|
690
694
|
}
|
|
691
695
|
|
|
@@ -777,7 +781,7 @@ function parseRunListFilter(
|
|
|
777
781
|
return { kind: 'err', message: '`topLevel` must be `true` or `false`' };
|
|
778
782
|
}
|
|
779
783
|
const topLevelOnly = topLevel === 'true';
|
|
780
|
-
if (parentRunId !== undefined && !
|
|
784
|
+
if (parentRunId !== undefined && !UUID_RE.test(parentRunId)) {
|
|
781
785
|
return { kind: 'err', message: '`parentRunId` must be a run id (a UUID)' };
|
|
782
786
|
}
|
|
783
787
|
if (parentRunId !== undefined && topLevelOnly) {
|
|
@@ -849,6 +853,7 @@ type ParsedStartRunBody =
|
|
|
849
853
|
readonly agentId: AgentId;
|
|
850
854
|
readonly agentVersion?: Semver;
|
|
851
855
|
readonly projectId?: ProjectId;
|
|
856
|
+
readonly segments?: readonly ScopeSegment[];
|
|
852
857
|
readonly input: unknown;
|
|
853
858
|
readonly dryRun?: boolean;
|
|
854
859
|
readonly wait?: boolean;
|
|
@@ -858,6 +863,7 @@ type ParsedStartRunBody =
|
|
|
858
863
|
readonly flowId: FlowId;
|
|
859
864
|
readonly flowVersion?: Semver;
|
|
860
865
|
readonly projectId?: ProjectId;
|
|
866
|
+
readonly segments?: readonly ScopeSegment[];
|
|
861
867
|
readonly input: unknown;
|
|
862
868
|
readonly dryRun?: boolean;
|
|
863
869
|
readonly wait?: boolean;
|
|
@@ -893,6 +899,11 @@ function parseStartRunBody(
|
|
|
893
899
|
const options = parseStartOptions(b.options);
|
|
894
900
|
if (options.kind === 'err') return options;
|
|
895
901
|
const { dryRun, wait } = options.value;
|
|
902
|
+
const parsedSegments = parseSegmentsBody(b.segments);
|
|
903
|
+
if (parsedSegments.kind === 'err') {
|
|
904
|
+
return { kind: 'err', error: { code: 'bad-input', message: parsedSegments.message } };
|
|
905
|
+
}
|
|
906
|
+
const { segments } = parsedSegments;
|
|
896
907
|
if (hasAgent) {
|
|
897
908
|
const agentVersion = b.agentVersion;
|
|
898
909
|
if (agentVersion !== undefined && typeof agentVersion !== 'string') {
|
|
@@ -915,6 +926,7 @@ function parseStartRunBody(
|
|
|
915
926
|
agentId: b.agent as AgentId,
|
|
916
927
|
...(agentVersion !== undefined && { agentVersion: agentVersion as Semver }),
|
|
917
928
|
...(agentProjectIdRaw !== undefined && { projectId: agentProjectIdRaw as ProjectId }),
|
|
929
|
+
...(segments !== undefined && { segments }),
|
|
918
930
|
input: b.input,
|
|
919
931
|
...(dryRun !== undefined && { dryRun }),
|
|
920
932
|
...(wait !== undefined && { wait }),
|
|
@@ -942,6 +954,7 @@ function parseStartRunBody(
|
|
|
942
954
|
flowId: b.flow as FlowId,
|
|
943
955
|
...(flowVersion !== undefined && { flowVersion: flowVersion as Semver }),
|
|
944
956
|
...(projectId !== undefined && { projectId: projectId as ProjectId }),
|
|
957
|
+
...(segments !== undefined && { segments }),
|
|
945
958
|
input: b.input,
|
|
946
959
|
...(dryRun !== undefined && { dryRun }),
|
|
947
960
|
...(wait !== undefined && { wait }),
|
package/src/routes/secrets.ts
CHANGED
|
@@ -160,6 +160,7 @@ export function secretsRouter(options: SecretsRouterOptions): Hono<AppEnv> {
|
|
|
160
160
|
});
|
|
161
161
|
return c.json({
|
|
162
162
|
data: page.data.map(serializeSecretRecord),
|
|
163
|
+
hasMore: page.nextCursor !== undefined,
|
|
163
164
|
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
164
165
|
});
|
|
165
166
|
});
|
|
@@ -232,6 +233,7 @@ export function secretsRouter(options: SecretsRouterOptions): Hono<AppEnv> {
|
|
|
232
233
|
});
|
|
233
234
|
return c.json({
|
|
234
235
|
data: page.data.map(serializeVersion),
|
|
236
|
+
hasMore: page.nextCursor !== undefined,
|
|
235
237
|
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
236
238
|
});
|
|
237
239
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ScopeSegment } from '@kindgi/types';
|
|
5
|
+
|
|
6
|
+
/** A segment key: lowercase, like an identifier (`company`, `contact-role`). */
|
|
7
|
+
const SEGMENT_KEY_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
8
|
+
const MAX_SEGMENTS = 8;
|
|
9
|
+
const MAX_VALUE_LENGTH = 256;
|
|
10
|
+
|
|
11
|
+
type Parsed =
|
|
12
|
+
| { readonly kind: 'ok'; readonly segments?: readonly ScopeSegment[] }
|
|
13
|
+
| { readonly kind: 'err'; readonly message: string };
|
|
14
|
+
|
|
15
|
+
function checked(segments: readonly ScopeSegment[], field: string): Parsed {
|
|
16
|
+
if (segments.length === 0) return { kind: 'ok' };
|
|
17
|
+
if (segments.length > MAX_SEGMENTS) {
|
|
18
|
+
return { kind: 'err', message: `\`${field}\` takes at most ${MAX_SEGMENTS} segments` };
|
|
19
|
+
}
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
for (const { key, value } of segments) {
|
|
22
|
+
if (!SEGMENT_KEY_RE.test(key)) {
|
|
23
|
+
return {
|
|
24
|
+
kind: 'err',
|
|
25
|
+
message: `\`${field}\`: a segment key is lowercase letters, digits, - and _ (starting with a letter); got "${key}"`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (seen.has(key))
|
|
29
|
+
return { kind: 'err', message: `\`${field}\`: the key "${key}" appears twice` };
|
|
30
|
+
seen.add(key);
|
|
31
|
+
if (value.length === 0 || value.length > MAX_VALUE_LENGTH) {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'err',
|
|
34
|
+
message: `\`${field}\`: a segment value is 1–${MAX_VALUE_LENGTH} characters (key "${key}")`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'ok', segments };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A run's or a pin's segment path from a JSON body: an ordered list of
|
|
43
|
+
* `{key, value}`, coarse to fine (e.g. `company` then `role`).
|
|
44
|
+
*/
|
|
45
|
+
export function parseSegmentsBody(raw: unknown, field = 'segments'): Parsed {
|
|
46
|
+
if (raw === undefined) return { kind: 'ok' };
|
|
47
|
+
if (!Array.isArray(raw)) {
|
|
48
|
+
return {
|
|
49
|
+
kind: 'err',
|
|
50
|
+
message: `\`${field}\` must be an array of {key, value}, coarse to fine`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const segments: ScopeSegment[] = [];
|
|
54
|
+
for (const item of raw) {
|
|
55
|
+
const { key, value } = (item ?? {}) as { key?: unknown; value?: unknown };
|
|
56
|
+
if (typeof key !== 'string' || typeof value !== 'string') {
|
|
57
|
+
return { kind: 'err', message: `\`${field}\`: each segment is {key: string, value: string}` };
|
|
58
|
+
}
|
|
59
|
+
segments.push({ key, value });
|
|
60
|
+
}
|
|
61
|
+
return checked(segments, field);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The same from a query: repeated `segment=key:value`, in order. */
|
|
65
|
+
export function parseSegmentsQuery(values: readonly string[]): Parsed {
|
|
66
|
+
const segments: ScopeSegment[] = [];
|
|
67
|
+
for (const raw of values) {
|
|
68
|
+
const colon = raw.indexOf(':');
|
|
69
|
+
if (colon <= 0) {
|
|
70
|
+
return { kind: 'err', message: `\`segment\` is key:value; got "${raw}"` };
|
|
71
|
+
}
|
|
72
|
+
segments.push({ key: raw.slice(0, colon), value: raw.slice(colon + 1) });
|
|
73
|
+
}
|
|
74
|
+
return checked(segments, 'segment');
|
|
75
|
+
}
|
package/src/routes/teams.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TeamBinding,
|
|
11
11
|
TeamMembership,
|
|
12
12
|
TeamMembershipBinding,
|
|
13
|
+
TeamMembershipUpdateRoleOutcome,
|
|
13
14
|
TeamPatch,
|
|
14
15
|
TeamRole,
|
|
15
16
|
TeamSpec,
|
|
@@ -19,7 +20,11 @@ import type { Cursor, OrgId, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
|
19
20
|
import { statusFor, toWireError } from '../errors.js';
|
|
20
21
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
21
22
|
import type { AppEnv } from '../types.js';
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
membershipNotKeptInStepError,
|
|
25
|
+
orgNotFoundError,
|
|
26
|
+
slugConflictError,
|
|
27
|
+
} from './hierarchy-errors.js';
|
|
23
28
|
import { clampLimit } from './pagination.js';
|
|
24
29
|
|
|
25
30
|
/**
|
|
@@ -57,6 +62,34 @@ export function teamsRouter(
|
|
|
57
62
|
): Hono<AppEnv> {
|
|
58
63
|
const r = new Hono<AppEnv>();
|
|
59
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Change a member's role. With an authorizer, the row and its FGA tuple
|
|
67
|
+
* change together through the tenant-hierarchy binding, or the change
|
|
68
|
+
* is refused; without one, the membership binding alone.
|
|
69
|
+
*/
|
|
70
|
+
async function updateMemberRole(
|
|
71
|
+
tenantId: TenantId,
|
|
72
|
+
teamId: TeamId,
|
|
73
|
+
userId: UserId,
|
|
74
|
+
role: TeamRole,
|
|
75
|
+
): Promise<
|
|
76
|
+
| { readonly kind: 'done'; readonly outcome: TeamMembershipUpdateRoleOutcome }
|
|
77
|
+
| { readonly kind: 'refused'; readonly error: ReturnType<typeof membershipNotKeptInStepError> }
|
|
78
|
+
> {
|
|
79
|
+
if (authorizer === undefined) {
|
|
80
|
+
return {
|
|
81
|
+
kind: 'done',
|
|
82
|
+
outcome: await membershipBinding.updateRole(tenantId, teamId, userId, role),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (tenantHierarchy.updateTeamMemberRole === undefined) {
|
|
86
|
+
return { kind: 'refused', error: membershipNotKeptInStepError('updateTeamMemberRole') };
|
|
87
|
+
}
|
|
88
|
+
const res = await tenantHierarchy.updateTeamMemberRole({ tenantId, teamId, userId, role });
|
|
89
|
+
if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
|
|
90
|
+
return { kind: 'done', outcome: res.value };
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
// ---------- Authorization middleware ----------
|
|
61
94
|
// Full PEP surface:
|
|
62
95
|
// POST / → admin on tenant
|
|
@@ -363,7 +396,19 @@ export function teamsRouter(
|
|
|
363
396
|
const tenantId = c.get('tenantId') as TenantId;
|
|
364
397
|
const teamId = c.req.param('teamId') as TeamId;
|
|
365
398
|
const userId = c.req.param('userId') as UserId;
|
|
366
|
-
|
|
399
|
+
// With an authorizer, the row and its FGA tuple go together, or not
|
|
400
|
+
// at all: removing the row alone would leave the permission in place.
|
|
401
|
+
if (authorizer !== undefined) {
|
|
402
|
+
if (tenantHierarchy.removeTeamMember === undefined) {
|
|
403
|
+
const refusal = membershipNotKeptInStepError('removeTeamMember');
|
|
404
|
+
c.status(statusFor(refusal.code) as never);
|
|
405
|
+
return c.json(toWireError(refusal, c.get('requestId')));
|
|
406
|
+
}
|
|
407
|
+
const res = await tenantHierarchy.removeTeamMember({ tenantId, teamId, userId });
|
|
408
|
+
if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
|
|
409
|
+
} else {
|
|
410
|
+
await membershipBinding.remove(tenantId, teamId, userId);
|
|
411
|
+
}
|
|
367
412
|
c.status(204);
|
|
368
413
|
return c.body(null);
|
|
369
414
|
});
|
|
@@ -403,7 +448,12 @@ export function teamsRouter(
|
|
|
403
448
|
),
|
|
404
449
|
);
|
|
405
450
|
}
|
|
406
|
-
const
|
|
451
|
+
const updated = await updateMemberRole(tenantId, teamId, userId, b.role);
|
|
452
|
+
if (updated.kind === 'refused') {
|
|
453
|
+
c.status(statusFor(updated.error.code) as never);
|
|
454
|
+
return c.json(toWireError(updated.error, requestId));
|
|
455
|
+
}
|
|
456
|
+
const { outcome } = updated;
|
|
407
457
|
if (outcome.kind === 'team-membership-not-found') {
|
|
408
458
|
c.status(statusFor('team-membership-not-found') as never);
|
|
409
459
|
return c.json(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { MiddlewareHandler } from 'hono';
|
|
5
|
+
|
|
6
|
+
import { statusFor, toWireError } from '../errors.js';
|
|
7
|
+
import type { AppEnv } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/** A UUID, the shape of a run's and a conversation's id. */
|
|
10
|
+
export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A path parameter that must be a UUID: one that isn't is a 400 naming
|
|
14
|
+
* it, before it reaches a query (where Postgres's uuid cast would fail it
|
|
15
|
+
* as a 500). `what` names the id, e.g. `a run id`.
|
|
16
|
+
*/
|
|
17
|
+
export function refuseMalformedUuidParam(param: string, what: string): MiddlewareHandler<AppEnv> {
|
|
18
|
+
return async (c, next) => {
|
|
19
|
+
if (UUID_RE.test(c.req.param(param) ?? '')) return next();
|
|
20
|
+
c.status(statusFor('bad-input') as never);
|
|
21
|
+
return c.json(
|
|
22
|
+
toWireError(
|
|
23
|
+
{ code: 'bad-input', message: `\`${param}\` must be ${what} (a UUID)` },
|
|
24
|
+
c.get('requestId'),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
}
|