@intx/hub-api 0.1.2
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/README.md +29 -0
- package/package.json +28 -0
- package/src/app.test.ts +225 -0
- package/src/app.ts +382 -0
- package/src/auth.ts +21 -0
- package/src/context.ts +38 -0
- package/src/format.ts +9 -0
- package/src/git-http/advertise-refs.test.ts +459 -0
- package/src/git-http/advertise-refs.ts +226 -0
- package/src/git-http/pkt-line.test.ts +220 -0
- package/src/git-http/pkt-line.ts +235 -0
- package/src/git-http/receive-pack.test.ts +397 -0
- package/src/git-http/receive-pack.ts +261 -0
- package/src/git-http/side-band-64k.test.ts +181 -0
- package/src/git-http/side-band-64k.ts +134 -0
- package/src/git-http/upload-pack.test.ts +545 -0
- package/src/git-http/upload-pack.ts +396 -0
- package/src/index.ts +23 -0
- package/src/middleware/git-token-auth.test.ts +587 -0
- package/src/middleware/git-token-auth.ts +315 -0
- package/src/middleware/grant.ts +106 -0
- package/src/middleware/session.ts +13 -0
- package/src/middleware/tenant.test.ts +192 -0
- package/src/middleware/tenant.ts +101 -0
- package/src/openapi.ts +66 -0
- package/src/pagination.ts +117 -0
- package/src/routes/agent-data.ts +179 -0
- package/src/routes/agent-state-git.ts +562 -0
- package/src/routes/agents.test.ts +337 -0
- package/src/routes/agents.ts +704 -0
- package/src/routes/approvals.ts +130 -0
- package/src/routes/assets.test.ts +567 -0
- package/src/routes/assets.ts +592 -0
- package/src/routes/credentials.ts +435 -0
- package/src/routes/git-tokens.test.ts +709 -0
- package/src/routes/git-tokens.ts +771 -0
- package/src/routes/grants.ts +509 -0
- package/src/routes/instances.test.ts +1103 -0
- package/src/routes/instances.ts +1797 -0
- package/src/routes/me.ts +405 -0
- package/src/routes/oauth-clients.ts +349 -0
- package/src/routes/observability.ts +146 -0
- package/src/routes/offerings.ts +382 -0
- package/src/routes/principals.ts +515 -0
- package/src/routes/providers.ts +351 -0
- package/src/routes/roles.ts +452 -0
- package/src/routes/sidecars.ts +221 -0
- package/src/routes/tenant-federation.ts +225 -0
- package/src/routes/tenants.ts +369 -0
- package/src/routes/wallets.ts +370 -0
- package/src/session.ts +44 -0
- package/src/timeline-reconstruction.test.ts +786 -0
- package/src/timeline-reconstruction.ts +383 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { eq, and } from "drizzle-orm";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
4
|
+
|
|
5
|
+
import { federationTrust, tenant } from "@intx/db/schema";
|
|
6
|
+
import type { DB } from "@intx/db";
|
|
7
|
+
import {
|
|
8
|
+
FederationTrust,
|
|
9
|
+
CreateFederationTrust,
|
|
10
|
+
ErrorResponse,
|
|
11
|
+
paginatedSchema,
|
|
12
|
+
} from "@intx/types";
|
|
13
|
+
|
|
14
|
+
import type { TenantEnv } from "../context";
|
|
15
|
+
import { ts } from "../format";
|
|
16
|
+
import { generateId } from "@intx/hub-common";
|
|
17
|
+
import {
|
|
18
|
+
parsePageParams,
|
|
19
|
+
cursorCondition,
|
|
20
|
+
pageOrder,
|
|
21
|
+
paginatedResponse,
|
|
22
|
+
pageParameters,
|
|
23
|
+
} from "../pagination";
|
|
24
|
+
|
|
25
|
+
export type CreateTenantFederationRoutesDeps = {
|
|
26
|
+
db: DB["db"];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function createTenantFederationRoutes({
|
|
30
|
+
db,
|
|
31
|
+
}: CreateTenantFederationRoutesDeps): Hono<TenantEnv> {
|
|
32
|
+
const app = new Hono<TenantEnv>();
|
|
33
|
+
|
|
34
|
+
app.get(
|
|
35
|
+
"/",
|
|
36
|
+
describeRoute({
|
|
37
|
+
tags: ["Tenants"],
|
|
38
|
+
summary: "List federation trust relationships",
|
|
39
|
+
parameters: [...pageParameters],
|
|
40
|
+
responses: {
|
|
41
|
+
200: {
|
|
42
|
+
description: "Federation trusts",
|
|
43
|
+
content: {
|
|
44
|
+
"application/json": {
|
|
45
|
+
schema: resolver(paginatedSchema(FederationTrust)),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
async (c) => {
|
|
52
|
+
const tenantCtx = c.get("tenant");
|
|
53
|
+
const { limit, cursor } = parsePageParams({
|
|
54
|
+
cursor: c.req.query("cursor"),
|
|
55
|
+
limit: c.req.query("limit"),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const conditions = [eq(federationTrust.tenantId, tenantCtx.id)];
|
|
59
|
+
if (cursor) {
|
|
60
|
+
conditions.push(
|
|
61
|
+
cursorCondition(
|
|
62
|
+
federationTrust.createdAt,
|
|
63
|
+
federationTrust.id,
|
|
64
|
+
cursor,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rows = await db.query.federationTrust.findMany({
|
|
70
|
+
where: and(...conditions),
|
|
71
|
+
orderBy: pageOrder(federationTrust.createdAt, federationTrust.id),
|
|
72
|
+
limit,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const targetIds = rows.map((t) => t.targetTenantId);
|
|
76
|
+
const tenants =
|
|
77
|
+
targetIds.length > 0
|
|
78
|
+
? await db.query.tenant.findMany({
|
|
79
|
+
where: (t, { inArray }) => inArray(t.id, targetIds),
|
|
80
|
+
})
|
|
81
|
+
: [];
|
|
82
|
+
const tenantMap = new Map(tenants.map((t) => [t.id, t]));
|
|
83
|
+
|
|
84
|
+
const items = rows.map((trust) => {
|
|
85
|
+
const target = tenantMap.get(trust.targetTenantId);
|
|
86
|
+
return {
|
|
87
|
+
tenantId: trust.targetTenantId,
|
|
88
|
+
tenantName: target?.name ?? "Unknown",
|
|
89
|
+
tenantDomain: target?.domain ?? "unknown",
|
|
90
|
+
direction: trust.direction,
|
|
91
|
+
createdAt: ts(trust.createdAt),
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return c.json(paginatedResponse(items, rows, limit));
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
app.post(
|
|
100
|
+
"/",
|
|
101
|
+
describeRoute({
|
|
102
|
+
tags: ["Tenants"],
|
|
103
|
+
summary: "Establish federation trust",
|
|
104
|
+
description:
|
|
105
|
+
"Creates a trust relationship with another tenant for cross-tenant agent discovery and interaction.",
|
|
106
|
+
responses: {
|
|
107
|
+
201: {
|
|
108
|
+
description: "Trust established",
|
|
109
|
+
content: {
|
|
110
|
+
"application/json": { schema: resolver(FederationTrust) },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
400: {
|
|
114
|
+
description: "Validation error",
|
|
115
|
+
content: {
|
|
116
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
validator("json", CreateFederationTrust),
|
|
122
|
+
async (c) => {
|
|
123
|
+
const tenantCtx = c.get("tenant");
|
|
124
|
+
const body = c.req.valid("json");
|
|
125
|
+
|
|
126
|
+
const target = await db.query.tenant.findFirst({
|
|
127
|
+
where: eq(tenant.id, body.targetTenantId),
|
|
128
|
+
});
|
|
129
|
+
if (!target) {
|
|
130
|
+
return c.json(
|
|
131
|
+
{
|
|
132
|
+
error: {
|
|
133
|
+
code: "not_found",
|
|
134
|
+
message: "Target tenant not found",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
404,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const existing = await db.query.federationTrust.findFirst({
|
|
142
|
+
where: and(
|
|
143
|
+
eq(federationTrust.tenantId, tenantCtx.id),
|
|
144
|
+
eq(federationTrust.targetTenantId, body.targetTenantId),
|
|
145
|
+
),
|
|
146
|
+
});
|
|
147
|
+
if (existing) {
|
|
148
|
+
return c.json(
|
|
149
|
+
{
|
|
150
|
+
error: {
|
|
151
|
+
code: "conflict",
|
|
152
|
+
message: "Trust relationship already exists",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
409,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await db.insert(federationTrust).values({
|
|
160
|
+
id: generateId("federationTrust"),
|
|
161
|
+
tenantId: tenantCtx.id,
|
|
162
|
+
targetTenantId: body.targetTenantId,
|
|
163
|
+
direction: body.direction,
|
|
164
|
+
createdAt: new Date(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return c.json(
|
|
168
|
+
{
|
|
169
|
+
tenantId: body.targetTenantId,
|
|
170
|
+
tenantName: target.name,
|
|
171
|
+
tenantDomain: target.domain,
|
|
172
|
+
direction: body.direction,
|
|
173
|
+
createdAt: ts(new Date()),
|
|
174
|
+
},
|
|
175
|
+
201,
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
app.delete(
|
|
181
|
+
"/:targetTenantId",
|
|
182
|
+
describeRoute({
|
|
183
|
+
tags: ["Tenants"],
|
|
184
|
+
summary: "Revoke federation trust",
|
|
185
|
+
responses: {
|
|
186
|
+
204: {
|
|
187
|
+
description: "Trust revoked",
|
|
188
|
+
},
|
|
189
|
+
404: {
|
|
190
|
+
description: "Trust not found",
|
|
191
|
+
content: {
|
|
192
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
async (c) => {
|
|
198
|
+
const tenantCtx = c.get("tenant");
|
|
199
|
+
const targetTenantId = c.req.param("targetTenantId");
|
|
200
|
+
|
|
201
|
+
const deleted = await db
|
|
202
|
+
.delete(federationTrust)
|
|
203
|
+
.where(
|
|
204
|
+
and(
|
|
205
|
+
eq(federationTrust.tenantId, tenantCtx.id),
|
|
206
|
+
eq(federationTrust.targetTenantId, targetTenantId),
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
.returning();
|
|
210
|
+
|
|
211
|
+
if (deleted.length === 0) {
|
|
212
|
+
return c.json(
|
|
213
|
+
{
|
|
214
|
+
error: { code: "not_found", message: "Trust not found" },
|
|
215
|
+
},
|
|
216
|
+
404,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return c.body(null, 204);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return app;
|
|
225
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { eq, and } from "drizzle-orm";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
4
|
+
|
|
5
|
+
import { tenant, principal, role, principalRole, grant } from "@intx/db/schema";
|
|
6
|
+
import { parseTenantRow } from "@intx/db";
|
|
7
|
+
import type { DB } from "@intx/db";
|
|
8
|
+
import {
|
|
9
|
+
CreateTenant,
|
|
10
|
+
UpdateTenant,
|
|
11
|
+
TenantResponse,
|
|
12
|
+
ErrorResponse,
|
|
13
|
+
} from "@intx/types";
|
|
14
|
+
|
|
15
|
+
import type { AppEnv } from "../context";
|
|
16
|
+
import { first, ts } from "../format";
|
|
17
|
+
import { generateId } from "@intx/hub-common";
|
|
18
|
+
|
|
19
|
+
const SYSTEM_ROLES = ["owner", "admin", "member"] as const;
|
|
20
|
+
|
|
21
|
+
function formatTenant(row: typeof tenant.$inferSelect) {
|
|
22
|
+
const parsed = parseTenantRow(row);
|
|
23
|
+
return {
|
|
24
|
+
id: parsed.id,
|
|
25
|
+
name: parsed.name,
|
|
26
|
+
slug: parsed.slug,
|
|
27
|
+
domain: parsed.domain,
|
|
28
|
+
parentId: parsed.parentId ?? null,
|
|
29
|
+
config: parsed.config ?? undefined,
|
|
30
|
+
createdAt: ts(parsed.createdAt),
|
|
31
|
+
updatedAt: ts(parsed.updatedAt),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CreateTenantRoutesDeps = {
|
|
36
|
+
db: DB["db"];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function createTenantRoutes({
|
|
40
|
+
db,
|
|
41
|
+
}: CreateTenantRoutesDeps): Hono<AppEnv> {
|
|
42
|
+
const app = new Hono<AppEnv>();
|
|
43
|
+
|
|
44
|
+
app.post(
|
|
45
|
+
"/",
|
|
46
|
+
describeRoute({
|
|
47
|
+
tags: ["Tenants"],
|
|
48
|
+
summary: "Create a tenant",
|
|
49
|
+
description:
|
|
50
|
+
"Creates a new tenant. The authenticated user becomes the owner with a principal and default owner role.",
|
|
51
|
+
responses: {
|
|
52
|
+
201: {
|
|
53
|
+
description: "Tenant created",
|
|
54
|
+
content: {
|
|
55
|
+
"application/json": { schema: resolver(TenantResponse) },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
400: {
|
|
59
|
+
description: "Validation error",
|
|
60
|
+
content: {
|
|
61
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
validator("json", CreateTenant),
|
|
67
|
+
async (c) => {
|
|
68
|
+
const user = c.get("user");
|
|
69
|
+
if (!user) {
|
|
70
|
+
return c.json(
|
|
71
|
+
{
|
|
72
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
73
|
+
},
|
|
74
|
+
401,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const body = c.req.valid("json");
|
|
79
|
+
|
|
80
|
+
const tenantId = generateId("tenant");
|
|
81
|
+
const domain = `${body.slug}.localhost`;
|
|
82
|
+
|
|
83
|
+
const existing = await db.query.tenant.findFirst({
|
|
84
|
+
where: eq(tenant.slug, body.slug),
|
|
85
|
+
});
|
|
86
|
+
if (existing) {
|
|
87
|
+
return c.json(
|
|
88
|
+
{
|
|
89
|
+
error: { code: "conflict", message: "Slug already taken" },
|
|
90
|
+
},
|
|
91
|
+
409,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const now = new Date();
|
|
96
|
+
|
|
97
|
+
const tenantRow = first(
|
|
98
|
+
await db
|
|
99
|
+
.insert(tenant)
|
|
100
|
+
.values({
|
|
101
|
+
id: tenantId,
|
|
102
|
+
name: body.name,
|
|
103
|
+
slug: body.slug,
|
|
104
|
+
domain,
|
|
105
|
+
parentId: body.parentId ?? null,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
})
|
|
109
|
+
.returning(),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const roleIds: Record<string, string> = {};
|
|
113
|
+
for (const roleName of SYSTEM_ROLES) {
|
|
114
|
+
const roleId = generateId("role");
|
|
115
|
+
roleIds[roleName] = roleId;
|
|
116
|
+
await db.insert(role).values({
|
|
117
|
+
id: roleId,
|
|
118
|
+
tenantId,
|
|
119
|
+
name: roleName,
|
|
120
|
+
description: `System ${roleName} role`,
|
|
121
|
+
isSystem: true,
|
|
122
|
+
createdAt: now,
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ownerRoleId = roleIds["owner"];
|
|
128
|
+
if (!ownerRoleId) throw new Error("Owner role was not created");
|
|
129
|
+
|
|
130
|
+
const principalId = generateId("principal");
|
|
131
|
+
await db.insert(principal).values({
|
|
132
|
+
id: principalId,
|
|
133
|
+
tenantId,
|
|
134
|
+
kind: "user",
|
|
135
|
+
refId: user.id,
|
|
136
|
+
status: "active",
|
|
137
|
+
createdAt: now,
|
|
138
|
+
updatedAt: now,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await db.insert(principalRole).values({
|
|
142
|
+
principalId,
|
|
143
|
+
roleId: ownerRoleId,
|
|
144
|
+
createdAt: now,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Grant owner role full access
|
|
148
|
+
await db.insert(grant).values({
|
|
149
|
+
id: generateId("grant"),
|
|
150
|
+
tenantId,
|
|
151
|
+
roleId: ownerRoleId,
|
|
152
|
+
resource: "*",
|
|
153
|
+
action: "*",
|
|
154
|
+
effect: "allow",
|
|
155
|
+
origin: "system",
|
|
156
|
+
createdAt: now,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Grant admin role broad management access
|
|
161
|
+
const adminRoleId = roleIds["admin"];
|
|
162
|
+
if (adminRoleId) {
|
|
163
|
+
await db.insert(grant).values([
|
|
164
|
+
{
|
|
165
|
+
id: generateId("grant"),
|
|
166
|
+
tenantId,
|
|
167
|
+
roleId: adminRoleId,
|
|
168
|
+
resource: "*",
|
|
169
|
+
action: "read",
|
|
170
|
+
effect: "allow",
|
|
171
|
+
origin: "system",
|
|
172
|
+
createdAt: now,
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: generateId("grant"),
|
|
177
|
+
tenantId,
|
|
178
|
+
roleId: adminRoleId,
|
|
179
|
+
resource: "*",
|
|
180
|
+
action: "create",
|
|
181
|
+
effect: "allow",
|
|
182
|
+
origin: "system",
|
|
183
|
+
createdAt: now,
|
|
184
|
+
updatedAt: now,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: generateId("grant"),
|
|
188
|
+
tenantId,
|
|
189
|
+
roleId: adminRoleId,
|
|
190
|
+
resource: "*",
|
|
191
|
+
action: "manage",
|
|
192
|
+
effect: "allow",
|
|
193
|
+
origin: "system",
|
|
194
|
+
createdAt: now,
|
|
195
|
+
updatedAt: now,
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Grant member role read-only access
|
|
201
|
+
const memberRoleId = roleIds["member"];
|
|
202
|
+
if (memberRoleId) {
|
|
203
|
+
await db.insert(grant).values({
|
|
204
|
+
id: generateId("grant"),
|
|
205
|
+
tenantId,
|
|
206
|
+
roleId: memberRoleId,
|
|
207
|
+
resource: "*",
|
|
208
|
+
action: "read",
|
|
209
|
+
effect: "allow",
|
|
210
|
+
origin: "system",
|
|
211
|
+
createdAt: now,
|
|
212
|
+
updatedAt: now,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return c.json(formatTenant(tenantRow), 201);
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
app.get(
|
|
221
|
+
"/:tenantId",
|
|
222
|
+
describeRoute({
|
|
223
|
+
tags: ["Tenants"],
|
|
224
|
+
summary: "Get tenant details",
|
|
225
|
+
responses: {
|
|
226
|
+
200: {
|
|
227
|
+
description: "Tenant details",
|
|
228
|
+
content: {
|
|
229
|
+
"application/json": { schema: resolver(TenantResponse) },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
403: {
|
|
233
|
+
description: "Not a member of this tenant",
|
|
234
|
+
content: {
|
|
235
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
404: {
|
|
239
|
+
description: "Tenant not found",
|
|
240
|
+
content: {
|
|
241
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
async (c) => {
|
|
247
|
+
const user = c.get("user");
|
|
248
|
+
if (!user) {
|
|
249
|
+
return c.json(
|
|
250
|
+
{
|
|
251
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
252
|
+
},
|
|
253
|
+
401,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const tenantId = c.req.param("tenantId");
|
|
258
|
+
|
|
259
|
+
const tenantRow = await db.query.tenant.findFirst({
|
|
260
|
+
where: eq(tenant.id, tenantId),
|
|
261
|
+
});
|
|
262
|
+
if (!tenantRow) {
|
|
263
|
+
return c.json(
|
|
264
|
+
{ error: { code: "not_found", message: "Tenant not found" } },
|
|
265
|
+
404,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const membership = await db.query.principal.findFirst({
|
|
270
|
+
where: and(
|
|
271
|
+
eq(principal.tenantId, tenantId),
|
|
272
|
+
eq(principal.kind, "user"),
|
|
273
|
+
eq(principal.refId, user.id),
|
|
274
|
+
),
|
|
275
|
+
});
|
|
276
|
+
if (!membership) {
|
|
277
|
+
return c.json(
|
|
278
|
+
{
|
|
279
|
+
error: {
|
|
280
|
+
code: "forbidden",
|
|
281
|
+
message: "Not a member of this tenant",
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
403,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return c.json(formatTenant(tenantRow));
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
app.patch(
|
|
293
|
+
"/:tenantId",
|
|
294
|
+
describeRoute({
|
|
295
|
+
tags: ["Tenants"],
|
|
296
|
+
summary: "Update tenant config",
|
|
297
|
+
description: "Requires admin or higher grant within the tenant.",
|
|
298
|
+
responses: {
|
|
299
|
+
200: {
|
|
300
|
+
description: "Tenant updated",
|
|
301
|
+
content: {
|
|
302
|
+
"application/json": { schema: resolver(TenantResponse) },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
403: {
|
|
306
|
+
description: "Insufficient grants",
|
|
307
|
+
content: {
|
|
308
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
validator("json", UpdateTenant),
|
|
314
|
+
async (c) => {
|
|
315
|
+
const user = c.get("user");
|
|
316
|
+
if (!user) {
|
|
317
|
+
return c.json(
|
|
318
|
+
{
|
|
319
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
320
|
+
},
|
|
321
|
+
401,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const tenantId = c.req.param("tenantId");
|
|
326
|
+
const body = c.req.valid("json");
|
|
327
|
+
|
|
328
|
+
const membership = await db.query.principal.findFirst({
|
|
329
|
+
where: and(
|
|
330
|
+
eq(principal.tenantId, tenantId),
|
|
331
|
+
eq(principal.kind, "user"),
|
|
332
|
+
eq(principal.refId, user.id),
|
|
333
|
+
),
|
|
334
|
+
});
|
|
335
|
+
if (!membership) {
|
|
336
|
+
return c.json(
|
|
337
|
+
{
|
|
338
|
+
error: {
|
|
339
|
+
code: "forbidden",
|
|
340
|
+
message: "Not a member of this tenant",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
403,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
348
|
+
if (body.name !== undefined) updates["name"] = body.name;
|
|
349
|
+
if (body.config !== undefined) updates["config"] = body.config;
|
|
350
|
+
|
|
351
|
+
const [updated] = await db
|
|
352
|
+
.update(tenant)
|
|
353
|
+
.set(updates)
|
|
354
|
+
.where(eq(tenant.id, tenantId))
|
|
355
|
+
.returning();
|
|
356
|
+
|
|
357
|
+
if (!updated) {
|
|
358
|
+
return c.json(
|
|
359
|
+
{ error: { code: "not_found", message: "Tenant not found" } },
|
|
360
|
+
404,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return c.json(formatTenant(updated));
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
return app;
|
|
369
|
+
}
|