@kindgi/api 0.1.3 → 0.1.4-rc.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/agent-binding.d.ts +26 -4
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts +48 -0
- package/dist/agent-pins.d.ts.map +1 -0
- package/dist/agent-pins.js +102 -0
- package/dist/agent-pins.js.map +1 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +27 -3
- package/dist/app.js.map +1 -1
- package/dist/block-binding.d.ts +132 -0
- package/dist/block-binding.d.ts.map +1 -0
- package/dist/block-binding.js +4 -0
- package/dist/block-binding.js.map +1 -0
- package/dist/block-pins.d.ts +22 -0
- package/dist/block-pins.d.ts.map +1 -0
- package/dist/block-pins.js +112 -0
- package/dist/block-pins.js.map +1 -0
- package/dist/cost-binding.d.ts +20 -1
- package/dist/cost-binding.d.ts.map +1 -1
- package/dist/cost-binding.js +4 -0
- package/dist/cost-binding.js.map +1 -1
- package/dist/deploy-versions.d.ts +60 -0
- package/dist/deploy-versions.d.ts.map +1 -0
- package/dist/deploy-versions.js +91 -0
- package/dist/deploy-versions.js.map +1 -0
- package/dist/deployment-binding.d.ts +24 -3
- package/dist/deployment-binding.d.ts.map +1 -1
- package/dist/derive-agent-version.d.ts +77 -0
- package/dist/derive-agent-version.d.ts.map +1 -0
- package/dist/derive-agent-version.js +149 -0
- package/dist/derive-agent-version.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +65 -0
- package/dist/eval-case-binding.d.ts.map +1 -0
- package/dist/eval-case-binding.js +4 -0
- package/dist/eval-case-binding.js.map +1 -0
- package/dist/eval-run-binding.d.ts +37 -0
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.js.map +1 -1
- package/dist/eval-run-dispatcher.d.ts +41 -3
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js +21 -15
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/eval-suite-binding.d.ts +1 -1
- package/dist/eval-suite-binding.d.ts.map +1 -1
- package/dist/eval-suite-binding.js +2 -0
- package/dist/eval-suite-binding.js.map +1 -1
- package/dist/flow-binding.d.ts +18 -4
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/flow-pins.d.ts +36 -0
- package/dist/flow-pins.d.ts.map +1 -0
- package/dist/flow-pins.js +81 -0
- package/dist/flow-pins.js.map +1 -0
- package/dist/guardrail-binding.d.ts +8 -0
- package/dist/guardrail-binding.d.ts.map +1 -1
- package/dist/handler-binding.d.ts +3 -0
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/index.d.ts +18 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +138 -0
- package/dist/judged-dispatcher.d.ts.map +1 -0
- package/dist/judged-dispatcher.js +308 -0
- package/dist/judged-dispatcher.js.map +1 -0
- package/dist/judged-items.d.ts +86 -0
- package/dist/judged-items.d.ts.map +1 -0
- package/dist/judged-items.js +184 -0
- package/dist/judged-items.js.map +1 -0
- package/dist/judgment-binding.d.ts +316 -0
- package/dist/judgment-binding.d.ts.map +1 -0
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -0
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -1
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +482 -13
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +46 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +1350 -175
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/provider-binding.d.ts +12 -7
- package/dist/provider-binding.d.ts.map +1 -1
- package/dist/registry-read-only.d.ts +32 -0
- package/dist/registry-read-only.d.ts.map +1 -0
- package/dist/registry-read-only.js +22 -0
- package/dist/registry-read-only.js.map +1 -0
- package/dist/routes/agents.d.ts +9 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +181 -11
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/blocks.d.ts +19 -0
- package/dist/routes/blocks.d.ts.map +1 -0
- package/dist/routes/blocks.js +306 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/cost.d.ts.map +1 -1
- package/dist/routes/cost.js +47 -2
- package/dist/routes/cost.js.map +1 -1
- package/dist/routes/deployments.d.ts +3 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +224 -55
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +15 -0
- package/dist/routes/eval-comparison.d.ts.map +1 -0
- package/dist/routes/eval-comparison.js +123 -0
- package/dist/routes/eval-comparison.js.map +1 -0
- package/dist/routes/eval-runs.d.ts +7 -1
- package/dist/routes/eval-runs.d.ts.map +1 -1
- package/dist/routes/eval-runs.js +42 -3
- package/dist/routes/eval-runs.js.map +1 -1
- package/dist/routes/eval-versions.d.ts +25 -0
- package/dist/routes/eval-versions.d.ts.map +1 -0
- package/dist/routes/eval-versions.js +66 -0
- package/dist/routes/eval-versions.js.map +1 -0
- package/dist/routes/flows.d.ts +13 -1
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +48 -3
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/guardrails.d.ts.map +1 -1
- package/dist/routes/guardrails.js +4 -0
- package/dist/routes/guardrails.js.map +1 -1
- package/dist/routes/hierarchy-errors.d.ts +45 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -0
- package/dist/routes/hierarchy-errors.js +47 -0
- package/dist/routes/hierarchy-errors.js.map +1 -0
- package/dist/routes/judged-suites.d.ts +20 -0
- package/dist/routes/judged-suites.d.ts.map +1 -0
- package/dist/routes/judged-suites.js +272 -0
- package/dist/routes/judged-suites.js.map +1 -0
- package/dist/routes/judgment-context.d.ts +22 -0
- package/dist/routes/judgment-context.d.ts.map +1 -0
- package/dist/routes/judgment-context.js +88 -0
- package/dist/routes/judgment-context.js.map +1 -0
- package/dist/routes/judgment-flow-context.d.ts +32 -0
- package/dist/routes/judgment-flow-context.d.ts.map +1 -0
- package/dist/routes/judgment-flow-context.js +195 -0
- package/dist/routes/judgment-flow-context.js.map +1 -0
- package/dist/routes/judgments.d.ts +41 -0
- package/dist/routes/judgments.d.ts.map +1 -0
- package/dist/routes/judgments.js +566 -0
- package/dist/routes/judgments.js.map +1 -0
- package/dist/routes/orgs.d.ts +5 -2
- package/dist/routes/orgs.d.ts.map +1 -1
- package/dist/routes/orgs.js +38 -22
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +12 -1
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts +10 -2
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +94 -80
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +6 -1
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/runs.js +25 -3
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/teams.d.ts +6 -2
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +83 -73
- package/dist/routes/teams.js.map +1 -1
- package/dist/routes/tools.d.ts.map +1 -1
- package/dist/routes/tools.js +4 -0
- package/dist/routes/tools.js.map +1 -1
- package/dist/tool-binding.d.ts +8 -0
- package/dist/tool-binding.d.ts.map +1 -1
- package/openapi.json +14253 -10099
- package/package.json +21 -21
- package/src/agent-binding.ts +28 -4
- package/src/agent-pins.ts +147 -0
- package/src/app.ts +81 -3
- package/src/block-binding.ts +137 -0
- package/src/block-pins.ts +148 -0
- package/src/cost-binding.ts +21 -1
- package/src/deploy-versions.ts +157 -0
- package/src/deployment-binding.ts +27 -3
- package/src/derive-agent-version.ts +217 -0
- package/src/errors.ts +18 -0
- package/src/eval-case-binding.ts +71 -0
- package/src/eval-run-binding.ts +40 -0
- package/src/eval-run-dispatcher.ts +60 -16
- package/src/eval-suite-binding.ts +2 -0
- package/src/flow-binding.ts +20 -4
- package/src/flow-pins.ts +113 -0
- package/src/guardrail-binding.ts +9 -0
- package/src/handler-binding.ts +3 -0
- package/src/index.ts +86 -2
- package/src/judged-dispatcher.ts +530 -0
- package/src/judged-items.ts +263 -0
- package/src/judgment-binding.ts +349 -0
- package/src/openapi/generate.ts +7 -1
- package/src/openapi/operations.ts +579 -13
- package/src/openapi/schemas.ts +1408 -128
- package/src/provider-binding.ts +12 -7
- package/src/registry-read-only.ts +43 -0
- package/src/routes/agents.ts +252 -19
- package/src/routes/blocks.ts +387 -0
- package/src/routes/cost.ts +55 -1
- package/src/routes/deployments.ts +291 -56
- package/src/routes/eval-comparison.ts +135 -0
- package/src/routes/eval-runs.ts +57 -3
- package/src/routes/eval-versions.ts +110 -0
- package/src/routes/flows.ts +70 -5
- package/src/routes/guardrails.ts +7 -0
- package/src/routes/hierarchy-errors.ts +60 -0
- package/src/routes/judged-suites.ts +363 -0
- package/src/routes/judgment-context.ts +128 -0
- package/src/routes/judgment-flow-context.ts +245 -0
- package/src/routes/judgments.ts +743 -0
- package/src/routes/orgs.ts +44 -27
- package/src/routes/policies.ts +19 -0
- package/src/routes/projects.ts +118 -96
- package/src/routes/providers.ts +5 -0
- package/src/routes/runs.ts +29 -3
- package/src/routes/teams.ts +104 -90
- package/src/routes/tools.ts +7 -0
- package/src/tool-binding.ts +9 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { type Context, Hono } from 'hono';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
BLOCK_KINDS,
|
|
8
|
+
type BlockDefinition,
|
|
9
|
+
type BlockKind,
|
|
10
|
+
settingsSchemaIssues,
|
|
11
|
+
validateBlock,
|
|
12
|
+
} from '@kindgi/agents';
|
|
13
|
+
import { ref } from '@kindgi/authz';
|
|
14
|
+
import type { Cursor, ProjectId, TenantId } from '@kindgi/types';
|
|
15
|
+
|
|
16
|
+
import type { BlockPublishOutcome, BlockRecord, BlockRegistryBinding } from '../block-binding.js';
|
|
17
|
+
import { statusFor, toWireError } from '../errors.js';
|
|
18
|
+
import type { Authorizer } from '../middleware/authorize.js';
|
|
19
|
+
import type { AppEnv } from '../types.js';
|
|
20
|
+
import { clampLimit } from './pagination.js';
|
|
21
|
+
import { parseScopeParams } from './scope-params.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Data blocks: versioned prompts and settings that agent versions pin
|
|
25
|
+
* (`/v1/blocks`).
|
|
26
|
+
*
|
|
27
|
+
* A block belongs to one project and is authorized through it: reading
|
|
28
|
+
* is `read` on the project, publishing, unregistering and reinstating
|
|
29
|
+
* are `write` on it. A block the caller can't read answers 404, as one
|
|
30
|
+
* that doesn't exist.
|
|
31
|
+
*
|
|
32
|
+
* A version never changes, nor does a block's kind. A settings block's
|
|
33
|
+
* values must satisfy the latest version's schema as well as their
|
|
34
|
+
* own, so whatever reads them sees a stable shape.
|
|
35
|
+
*/
|
|
36
|
+
export function blocksRouter(binding: BlockRegistryBinding, authorizer?: Authorizer): Hono<AppEnv> {
|
|
37
|
+
const r = new Hono<AppEnv>();
|
|
38
|
+
|
|
39
|
+
async function canProject(
|
|
40
|
+
c: Context<AppEnv>,
|
|
41
|
+
action: 'read' | 'write',
|
|
42
|
+
projectId: ProjectId,
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
return authorizer === undefined || authorizer.can(c, action, ref('project', projectId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function notFound(c: Context<AppEnv>, blockId: string, version?: string) {
|
|
48
|
+
c.status(statusFor('block-not-found') as never);
|
|
49
|
+
return c.json(
|
|
50
|
+
toWireError(
|
|
51
|
+
{
|
|
52
|
+
code: 'block-not-found',
|
|
53
|
+
message: `No block "${blockId}"${version !== undefined ? ` at version "${version}"` : ''}`,
|
|
54
|
+
blockId,
|
|
55
|
+
...(version !== undefined && { version }),
|
|
56
|
+
},
|
|
57
|
+
c.get('requestId'),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function badInput(c: Context<AppEnv>, message: string) {
|
|
63
|
+
c.status(statusFor('bad-input') as never);
|
|
64
|
+
return c.json(toWireError({ code: 'bad-input', message }, c.get('requestId')));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- GET / (each block's latest version) ----------
|
|
68
|
+
r.get('/', async (c) => {
|
|
69
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
70
|
+
const kind = c.req.query('kind');
|
|
71
|
+
if (kind !== undefined && kind.length > 0 && !isBlockKind(kind)) {
|
|
72
|
+
return badInput(
|
|
73
|
+
c,
|
|
74
|
+
`Unknown block kind "${kind}". Expected one of: ${BLOCK_KINDS.join(', ')}.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const scope = parseScopeParams(c.req.query(), { tenantId });
|
|
78
|
+
if (scope.kind === 'err') {
|
|
79
|
+
c.status(statusFor('scope-invalid') as never);
|
|
80
|
+
return c.json(
|
|
81
|
+
toWireError({ code: 'scope-invalid', message: scope.message }, c.get('requestId')),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const cursor = c.req.query('cursor');
|
|
85
|
+
const name = c.req.query('name');
|
|
86
|
+
const page = await binding.list({
|
|
87
|
+
tenantId,
|
|
88
|
+
limit: clampLimit(c.req.query('limit')),
|
|
89
|
+
...(cursor !== undefined && cursor.length > 0 && { cursor: cursor as Cursor }),
|
|
90
|
+
...(kind !== undefined && kind.length > 0 && { blockKind: kind as BlockKind }),
|
|
91
|
+
...(name !== undefined && name.length > 0 && { nameFilter: name }),
|
|
92
|
+
...(scope.scope !== undefined && { scope: scope.scope }),
|
|
93
|
+
});
|
|
94
|
+
const visible =
|
|
95
|
+
authorizer === undefined
|
|
96
|
+
? page.data
|
|
97
|
+
: await authorizer.filterByCan(c, 'read', page.data, (b) => ref('project', b.projectId));
|
|
98
|
+
return c.json({
|
|
99
|
+
data: visible.map(serializeBlock),
|
|
100
|
+
hasMore: page.nextCursor !== undefined,
|
|
101
|
+
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------- GET /:blockId (latest version) ----------
|
|
106
|
+
r.get('/:blockId', async (c) => {
|
|
107
|
+
const blockId = c.req.param('blockId');
|
|
108
|
+
const block = await binding.get({ tenantId: c.get('tenantId') as TenantId, blockId });
|
|
109
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
110
|
+
return notFound(c, blockId);
|
|
111
|
+
}
|
|
112
|
+
return c.json(serializeBlock(block));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---------- GET /:blockId/versions ----------
|
|
116
|
+
r.get('/:blockId/versions', async (c) => {
|
|
117
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
118
|
+
const blockId = c.req.param('blockId');
|
|
119
|
+
const cursor = c.req.query('cursor');
|
|
120
|
+
const page = await binding.listVersions({
|
|
121
|
+
tenantId,
|
|
122
|
+
blockId,
|
|
123
|
+
limit: clampLimit(c.req.query('limit')),
|
|
124
|
+
includeTombstoned: c.req.query('includeTombstoned') === 'true',
|
|
125
|
+
...(cursor !== undefined && cursor.length > 0 && { cursor: cursor as Cursor }),
|
|
126
|
+
});
|
|
127
|
+
const owner = page.data[0] ?? (await anyVersion(tenantId, blockId));
|
|
128
|
+
if (owner === null || !(await canProject(c, 'read', owner.projectId))) {
|
|
129
|
+
return notFound(c, blockId);
|
|
130
|
+
}
|
|
131
|
+
return c.json({
|
|
132
|
+
data: page.data.map(serializeBlock),
|
|
133
|
+
hasMore: page.nextCursor !== undefined,
|
|
134
|
+
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------- GET /:blockId/versions/:version ----------
|
|
139
|
+
r.get('/:blockId/versions/:version', async (c) => {
|
|
140
|
+
const blockId = c.req.param('blockId');
|
|
141
|
+
const version = c.req.param('version');
|
|
142
|
+
const block = await binding.getVersion({
|
|
143
|
+
tenantId: c.get('tenantId') as TenantId,
|
|
144
|
+
blockId,
|
|
145
|
+
version,
|
|
146
|
+
});
|
|
147
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
148
|
+
return notFound(c, blockId, version);
|
|
149
|
+
}
|
|
150
|
+
return c.json(serializeBlock(block));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------- POST / (publish a version) ----------
|
|
154
|
+
r.post('/', async (c) => {
|
|
155
|
+
const requestId = c.get('requestId');
|
|
156
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
157
|
+
let body: unknown;
|
|
158
|
+
try {
|
|
159
|
+
body = await c.req.json();
|
|
160
|
+
} catch {
|
|
161
|
+
return badInput(c, 'Request body must be valid JSON');
|
|
162
|
+
}
|
|
163
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
164
|
+
return badInput(c, 'Request body must be an object');
|
|
165
|
+
}
|
|
166
|
+
const { projectId, ...definition } = body as Record<string, unknown>;
|
|
167
|
+
if (typeof projectId !== 'string' || projectId.length === 0) {
|
|
168
|
+
return badInput(c, '`projectId` is required');
|
|
169
|
+
}
|
|
170
|
+
if (!(await canProject(c, 'write', projectId as ProjectId))) {
|
|
171
|
+
c.status(statusFor('permission-denied') as never);
|
|
172
|
+
return c.json(
|
|
173
|
+
toWireError(
|
|
174
|
+
{
|
|
175
|
+
code: 'permission-denied',
|
|
176
|
+
message: `Publishing a block needs write on project "${projectId}"`,
|
|
177
|
+
},
|
|
178
|
+
requestId,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const validated = validateBlock(definition);
|
|
183
|
+
if (validated.kind === 'err')
|
|
184
|
+
return validationFailed(c, validated.error.message, validated.error.issues);
|
|
185
|
+
const block = validated.value;
|
|
186
|
+
|
|
187
|
+
// A block keeps its kind. A settings version without a schema keeps
|
|
188
|
+
// the latest version's (stored on it, so the check carries forward to
|
|
189
|
+
// every later version); one that gives a schema replaces it.
|
|
190
|
+
const latest = await binding.get({ tenantId, blockId: block.id });
|
|
191
|
+
const continuity = continuityIssues(latest, block);
|
|
192
|
+
if (continuity.length > 0) {
|
|
193
|
+
return validationFailed(c, `Block "${block.id}" can't take this version`, continuity);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const outcome = await binding.publish({
|
|
197
|
+
tenantId,
|
|
198
|
+
projectId: projectId as ProjectId,
|
|
199
|
+
block: withCarriedSchema(latest, block),
|
|
200
|
+
});
|
|
201
|
+
return published(c, outcome);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------- POST /:blockId/versions/:version/unregister ----------
|
|
205
|
+
r.post('/:blockId/versions/:version/unregister', async (c) => {
|
|
206
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
207
|
+
const blockId = c.req.param('blockId');
|
|
208
|
+
const version = c.req.param('version');
|
|
209
|
+
const block = await binding.getVersion({ tenantId, blockId, version });
|
|
210
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
211
|
+
return notFound(c, blockId, version);
|
|
212
|
+
}
|
|
213
|
+
if (!(await canProject(c, 'write', block.projectId))) return denied(c, block.projectId);
|
|
214
|
+
const outcome = await binding.unregister({ tenantId, blockId, version });
|
|
215
|
+
if (!outcome.unregistered) return notFound(c, blockId, version);
|
|
216
|
+
return c.json({ blockId, version, unregistered: true });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ---------- POST /:blockId/versions/:version/reinstate ----------
|
|
220
|
+
r.post('/:blockId/versions/:version/reinstate', async (c) => {
|
|
221
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
222
|
+
const blockId = c.req.param('blockId');
|
|
223
|
+
const version = c.req.param('version');
|
|
224
|
+
const block = await binding.getVersion({ tenantId, blockId, version });
|
|
225
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
226
|
+
return notFound(c, blockId, version);
|
|
227
|
+
}
|
|
228
|
+
if (!(await canProject(c, 'write', block.projectId))) return denied(c, block.projectId);
|
|
229
|
+
const outcome = await binding.reinstateVersion({ tenantId, blockId, version });
|
|
230
|
+
if (outcome.kind === 'not-found') return notFound(c, blockId, version);
|
|
231
|
+
return c.json({ blockId, version, wasTombstoned: outcome.wasTombstoned });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/** Any version of a block, unregistered ones included: whose project it is. */
|
|
235
|
+
async function anyVersion(tenantId: TenantId, blockId: string): Promise<BlockRecord | null> {
|
|
236
|
+
const page = await binding.listVersions({
|
|
237
|
+
tenantId,
|
|
238
|
+
blockId,
|
|
239
|
+
limit: 1,
|
|
240
|
+
includeTombstoned: true,
|
|
241
|
+
});
|
|
242
|
+
return page.data[0] ?? null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return r;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isBlockKind(value: string): value is BlockKind {
|
|
249
|
+
return (BLOCK_KINDS as readonly string[]).includes(value);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A new version against the block's latest: the same kind, and a
|
|
254
|
+
* settings version without a schema of its own satisfies the schema it
|
|
255
|
+
* keeps (`carriedSchema`).
|
|
256
|
+
*/
|
|
257
|
+
function continuityIssues(
|
|
258
|
+
latest: BlockRecord | null,
|
|
259
|
+
block: BlockDefinition,
|
|
260
|
+
): { path: string; message: string }[] {
|
|
261
|
+
if (latest === null) return [];
|
|
262
|
+
if (latest.kind !== block.kind) {
|
|
263
|
+
return [
|
|
264
|
+
{
|
|
265
|
+
path: '/kind',
|
|
266
|
+
message: `block "${block.id}" is a ${latest.kind} block; a version can't change its kind`,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
const carried = carriedSchema(latest, block);
|
|
271
|
+
if (block.kind !== 'settings' || carried === undefined) return [];
|
|
272
|
+
return settingsSchemaIssues(block.content.values, carried).map((i) => ({
|
|
273
|
+
...i,
|
|
274
|
+
message: `${i.message} (the schema of version ${latest.version})`,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The schema a settings version that gives none keeps: the latest
|
|
280
|
+
* version's. A version that gives a schema replaces it (`{}` drops the
|
|
281
|
+
* check on purpose).
|
|
282
|
+
*/
|
|
283
|
+
function carriedSchema(
|
|
284
|
+
latest: BlockRecord | null,
|
|
285
|
+
block: BlockDefinition,
|
|
286
|
+
): Readonly<Record<string, unknown>> | undefined {
|
|
287
|
+
if (latest?.kind !== 'settings' || block.kind !== 'settings') return undefined;
|
|
288
|
+
return block.content.schema === undefined ? latest.content.schema : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The version as stored: with the schema it keeps, if any. */
|
|
292
|
+
function withCarriedSchema(latest: BlockRecord | null, block: BlockDefinition): BlockDefinition {
|
|
293
|
+
const schema = carriedSchema(latest, block);
|
|
294
|
+
if (schema === undefined || block.kind !== 'settings') return block;
|
|
295
|
+
return { ...block, content: { ...block.content, schema } };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The response to a publish outcome. */
|
|
299
|
+
function published(c: Context<AppEnv>, outcome: BlockPublishOutcome) {
|
|
300
|
+
const requestId = c.get('requestId');
|
|
301
|
+
switch (outcome.kind) {
|
|
302
|
+
case 'ok':
|
|
303
|
+
c.status(201);
|
|
304
|
+
return c.json({ blockId: outcome.blockId, version: outcome.version });
|
|
305
|
+
case 'already-registered':
|
|
306
|
+
c.status(statusFor('block-already-registered') as never);
|
|
307
|
+
return c.json(
|
|
308
|
+
toWireError(
|
|
309
|
+
{
|
|
310
|
+
code: 'block-already-registered',
|
|
311
|
+
message: `Block "${outcome.blockId}" version "${outcome.version}" is already published; versions never change, so publish a new one`,
|
|
312
|
+
blockId: outcome.blockId,
|
|
313
|
+
version: outcome.version,
|
|
314
|
+
},
|
|
315
|
+
requestId,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
case 'project-mismatch':
|
|
319
|
+
c.status(statusFor('block-project-mismatch') as never);
|
|
320
|
+
return c.json(
|
|
321
|
+
toWireError(
|
|
322
|
+
{
|
|
323
|
+
code: 'block-project-mismatch',
|
|
324
|
+
message: `Block "${outcome.blockId}" belongs to project "${outcome.projectId as unknown as string}"; publish its versions there`,
|
|
325
|
+
blockId: outcome.blockId,
|
|
326
|
+
projectId: outcome.projectId as unknown as string,
|
|
327
|
+
},
|
|
328
|
+
requestId,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
case 'project-not-found':
|
|
332
|
+
c.status(statusFor('bad-input') as never);
|
|
333
|
+
return c.json(
|
|
334
|
+
toWireError(
|
|
335
|
+
{
|
|
336
|
+
code: 'bad-input',
|
|
337
|
+
message: `\`projectId\` "${outcome.projectId as unknown as string}" does not resolve to a project in this tenant`,
|
|
338
|
+
},
|
|
339
|
+
requestId,
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function validationFailed(
|
|
346
|
+
c: Context<AppEnv>,
|
|
347
|
+
message: string,
|
|
348
|
+
issues: readonly { readonly path: string; readonly message: string }[],
|
|
349
|
+
) {
|
|
350
|
+
c.status(statusFor('validation-failed') as never);
|
|
351
|
+
return c.json(
|
|
352
|
+
toWireError(
|
|
353
|
+
{
|
|
354
|
+
code: 'validation-failed',
|
|
355
|
+
message,
|
|
356
|
+
issues: issues as unknown as Record<string, unknown>[],
|
|
357
|
+
},
|
|
358
|
+
c.get('requestId'),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function denied(c: Context<AppEnv>, projectId: ProjectId) {
|
|
364
|
+
c.status(statusFor('permission-denied') as never);
|
|
365
|
+
return c.json(
|
|
366
|
+
toWireError(
|
|
367
|
+
{
|
|
368
|
+
code: 'permission-denied',
|
|
369
|
+
message: `This needs write on the block's project "${projectId as unknown as string}"`,
|
|
370
|
+
},
|
|
371
|
+
c.get('requestId'),
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function serializeBlock(b: BlockRecord): Record<string, unknown> {
|
|
377
|
+
return {
|
|
378
|
+
id: b.id,
|
|
379
|
+
version: b.version,
|
|
380
|
+
kind: b.kind,
|
|
381
|
+
...(b.description !== undefined && { description: b.description }),
|
|
382
|
+
content: b.content,
|
|
383
|
+
projectId: b.projectId as unknown as string,
|
|
384
|
+
publishedAt: b.publishedAt,
|
|
385
|
+
...(b.unregisteredAt !== undefined && { unregisteredAt: b.unregisteredAt }),
|
|
386
|
+
};
|
|
387
|
+
}
|
package/src/routes/cost.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { Hono } from 'hono';
|
|
|
6
6
|
import type { Cursor, TenantId, Timestamp } from '@kindgi/types';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
COST_AGGREGATE_DEFAULT_LIMIT,
|
|
10
|
+
COST_AGGREGATE_MAX_LIMIT,
|
|
9
11
|
COST_GROUP_DIMENSIONS,
|
|
10
12
|
type CostAggregateGroup,
|
|
11
13
|
type CostBinding,
|
|
@@ -169,6 +171,21 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
|
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
// limit — the most groups to return, the most expensive first.
|
|
175
|
+
const limit = parseAggregateLimit(query.limit);
|
|
176
|
+
if (limit === null) {
|
|
177
|
+
c.status(statusFor('bad-input') as never);
|
|
178
|
+
return c.json(
|
|
179
|
+
toWireError(
|
|
180
|
+
{
|
|
181
|
+
code: 'bad-input',
|
|
182
|
+
message: `\`limit\` must be an integer from 1 to ${COST_AGGREGATE_MAX_LIMIT} (default ${COST_AGGREGATE_DEFAULT_LIMIT})`,
|
|
183
|
+
},
|
|
184
|
+
requestId,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
// Time range — required. Default to last 30 days when both absent
|
|
173
190
|
// (documented in the response `timeRange`). If only one endpoint is
|
|
174
191
|
// supplied, reject — half-open defaults invite confusion.
|
|
@@ -240,10 +257,18 @@ export function costRouter(binding: CostBinding): Hono<AppEnv> {
|
|
|
240
257
|
...(Object.keys(filter.value).length > 0 && { filter: filter.value }),
|
|
241
258
|
...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
|
|
242
259
|
...(scopeParsed.inherit !== undefined && { inherit: scopeParsed.inherit }),
|
|
260
|
+
limit,
|
|
243
261
|
});
|
|
244
262
|
|
|
263
|
+
// The most expensive groups, capped here too: a binding may return
|
|
264
|
+
// every group. The window's totals stay over every record.
|
|
265
|
+
const groups = [...result.groups].sort((a, b) => compareGroups(a, b, groupBy)).slice(0, limit);
|
|
266
|
+
const totalGroups = Math.max(result.totalGroups ?? 0, result.groups.length);
|
|
267
|
+
|
|
245
268
|
return c.json({
|
|
246
|
-
groups:
|
|
269
|
+
groups: groups.map(serializeGroup),
|
|
270
|
+
totalGroups,
|
|
271
|
+
truncated: totalGroups > groups.length,
|
|
247
272
|
totalUsd: result.totalUsd,
|
|
248
273
|
totalRecords: result.totalRecords,
|
|
249
274
|
tokens: serializeTokens(result.tokens),
|
|
@@ -388,6 +413,35 @@ function pick(rec: CostRecord, keys: readonly (keyof CostRecord)[]): Record<stri
|
|
|
388
413
|
return out;
|
|
389
414
|
}
|
|
390
415
|
|
|
416
|
+
/** `?limit=` on the aggregate: the default when absent, `null` when it isn't 1..max. */
|
|
417
|
+
function parseAggregateLimit(raw: string | undefined): number | null {
|
|
418
|
+
if (raw === undefined) return COST_AGGREGATE_DEFAULT_LIMIT;
|
|
419
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
420
|
+
const n = Number(raw);
|
|
421
|
+
return n >= 1 && n <= COST_AGGREGATE_MAX_LIMIT ? n : null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The aggregate's order: `totalUsd` descending, then the key, dimension
|
|
426
|
+
* by dimension in `groupBy` order (code-point order; `null` last).
|
|
427
|
+
*/
|
|
428
|
+
function compareGroups(
|
|
429
|
+
a: CostAggregateGroup,
|
|
430
|
+
b: CostAggregateGroup,
|
|
431
|
+
groupBy: readonly CostGroupDimension[],
|
|
432
|
+
): number {
|
|
433
|
+
if (a.totalUsd !== b.totalUsd) return b.totalUsd - a.totalUsd;
|
|
434
|
+
for (const dim of groupBy) {
|
|
435
|
+
const x = a.key[dim] ?? null;
|
|
436
|
+
const y = b.key[dim] ?? null;
|
|
437
|
+
if (x === y) continue;
|
|
438
|
+
if (x === null) return 1;
|
|
439
|
+
if (y === null) return -1;
|
|
440
|
+
return x < y ? -1 : 1;
|
|
441
|
+
}
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
391
445
|
function serializeGroup(g: CostAggregateGroup): Record<string, unknown> {
|
|
392
446
|
return {
|
|
393
447
|
key: g.key,
|