@kindgi/api 0.1.5-rc.0 → 0.1.5

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.
Files changed (55) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/app.js.map +1 -1
  3. package/dist/cost-binding.d.ts +19 -0
  4. package/dist/cost-binding.d.ts.map +1 -1
  5. package/dist/cost-binding.js.map +1 -1
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/errors.js +2 -0
  8. package/dist/errors.js.map +1 -1
  9. package/dist/middleware/authorize.d.ts +5 -2
  10. package/dist/middleware/authorize.d.ts.map +1 -1
  11. package/dist/middleware/authorize.js +10 -1
  12. package/dist/middleware/authorize.js.map +1 -1
  13. package/dist/openapi/operations.js +1 -1
  14. package/dist/openapi/operations.js.map +1 -1
  15. package/dist/openapi/schemas.js +1 -1
  16. package/dist/openapi/schemas.js.map +1 -1
  17. package/dist/publish-refused.d.ts +12 -2
  18. package/dist/publish-refused.d.ts.map +1 -1
  19. package/dist/publish-refused.js +6 -0
  20. package/dist/publish-refused.js.map +1 -1
  21. package/dist/routes/cost.d.ts +8 -3
  22. package/dist/routes/cost.d.ts.map +1 -1
  23. package/dist/routes/cost.js +56 -4
  24. package/dist/routes/cost.js.map +1 -1
  25. package/dist/routes/deployments.d.ts +8 -0
  26. package/dist/routes/deployments.d.ts.map +1 -1
  27. package/dist/routes/deployments.js +127 -1
  28. package/dist/routes/deployments.js.map +1 -1
  29. package/dist/routes/eval-runs.js +4 -1
  30. package/dist/routes/eval-runs.js.map +1 -1
  31. package/dist/routes/eval-suites.d.ts.map +1 -1
  32. package/dist/routes/eval-suites.js +8 -1
  33. package/dist/routes/eval-suites.js.map +1 -1
  34. package/dist/routes/memory-access.d.ts.map +1 -1
  35. package/dist/routes/memory-access.js +18 -33
  36. package/dist/routes/memory-access.js.map +1 -1
  37. package/dist/routes/readable-projects.d.ts +26 -0
  38. package/dist/routes/readable-projects.d.ts.map +1 -0
  39. package/dist/routes/readable-projects.js +39 -0
  40. package/dist/routes/readable-projects.js.map +1 -0
  41. package/openapi.json +3 -2
  42. package/package.json +21 -21
  43. package/src/app.ts +1 -1
  44. package/src/cost-binding.ts +19 -0
  45. package/src/errors.ts +2 -0
  46. package/src/middleware/authorize.ts +19 -3
  47. package/src/openapi/operations.ts +1 -1
  48. package/src/openapi/schemas.ts +1 -1
  49. package/src/publish-refused.ts +20 -2
  50. package/src/routes/cost.ts +81 -4
  51. package/src/routes/deployments.ts +148 -1
  52. package/src/routes/eval-runs.ts +3 -0
  53. package/src/routes/eval-suites.ts +7 -1
  54. package/src/routes/memory-access.ts +19 -38
  55. package/src/routes/readable-projects.ts +66 -0
@@ -20,10 +20,11 @@ import type { Context } from 'hono';
20
20
  import { type Principal, ref } from '@kindgi/authz';
21
21
  import type { MemoryReaders, MemoryScope } from '@kindgi/memory';
22
22
  import type { ProjectBinding } from '@kindgi/platform';
23
- import type { OrgId, ProjectId, TenantId, UserId } from '@kindgi/types';
23
+ import type { OrgId, ProjectId, UserId } from '@kindgi/types';
24
24
 
25
25
  import type { Authorizer } from '../middleware/authorize.js';
26
26
  import type { AppEnv } from '../types.js';
27
+ import { type ProjectRow, allProjects } from './readable-projects.js';
27
28
 
28
29
  export interface MemoryAccessDeps {
29
30
  readonly authorizer?: Authorizer;
@@ -31,8 +32,6 @@ export interface MemoryAccessDeps {
31
32
  readonly projects?: Pick<ProjectBinding, 'list'>;
32
33
  }
33
34
 
34
- const PROJECT_PAGE = 200;
35
-
36
35
  /** The caller's user id, when the caller is a Kindgi user. */
37
36
  function callerUserId(c: Context<AppEnv>): UserId | undefined {
38
37
  const actor = (c.get('principal') as Principal | undefined)?.actor;
@@ -60,51 +59,33 @@ async function isTenantAdmin(c: Context<AppEnv>, authorizer: Authorizer): Promis
60
59
  return authorizer.can(c, 'admin', ref('tenant', c.get('tenantId') as unknown as string));
61
60
  }
62
61
 
63
- type ProjectRow = { readonly id: ProjectId; readonly orgId?: OrgId };
64
-
65
- /** The tenant's projects, every page. */
66
- async function allProjects(c: Context<AppEnv>, deps: MemoryAccessDeps): Promise<ProjectRow[]> {
67
- if (deps.projects === undefined) return [];
68
- const tenantId = c.get('tenantId') as TenantId;
69
- const all: ProjectRow[] = [];
70
- let cursor: Parameters<ProjectBinding['list']>[1]['cursor'];
71
- for (;;) {
72
- const page = await deps.projects.list(tenantId, {
73
- limit: PROJECT_PAGE,
74
- ...(cursor !== undefined && { cursor }),
75
- });
76
- all.push(
77
- ...page.items.map((p) => ({ id: p.id, ...(p.orgId !== undefined && { orgId: p.orgId }) })),
78
- );
79
- if (page.nextCursor === undefined) break;
80
- cursor = page.nextCursor;
81
- }
82
- return all;
83
- }
84
-
85
62
  /**
86
63
  * The projects the caller may `action`, and the orgs whose org-wide facts
87
64
  * it reads: listed by the authorizer (`read` on the org), else the
88
- * projects checked one by one and the orgs of those it may read.
65
+ * projects checked one by one and the orgs of those it may read. A key
66
+ * limited to a project is always checked: the listing gives every org its
67
+ * user may read, and the key reaches only its own project's.
89
68
  */
90
69
  async function readerContainers(
91
70
  c: Context<AppEnv>,
92
71
  deps: MemoryAccessDeps & { readonly authorizer: Authorizer },
93
72
  ): Promise<{ readable: ProjectId[]; writable: ProjectId[]; orgs: OrgId[] }> {
94
73
  const { authorizer } = deps;
95
- const [readable, writable, orgs] = await Promise.all([
96
- authorizer.listObjects?.(c, 'read', 'project'),
97
- authorizer.listObjects?.(c, 'write', 'project'),
98
- authorizer.listObjects?.(c, 'read', 'org'),
99
- ]);
100
- if (readable !== undefined && writable !== undefined && orgs !== undefined) {
101
- return {
102
- readable: readable.map((id) => id as ProjectId),
103
- writable: writable.map((id) => id as ProjectId),
104
- orgs: orgs.map((id) => id as OrgId),
105
- };
74
+ if (c.get('tokenProjectId') === undefined) {
75
+ const [readable, writable, orgs] = await Promise.all([
76
+ authorizer.listObjects?.(c, 'read', 'project'),
77
+ authorizer.listObjects?.(c, 'write', 'project'),
78
+ authorizer.listObjects?.(c, 'read', 'org'),
79
+ ]);
80
+ if (readable !== undefined && writable !== undefined && orgs !== undefined) {
81
+ return {
82
+ readable: readable.map((id) => id as ProjectId),
83
+ writable: writable.map((id) => id as ProjectId),
84
+ orgs: orgs.map((id) => id as OrgId),
85
+ };
86
+ }
106
87
  }
107
- const all = await allProjects(c, deps);
88
+ const all = await allProjects(c, deps.projects);
108
89
  const projectRef = (p: ProjectRow) => ref('project', p.id as unknown as string);
109
90
  const [canRead, canWrite] = await Promise.all([
110
91
  authorizer.filterByCan(c, 'read', all, projectRef),
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * The projects a caller may act on, for a route that hands them to its
6
+ * binding to apply inside the query (memory reads, the cost aggregate)
7
+ * rather than filtering a page after it: the authorizer's list when it can
8
+ * list (`listObjects`), else the tenant's projects checked one by one.
9
+ */
10
+
11
+ import type { Context } from 'hono';
12
+
13
+ import { type Action, ref } from '@kindgi/authz';
14
+ import type { ProjectBinding } from '@kindgi/platform';
15
+ import type { OrgId, ProjectId, TenantId } from '@kindgi/types';
16
+
17
+ import type { Authorizer } from '../middleware/authorize.js';
18
+ import type { AppEnv } from '../types.js';
19
+
20
+ const PROJECT_PAGE = 200;
21
+
22
+ export type ProjectRow = { readonly id: ProjectId; readonly orgId?: OrgId };
23
+
24
+ /** The tenant's projects, every page; none without a project binding. */
25
+ export async function allProjects(
26
+ c: Context<AppEnv>,
27
+ projects: Pick<ProjectBinding, 'list'> | undefined,
28
+ ): Promise<ProjectRow[]> {
29
+ if (projects === undefined) return [];
30
+ const tenantId = c.get('tenantId') as TenantId;
31
+ const all: ProjectRow[] = [];
32
+ let cursor: Parameters<ProjectBinding['list']>[1]['cursor'];
33
+ for (;;) {
34
+ const page = await projects.list(tenantId, {
35
+ limit: PROJECT_PAGE,
36
+ ...(cursor !== undefined && { cursor }),
37
+ });
38
+ all.push(
39
+ ...page.items.map((p) => ({ id: p.id, ...(p.orgId !== undefined && { orgId: p.orgId }) })),
40
+ );
41
+ if (page.nextCursor === undefined) break;
42
+ cursor = page.nextCursor;
43
+ }
44
+ return all;
45
+ }
46
+
47
+ /**
48
+ * The ids of the projects the caller may `action`: listed by the
49
+ * authorizer, else the tenant's projects checked one by one. `undefined`
50
+ * when neither can say (the authorizer can't list, and there's no project
51
+ * binding to check against).
52
+ */
53
+ export async function projectIdsCallerMay(
54
+ c: Context<AppEnv>,
55
+ authorizer: Authorizer,
56
+ projects: Pick<ProjectBinding, 'list'> | undefined,
57
+ action: Action,
58
+ ): Promise<ProjectId[] | undefined> {
59
+ const listed = await authorizer.listObjects?.(c, action, 'project');
60
+ if (listed !== undefined) return listed.map((id) => id as ProjectId);
61
+ if (projects === undefined) return undefined;
62
+ const may = await authorizer.filterByCan(c, action, await allProjects(c, projects), (p) =>
63
+ ref('project', p.id as unknown as string),
64
+ );
65
+ return may.map((p) => p.id);
66
+ }