@cosmicdrift/kumiko-bundled-features 0.124.0 → 0.125.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/package.json +14 -6
- package/src/admin-shell/__tests__/admin-shell-security.integration.test.ts +199 -0
- package/src/admin-shell/__tests__/admin-shell.boot.test.ts +131 -0
- package/src/admin-shell/__tests__/overview-allowlist.test.ts +33 -0
- package/src/admin-shell/__tests__/overview-screens.boot.test.ts +33 -0
- package/src/admin-shell/constants.ts +11 -0
- package/src/admin-shell/feature.ts +134 -0
- package/src/admin-shell/i18n.ts +35 -0
- package/src/admin-shell/index.ts +17 -0
- package/src/admin-shell/overview-allowlist.ts +32 -0
- package/src/admin-shell/web/client-plugin.tsx +26 -0
- package/src/admin-shell/web/i18n.ts +16 -0
- package/src/admin-shell/web/index.ts +2 -0
- package/src/admin-shell/web/overview-layout.tsx +68 -0
- package/src/admin-shell/web/overview-query.ts +15 -0
- package/src/admin-shell/web/platform-overview-screen.tsx +75 -0
- package/src/admin-shell/web/tenant-overview-screen.tsx +90 -0
- package/src/audit/__tests__/audit-screens.boot.test.ts +29 -0
- package/src/audit/__tests__/audit-security.integration.test.ts +151 -0
- package/src/audit/__tests__/audit.integration.test.ts +0 -4
- package/src/audit/constants.ts +5 -2
- package/src/audit/feature.ts +24 -1
- package/src/audit/handlers/list.query.ts +2 -2
- package/src/audit/i18n.ts +18 -0
- package/src/audit/web/audit-log-screen.tsx +136 -0
- package/src/audit/web/client-plugin.tsx +20 -0
- package/src/audit/web/i18n.ts +12 -0
- package/src/audit/web/index.ts +4 -0
- package/src/auth-email-password/handlers/invite-create.write.ts +2 -2
- package/src/auth-email-password/web/use-shell-user.ts +5 -2
- package/src/cap-counter/__tests__/cap-list.boot.test.ts +27 -0
- package/src/cap-counter/__tests__/cap-list.security.integration.test.ts +53 -0
- package/src/cap-counter/constants.ts +2 -0
- package/src/cap-counter/feature.ts +20 -1
- package/src/compliance-profiles/__tests__/profile-picker.boot.test.ts +28 -0
- package/src/compliance-profiles/__tests__/profile-picker.security.integration.test.ts +91 -0
- package/src/compliance-profiles/constants.ts +15 -0
- package/src/compliance-profiles/feature.ts +19 -1
- package/src/compliance-profiles/handlers/set-profile.write.ts +2 -2
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/web/client-plugin.tsx +22 -0
- package/src/compliance-profiles/web/compliance-profile-screen.tsx +152 -0
- package/src/compliance-profiles/web/i18n.ts +25 -0
- package/src/compliance-profiles/web/index.ts +6 -0
- package/src/delivery/__tests__/delivery-screens.boot.test.ts +27 -0
- package/src/delivery/__tests__/delivery-security.integration.test.ts +106 -0
- package/src/delivery/constants.ts +13 -51
- package/src/delivery/feature.ts +19 -2
- package/src/delivery/handlers/log.query.ts +14 -6
- package/src/delivery/index.ts +1 -0
- package/src/delivery/public-names.ts +36 -0
- package/src/delivery/web/client-plugin.tsx +20 -0
- package/src/delivery/web/delivery-log-screen.tsx +103 -0
- package/src/delivery/web/i18n.ts +25 -0
- package/src/delivery/web/index.ts +3 -0
- package/src/feature-toggles/__tests__/toggle-admin.boot.test.ts +31 -0
- package/src/feature-toggles/__tests__/toggle-admin.security.integration.test.ts +69 -0
- package/src/feature-toggles/constants.ts +14 -0
- package/src/feature-toggles/feature.ts +14 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +1 -1
- package/src/feature-toggles/index.ts +6 -0
- package/src/feature-toggles/web/client-plugin.tsx +22 -0
- package/src/feature-toggles/web/i18n.ts +29 -0
- package/src/feature-toggles/web/index.ts +6 -0
- package/src/feature-toggles/web/toggle-admin-screen.tsx +136 -0
- package/src/jobs/__tests__/jobs-screens.boot.test.ts +36 -0
- package/src/jobs/__tests__/jobs-security.integration.test.ts +108 -0
- package/src/jobs/constants.ts +8 -1
- package/src/jobs/feature.ts +26 -0
- package/src/jobs/i18n.ts +32 -0
- package/src/jobs/web/client-plugin.tsx +22 -0
- package/src/jobs/web/i18n.ts +12 -0
- package/src/jobs/web/index.ts +4 -0
- package/src/jobs/web/job-run-detail-screen.tsx +169 -0
- package/src/jobs/web/job-runs-screen.tsx +148 -0
- package/src/tenant/__tests__/members-screens.boot.test.ts +44 -0
- package/src/tenant/__tests__/tenant-security.integration.test.ts +295 -0
- package/src/tenant/constants.ts +5 -0
- package/src/tenant/feature.ts +16 -0
- package/src/tenant/handlers/cancel-invitation.write.ts +2 -2
- package/src/tenant/handlers/invitations.query.ts +2 -2
- package/src/tenant/handlers/members.query.ts +2 -2
- package/src/tenant/web/client-plugin.tsx +20 -0
- package/src/tenant/web/i18n.ts +43 -0
- package/src/tenant/web/index.ts +4 -0
- package/src/tenant/web/members-screen.tsx +265 -0
- package/src/tier-engine/web/tier-admin-screen.tsx +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -19,13 +19,18 @@
|
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
"./audit": "./src/audit/index.ts",
|
|
22
|
+
"./audit/web": "./src/audit/web/index.ts",
|
|
23
|
+
"./admin-shell": "./src/admin-shell/index.ts",
|
|
24
|
+
"./admin-shell/web": "./src/admin-shell/web/index.ts",
|
|
22
25
|
"./compliance-profiles": "./src/compliance-profiles/index.ts",
|
|
26
|
+
"./compliance-profiles/web": "./src/compliance-profiles/web/index.ts",
|
|
23
27
|
"./config": "./src/config/index.ts",
|
|
24
28
|
"./crypto-shredding": "./src/crypto-shredding/index.ts",
|
|
25
29
|
"./config/web": "./src/config/web/index.ts",
|
|
26
30
|
"./data-retention": "./src/data-retention/index.ts",
|
|
27
31
|
"./readiness": "./src/readiness/index.ts",
|
|
28
32
|
"./jobs": "./src/jobs/index.ts",
|
|
33
|
+
"./jobs/web": "./src/jobs/web/index.ts",
|
|
29
34
|
"./tier-engine": "./src/tier-engine/index.ts",
|
|
30
35
|
"./tier-engine/web": "./src/tier-engine/web/index.ts",
|
|
31
36
|
"./cap-counter": "./src/cap-counter/index.ts",
|
|
@@ -55,6 +60,7 @@
|
|
|
55
60
|
"./user-data-rights-defaults": "./src/user-data-rights-defaults/index.ts",
|
|
56
61
|
"./tenant": "./src/tenant/index.ts",
|
|
57
62
|
"./tenant/constants": "./src/tenant/constants.ts",
|
|
63
|
+
"./tenant/web": "./src/tenant/web/index.ts",
|
|
58
64
|
"./tenant/seeding": "./src/tenant/seeding.ts",
|
|
59
65
|
"./tenant/testing": "./src/tenant/testing.ts",
|
|
60
66
|
"./user": "./src/user/index.ts",
|
|
@@ -68,6 +74,7 @@
|
|
|
68
74
|
"./auth-email-password/testing": "./src/auth-email-password/testing.ts",
|
|
69
75
|
"./auth-email-password/web": "./src/auth-email-password/web/index.ts",
|
|
70
76
|
"./delivery": "./src/delivery/index.ts",
|
|
77
|
+
"./delivery/web": "./src/delivery/web/index.ts",
|
|
71
78
|
"./channel-in-app": "./src/channel-in-app/index.ts",
|
|
72
79
|
"./channel-email": "./src/channel-email/index.ts",
|
|
73
80
|
"./channel-push": "./src/channel-push/index.ts",
|
|
@@ -81,6 +88,7 @@
|
|
|
81
88
|
"./personal-access-tokens": "./src/personal-access-tokens/index.ts",
|
|
82
89
|
"./personal-access-tokens/web": "./src/personal-access-tokens/web/index.ts",
|
|
83
90
|
"./feature-toggles": "./src/feature-toggles/index.ts",
|
|
91
|
+
"./feature-toggles/web": "./src/feature-toggles/web/index.ts",
|
|
84
92
|
"./text-content": "./src/text-content/index.ts",
|
|
85
93
|
"./text-content/seeding": "./src/text-content/seeding.ts",
|
|
86
94
|
"./text-content/web": "./src/text-content/web/index.ts",
|
|
@@ -98,11 +106,11 @@
|
|
|
98
106
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
99
107
|
},
|
|
100
108
|
"dependencies": {
|
|
101
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
102
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
103
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
104
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
105
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
109
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.125.0",
|
|
110
|
+
"@cosmicdrift/kumiko-framework": "0.125.0",
|
|
111
|
+
"@cosmicdrift/kumiko-headless": "0.125.0",
|
|
112
|
+
"@cosmicdrift/kumiko-renderer": "0.125.0",
|
|
113
|
+
"@cosmicdrift/kumiko-renderer-web": "0.125.0",
|
|
106
114
|
"@mollie/api-client": "^4.5.0",
|
|
107
115
|
"@node-rs/argon2": "^2.0.2",
|
|
108
116
|
"@types/nodemailer": "^8.0.0",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Security integration: workspace role gates + HTTP 403 on platform queries for TenantAdmin.
|
|
2
|
+
|
|
3
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
5
|
+
import {
|
|
6
|
+
access,
|
|
7
|
+
createRegistry,
|
|
8
|
+
type SessionUser,
|
|
9
|
+
type TenantId,
|
|
10
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
11
|
+
import {
|
|
12
|
+
setupTestStack,
|
|
13
|
+
type TestStack,
|
|
14
|
+
TestUsers,
|
|
15
|
+
unsafeCreateEntityTable,
|
|
16
|
+
unsafePushTables,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
|
+
import { createAuditFeature } from "../../audit/feature";
|
|
19
|
+
import { hashPassword } from "../../auth-email-password/password-hashing";
|
|
20
|
+
import { ConfigQueries } from "../../config/constants";
|
|
21
|
+
import { createConfigFeature } from "../../config/feature";
|
|
22
|
+
import { createConfigResolver } from "../../config/resolver";
|
|
23
|
+
import { configValuesTable } from "../../config/table";
|
|
24
|
+
import { createFeatureTogglesFeature } from "../../feature-toggles/feature";
|
|
25
|
+
import { globalFeatureStateTable } from "../../feature-toggles/global-feature-state-table";
|
|
26
|
+
import { JobQueries } from "../../jobs/constants";
|
|
27
|
+
import { createJobsFeature } from "../../jobs/feature";
|
|
28
|
+
import { jobRunLogsTable, jobRunsTable } from "../../jobs/job-run-table";
|
|
29
|
+
import { TenantQueries } from "../../tenant/constants";
|
|
30
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
31
|
+
import { tenantInvitationEntity } from "../../tenant/invitation-table";
|
|
32
|
+
import { tenantMembershipsTable } from "../../tenant/membership-table";
|
|
33
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
34
|
+
import { seedTenant, seedTenantMembership } from "../../tenant/seeding";
|
|
35
|
+
import { tierEngineFeature } from "../../tier-engine/feature";
|
|
36
|
+
import { createUserFeature } from "../../user/feature";
|
|
37
|
+
import { userEntity } from "../../user/schema/user";
|
|
38
|
+
import { seedUser } from "../../user/seeding";
|
|
39
|
+
import {
|
|
40
|
+
ADMIN_SHELL_FEATURE,
|
|
41
|
+
DEFAULT_PLATFORM_WORKSPACE_ID,
|
|
42
|
+
DEFAULT_TENANT_WORKSPACE_ID,
|
|
43
|
+
} from "../constants";
|
|
44
|
+
import { createAdminShellFeature } from "../feature";
|
|
45
|
+
import {
|
|
46
|
+
TENANT_OVERVIEW_ALLOWED_QUERIES,
|
|
47
|
+
TENANT_OVERVIEW_FORBIDDEN_QUERIES,
|
|
48
|
+
} from "../overview-allowlist";
|
|
49
|
+
|
|
50
|
+
let stack: TestStack;
|
|
51
|
+
let TENANT_ID: TenantId;
|
|
52
|
+
let tenantAdminId: string;
|
|
53
|
+
|
|
54
|
+
const adminShell = createAdminShellFeature();
|
|
55
|
+
const features = [
|
|
56
|
+
createConfigFeature(),
|
|
57
|
+
createUserFeature(),
|
|
58
|
+
createTenantFeature(),
|
|
59
|
+
createAuditFeature(),
|
|
60
|
+
createJobsFeature(),
|
|
61
|
+
createFeatureTogglesFeature(),
|
|
62
|
+
tierEngineFeature,
|
|
63
|
+
adminShell,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function tenantAdmin(): SessionUser {
|
|
67
|
+
return { id: tenantAdminId, tenantId: TENANT_ID, roles: ["TenantAdmin"] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function regularUser(): SessionUser {
|
|
71
|
+
return { id: tenantAdminId, tenantId: TENANT_ID, roles: ["User"] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
stack = await setupTestStack({
|
|
76
|
+
features,
|
|
77
|
+
extraContext: () => ({ configResolver: createConfigResolver() }),
|
|
78
|
+
});
|
|
79
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
80
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
81
|
+
await unsafeCreateEntityTable(stack.db, tenantInvitationEntity);
|
|
82
|
+
await unsafePushTables(stack.db, {
|
|
83
|
+
configValuesTable,
|
|
84
|
+
tenantMembershipsTable,
|
|
85
|
+
globalFeatureStateTable,
|
|
86
|
+
jobRunsTable,
|
|
87
|
+
jobRunLogsTable,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterAll(async () => {
|
|
92
|
+
await stack.cleanup();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
|
|
97
|
+
TENANT_ID = crypto.randomUUID() as TenantId;
|
|
98
|
+
await seedTenant(stack.db, {
|
|
99
|
+
id: TENANT_ID,
|
|
100
|
+
key: `t-${TENANT_ID.slice(0, 8)}`,
|
|
101
|
+
name: "Shell Tenant",
|
|
102
|
+
});
|
|
103
|
+
({ id: tenantAdminId } = await seedUser(stack.db, {
|
|
104
|
+
email: `ta-${TENANT_ID.slice(0, 8)}@example.com`,
|
|
105
|
+
displayName: "Tenant Admin",
|
|
106
|
+
passwordHash: await hashPassword("pw-shell-ta-1234"),
|
|
107
|
+
emailVerified: true,
|
|
108
|
+
}));
|
|
109
|
+
await seedTenantMembership(stack.db, {
|
|
110
|
+
userId: tenantAdminId,
|
|
111
|
+
tenantId: TENANT_ID,
|
|
112
|
+
roles: ["TenantAdmin"],
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("workspace access matrix", () => {
|
|
117
|
+
test("tenant workspace is access.admin; platform is SystemAdmin-only", () => {
|
|
118
|
+
const registry = createRegistry(features);
|
|
119
|
+
const tenantWs = registry.getWorkspace(
|
|
120
|
+
`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_TENANT_WORKSPACE_ID}`,
|
|
121
|
+
);
|
|
122
|
+
const platformWs = registry.getWorkspace(
|
|
123
|
+
`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_PLATFORM_WORKSPACE_ID}`,
|
|
124
|
+
);
|
|
125
|
+
expect(tenantWs?.access).toEqual({ roles: access.admin });
|
|
126
|
+
expect(platformWs?.access).toEqual({ roles: access.systemAdmin });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("TenantAdmin HTTP surface", () => {
|
|
131
|
+
test("can list members and audit for own tenant", async () => {
|
|
132
|
+
const members = await stack.http.queryOk<readonly unknown[]>(
|
|
133
|
+
TenantQueries.members,
|
|
134
|
+
{},
|
|
135
|
+
tenantAdmin(),
|
|
136
|
+
);
|
|
137
|
+
expect(members.some((m) => (m as { userId: string }).userId === tenantAdminId)).toBe(true);
|
|
138
|
+
const audit = await stack.http.queryOk<{ rows: readonly unknown[] }>(
|
|
139
|
+
"audit:query:list",
|
|
140
|
+
{},
|
|
141
|
+
tenantAdmin(),
|
|
142
|
+
);
|
|
143
|
+
expect(Array.isArray(audit.rows)).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("403 on platform jobs:list and tenant:list", async () => {
|
|
147
|
+
expect((await stack.http.query(JobQueries.list, {}, tenantAdmin())).status).toBe(403);
|
|
148
|
+
expect((await stack.http.query(TenantQueries.list, {}, tenantAdmin())).status).toBe(403);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("regular User denied tenant-admin nav data", () => {
|
|
153
|
+
test("403 on members and audit", async () => {
|
|
154
|
+
expect((await stack.http.query(TenantQueries.members, {}, regularUser())).status).toBe(403);
|
|
155
|
+
expect((await stack.http.query("audit:query:list", {}, regularUser())).status).toBe(403);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("SystemAdmin platform queries", () => {
|
|
160
|
+
test("can list tenants and job runs", async () => {
|
|
161
|
+
const tenants = await stack.http.queryOk<{ rows: readonly unknown[] }>(
|
|
162
|
+
TenantQueries.list,
|
|
163
|
+
{},
|
|
164
|
+
TestUsers.systemAdmin,
|
|
165
|
+
);
|
|
166
|
+
expect(tenants.rows.length).toBeGreaterThanOrEqual(1);
|
|
167
|
+
const jobs = await stack.http.queryOk<{ rows: readonly unknown[] }>(
|
|
168
|
+
JobQueries.list,
|
|
169
|
+
{},
|
|
170
|
+
TestUsers.systemAdmin,
|
|
171
|
+
);
|
|
172
|
+
expect(Array.isArray(jobs.rows)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("tenant overview query allowlist", () => {
|
|
177
|
+
test("static allowlist excludes every forbidden platform query", () => {
|
|
178
|
+
for (const qn of TENANT_OVERVIEW_FORBIDDEN_QUERIES) {
|
|
179
|
+
expect(TENANT_OVERVIEW_ALLOWED_QUERIES).not.toContain(qn);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("TenantAdmin can run tenant-overview allowed queries", async () => {
|
|
184
|
+
await stack.http.queryOk(TenantQueries.invitations, {}, tenantAdmin());
|
|
185
|
+
await stack.http.queryOk(TenantQueries.members, {}, tenantAdmin());
|
|
186
|
+
const readiness = await stack.http.queryOk<{ missing: readonly unknown[] }>(
|
|
187
|
+
ConfigQueries.readiness,
|
|
188
|
+
{},
|
|
189
|
+
tenantAdmin(),
|
|
190
|
+
);
|
|
191
|
+
expect(Array.isArray(readiness.missing)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("TenantAdmin gets 403 on every forbidden overview query", async () => {
|
|
195
|
+
for (const qn of TENANT_OVERVIEW_FORBIDDEN_QUERIES) {
|
|
196
|
+
expect((await stack.http.query(qn, {}, tenantAdmin())).status).toBe(403);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { access, createRegistry, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createAuditFeature } from "../../audit/feature";
|
|
4
|
+
import { createConfigFeature } from "../../config/feature";
|
|
5
|
+
import { createJobsFeature } from "../../jobs/feature";
|
|
6
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
7
|
+
import { tierEngineFeature } from "../../tier-engine/feature";
|
|
8
|
+
import {
|
|
9
|
+
ADMIN_SHELL_FEATURE,
|
|
10
|
+
DEFAULT_PLATFORM_WORKSPACE_ID,
|
|
11
|
+
DEFAULT_TENANT_WORKSPACE_ID,
|
|
12
|
+
} from "../constants";
|
|
13
|
+
import { createAdminShellFeature } from "../feature";
|
|
14
|
+
|
|
15
|
+
const features = [
|
|
16
|
+
createConfigFeature(),
|
|
17
|
+
createTenantFeature(),
|
|
18
|
+
createAuditFeature(),
|
|
19
|
+
createJobsFeature(),
|
|
20
|
+
tierEngineFeature,
|
|
21
|
+
createAdminShellFeature(),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe("admin-shell boot + workspace composition", () => {
|
|
25
|
+
test("validateBoot with tenant, audit, jobs, tier-engine", () => {
|
|
26
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("registers tenant + platform workspaces with qualified ids", () => {
|
|
30
|
+
const registry = createRegistry(features);
|
|
31
|
+
expect(
|
|
32
|
+
registry.getWorkspace(`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_TENANT_WORKSPACE_ID}`)?.id,
|
|
33
|
+
).toBe(`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_TENANT_WORKSPACE_ID}`);
|
|
34
|
+
expect(
|
|
35
|
+
registry.getWorkspace(`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_PLATFORM_WORKSPACE_ID}`)
|
|
36
|
+
?.id,
|
|
37
|
+
).toBe(`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_PLATFORM_WORKSPACE_ID}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("tenant workspace nav references owner-feature navs", () => {
|
|
41
|
+
const registry = createRegistry(features);
|
|
42
|
+
const navs = registry.getWorkspaceNavs(
|
|
43
|
+
`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_TENANT_WORKSPACE_ID}`,
|
|
44
|
+
);
|
|
45
|
+
expect(navs).toEqual([
|
|
46
|
+
"admin-shell:nav:tenant-overview",
|
|
47
|
+
"tenant:nav:members",
|
|
48
|
+
"audit:nav:audit-log",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("platform workspace nav includes overview, tenants, jobs, tier-admin", () => {
|
|
53
|
+
const registry = createRegistry(features);
|
|
54
|
+
const navs = registry.getWorkspaceNavs(
|
|
55
|
+
`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_PLATFORM_WORKSPACE_ID}`,
|
|
56
|
+
);
|
|
57
|
+
expect(navs).toEqual([
|
|
58
|
+
"admin-shell:nav:platform-overview",
|
|
59
|
+
"admin-shell:nav:tenants",
|
|
60
|
+
"jobs:nav:job-runs",
|
|
61
|
+
"admin-shell:nav:tier-admin",
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("workspace access: tenant=access.admin, platform=systemAdmin", () => {
|
|
66
|
+
const registry = createRegistry(features);
|
|
67
|
+
const tenantWs = registry.getWorkspace(
|
|
68
|
+
`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_TENANT_WORKSPACE_ID}`,
|
|
69
|
+
);
|
|
70
|
+
const platformWs = registry.getWorkspace(
|
|
71
|
+
`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_PLATFORM_WORKSPACE_ID}`,
|
|
72
|
+
);
|
|
73
|
+
expect(tenantWs?.access).toEqual({ roles: access.admin });
|
|
74
|
+
expect(platformWs?.access).toEqual({ roles: access.systemAdmin });
|
|
75
|
+
expect(tenantWs?.default).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("custom workspace ids via options", () => {
|
|
79
|
+
const custom = createAdminShellFeature({
|
|
80
|
+
workspaceIds: { tenant: "admin", platform: "sysadmin" },
|
|
81
|
+
includeTierAdmin: false,
|
|
82
|
+
});
|
|
83
|
+
const registry = createRegistry([
|
|
84
|
+
createConfigFeature(),
|
|
85
|
+
createTenantFeature(),
|
|
86
|
+
createAuditFeature(),
|
|
87
|
+
createJobsFeature(),
|
|
88
|
+
custom,
|
|
89
|
+
]);
|
|
90
|
+
expect(registry.getWorkspace("admin-shell:workspace:admin")?.id).toBeDefined();
|
|
91
|
+
expect(registry.getWorkspace("admin-shell:workspace:sysadmin")?.id).toBeDefined();
|
|
92
|
+
expect(registry.getWorkspaceNavs("admin-shell:workspace:sysadmin")).toEqual([
|
|
93
|
+
"admin-shell:nav:platform-overview",
|
|
94
|
+
"admin-shell:nav:tenants",
|
|
95
|
+
"jobs:nav:job-runs",
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("registerWorkspaces:false registers overview screens only", () => {
|
|
100
|
+
const shellOnly = createAdminShellFeature({
|
|
101
|
+
registerWorkspaces: false,
|
|
102
|
+
includeTierAdmin: false,
|
|
103
|
+
});
|
|
104
|
+
const registry = createRegistry([
|
|
105
|
+
createConfigFeature(),
|
|
106
|
+
createTenantFeature(),
|
|
107
|
+
createAuditFeature(),
|
|
108
|
+
createJobsFeature(),
|
|
109
|
+
shellOnly,
|
|
110
|
+
]);
|
|
111
|
+
expect(
|
|
112
|
+
registry.getWorkspace(`${ADMIN_SHELL_FEATURE}:workspace:${DEFAULT_TENANT_WORKSPACE_ID}`),
|
|
113
|
+
).toBeUndefined();
|
|
114
|
+
expect(registry.getScreen("admin-shell:screen:tenant-overview")?.id).toBeDefined();
|
|
115
|
+
expect(registry.getScreen("admin-shell:screen:platform-overview")?.id).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("admin-shell nav entries declare icons (sidebar standard)", () => {
|
|
119
|
+
const shell = createAdminShellFeature({ includeTierAdmin: true });
|
|
120
|
+
const navIds = ["tenant-overview", "platform-overview", "tenants", "tier-admin"] as const;
|
|
121
|
+
for (const id of navIds) {
|
|
122
|
+
expect(shell.navs[id]?.icon, `admin-shell:nav:${id}`).toBeDefined();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("admin-shell registers server translations bundle", () => {
|
|
127
|
+
const shell = createAdminShellFeature();
|
|
128
|
+
expect(shell.translations?.["admin-shell:nav.tenantOverview"]?.["de"]).toBe("Übersicht");
|
|
129
|
+
expect(shell.translations?.["screen:tenant-overview.title"]?.["en"]).toBe("Overview");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { JobQueries } from "../../jobs/constants";
|
|
3
|
+
import { TenantQueries } from "../../tenant/constants";
|
|
4
|
+
import {
|
|
5
|
+
isOverviewQueryAllowed,
|
|
6
|
+
PLATFORM_OVERVIEW_ALLOWED_QUERIES,
|
|
7
|
+
TENANT_OVERVIEW_ALLOWED_QUERIES,
|
|
8
|
+
TENANT_OVERVIEW_FORBIDDEN_QUERIES,
|
|
9
|
+
} from "../overview-allowlist";
|
|
10
|
+
|
|
11
|
+
describe("overview query allowlist", () => {
|
|
12
|
+
test("tenant allowlist excludes platform-only queries", () => {
|
|
13
|
+
for (const forbidden of TENANT_OVERVIEW_FORBIDDEN_QUERIES) {
|
|
14
|
+
expect(isOverviewQueryAllowed("tenant", forbidden)).toBe(false);
|
|
15
|
+
expect(TENANT_OVERVIEW_ALLOWED_QUERIES).not.toContain(forbidden);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("tenant allowlist includes members, invitations, readiness", () => {
|
|
20
|
+
expect(TENANT_OVERVIEW_ALLOWED_QUERIES).toContain(TenantQueries.members);
|
|
21
|
+
expect(TENANT_OVERVIEW_ALLOWED_QUERIES).toContain(TenantQueries.invitations);
|
|
22
|
+
expect(TENANT_OVERVIEW_ALLOWED_QUERIES).toContain("config:query:readiness");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("platform allowlist is tenant:list + jobs:list only", () => {
|
|
26
|
+
expect(PLATFORM_OVERVIEW_ALLOWED_QUERIES).toEqual([TenantQueries.list, JobQueries.list]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("platform queries are not tenant-allowlisted", () => {
|
|
30
|
+
expect(isOverviewQueryAllowed("tenant", TenantQueries.list)).toBe(false);
|
|
31
|
+
expect(isOverviewQueryAllowed("tenant", JobQueries.list)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { access, createRegistry } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createAuditFeature } from "../../audit/feature";
|
|
4
|
+
import { createConfigFeature } from "../../config/feature";
|
|
5
|
+
import { createJobsFeature } from "../../jobs/feature";
|
|
6
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
7
|
+
import { tierEngineFeature } from "../../tier-engine/feature";
|
|
8
|
+
import { PLATFORM_OVERVIEW_SCREEN_ID, TENANT_OVERVIEW_SCREEN_ID } from "../constants";
|
|
9
|
+
import { createAdminShellFeature } from "../feature";
|
|
10
|
+
|
|
11
|
+
const features = [
|
|
12
|
+
createConfigFeature(),
|
|
13
|
+
createTenantFeature(),
|
|
14
|
+
createAuditFeature(),
|
|
15
|
+
createJobsFeature(),
|
|
16
|
+
tierEngineFeature,
|
|
17
|
+
createAdminShellFeature(),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("overview screens boot", () => {
|
|
21
|
+
test("tenant-overview screen is access.admin", () => {
|
|
22
|
+
const registry = createRegistry(features);
|
|
23
|
+
const screen = registry.getScreen(`admin-shell:screen:${TENANT_OVERVIEW_SCREEN_ID}`);
|
|
24
|
+
expect(screen?.access).toEqual({ roles: access.admin });
|
|
25
|
+
expect(screen?.type).toBe("custom");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("platform-overview screen is SystemAdmin-only", () => {
|
|
29
|
+
const registry = createRegistry(features);
|
|
30
|
+
const screen = registry.getScreen(`admin-shell:screen:${PLATFORM_OVERVIEW_SCREEN_ID}`);
|
|
31
|
+
expect(screen?.access).toEqual({ roles: access.systemAdmin });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
export const ADMIN_SHELL_FEATURE = "admin-shell" as const;
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TENANT_WORKSPACE_ID = "tenant-admin" as const;
|
|
5
|
+
export const DEFAULT_PLATFORM_WORKSPACE_ID = "platform" as const;
|
|
6
|
+
|
|
7
|
+
/** Tenant workspace home — attention cards, query-allowlisted. */
|
|
8
|
+
export const TENANT_OVERVIEW_SCREEN_ID = "tenant-overview" as const;
|
|
9
|
+
|
|
10
|
+
/** Platform workspace home — attention cards, query-allowlisted. */
|
|
11
|
+
export const PLATFORM_OVERVIEW_SCREEN_ID = "platform-overview" as const;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// admin-shell — role-gated workspaces + provider nav for tenant vs platform operators.
|
|
2
|
+
// Screens live in owner features (tenant, audit, jobs, …); this feature only
|
|
3
|
+
// composes workspaces and cross-feature nav entries.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
access,
|
|
7
|
+
defineFeature,
|
|
8
|
+
type FeatureDefinition,
|
|
9
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
10
|
+
import {
|
|
11
|
+
ADMIN_SHELL_FEATURE,
|
|
12
|
+
DEFAULT_PLATFORM_WORKSPACE_ID,
|
|
13
|
+
DEFAULT_TENANT_WORKSPACE_ID,
|
|
14
|
+
PLATFORM_OVERVIEW_SCREEN_ID,
|
|
15
|
+
TENANT_OVERVIEW_SCREEN_ID,
|
|
16
|
+
} from "./constants";
|
|
17
|
+
import { ADMIN_SHELL_I18N } from "./i18n";
|
|
18
|
+
|
|
19
|
+
export type CreateAdminShellOptions = {
|
|
20
|
+
/** Short workspace id for tenant operators (URL segment). Default `tenant-admin`. */
|
|
21
|
+
readonly workspaceIds?: {
|
|
22
|
+
readonly tenant?: string;
|
|
23
|
+
readonly platform?: string;
|
|
24
|
+
};
|
|
25
|
+
/** Platform nav → tier-engine:screen:tier-admin. Requires tier-engine mounted. Default true. */
|
|
26
|
+
readonly includeTierAdmin?: boolean;
|
|
27
|
+
/** When false, only overview screens + nav are registered — app owns r.workspace(). Default true. */
|
|
28
|
+
readonly registerWorkspaces?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function createAdminShellFeature(options: CreateAdminShellOptions = {}): FeatureDefinition {
|
|
32
|
+
const tenantWsId = options.workspaceIds?.tenant ?? DEFAULT_TENANT_WORKSPACE_ID;
|
|
33
|
+
const platformWsId = options.workspaceIds?.platform ?? DEFAULT_PLATFORM_WORKSPACE_ID;
|
|
34
|
+
const includeTierAdmin = options.includeTierAdmin ?? true;
|
|
35
|
+
const registerWorkspaces = options.registerWorkspaces ?? true;
|
|
36
|
+
|
|
37
|
+
return defineFeature(ADMIN_SHELL_FEATURE, (r) => {
|
|
38
|
+
r.describe(
|
|
39
|
+
"Registers tenant-admin and platform-admin workspaces with provider nav into owner-feature screens (`tenant:screen:members`, `audit:screen:audit-log`, `tenant:screen:tenant-list`, `jobs:screen:job-runs`, optional `tier-engine:screen:tier-admin`). Mount after tenant, audit, and jobs; pass `workspaceIds` to match app URL conventions (e.g. Studio `d`/`s`, PublicStatus `admin`/`sysadmin`). Client: `adminShellClient()`, `tenantClient()`, `auditClient()`, `jobsClient()`, optional `tierEngineClient()`.",
|
|
40
|
+
);
|
|
41
|
+
r.uiHints({
|
|
42
|
+
displayLabel: "Admin Shell",
|
|
43
|
+
category: "operations",
|
|
44
|
+
recommended: false,
|
|
45
|
+
});
|
|
46
|
+
r.requires("tenant");
|
|
47
|
+
r.requires("audit");
|
|
48
|
+
r.requires("jobs");
|
|
49
|
+
if (includeTierAdmin) r.requires("tier-engine");
|
|
50
|
+
|
|
51
|
+
r.translations({ keys: ADMIN_SHELL_I18N });
|
|
52
|
+
|
|
53
|
+
const tenantNav = [
|
|
54
|
+
"admin-shell:nav:tenant-overview",
|
|
55
|
+
"tenant:nav:members",
|
|
56
|
+
"audit:nav:audit-log",
|
|
57
|
+
] as const;
|
|
58
|
+
const platformNav: string[] = [
|
|
59
|
+
"admin-shell:nav:platform-overview",
|
|
60
|
+
"admin-shell:nav:tenants",
|
|
61
|
+
"jobs:nav:job-runs",
|
|
62
|
+
...(includeTierAdmin ? (["admin-shell:nav:tier-admin"] as const) : []),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
r.screen({
|
|
66
|
+
id: TENANT_OVERVIEW_SCREEN_ID,
|
|
67
|
+
type: "custom",
|
|
68
|
+
renderer: { react: { __component: "TenantOverviewScreen" } },
|
|
69
|
+
access: { roles: access.admin },
|
|
70
|
+
});
|
|
71
|
+
r.nav({
|
|
72
|
+
id: "tenant-overview",
|
|
73
|
+
label: "admin-shell:nav.tenantOverview",
|
|
74
|
+
icon: "home",
|
|
75
|
+
screen: "admin-shell:screen:tenant-overview",
|
|
76
|
+
order: 1,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
r.screen({
|
|
80
|
+
id: PLATFORM_OVERVIEW_SCREEN_ID,
|
|
81
|
+
type: "custom",
|
|
82
|
+
renderer: { react: { __component: "PlatformOverviewScreen" } },
|
|
83
|
+
access: { roles: access.systemAdmin },
|
|
84
|
+
});
|
|
85
|
+
r.nav({
|
|
86
|
+
id: "platform-overview",
|
|
87
|
+
label: "admin-shell:nav.platformOverview",
|
|
88
|
+
icon: "dashboard",
|
|
89
|
+
screen: "admin-shell:screen:platform-overview",
|
|
90
|
+
order: 1,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
r.nav({
|
|
94
|
+
id: "tenants",
|
|
95
|
+
label: "admin-shell:nav.tenants",
|
|
96
|
+
icon: "building",
|
|
97
|
+
screen: "tenant:screen:tenant-list",
|
|
98
|
+
access: { roles: access.systemAdmin },
|
|
99
|
+
order: 10,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (includeTierAdmin) {
|
|
103
|
+
r.nav({
|
|
104
|
+
id: "tier-admin",
|
|
105
|
+
label: "admin-shell:nav.tierAdmin",
|
|
106
|
+
icon: "shield",
|
|
107
|
+
screen: "tier-engine:screen:tier-admin",
|
|
108
|
+
access: { roles: access.systemAdmin },
|
|
109
|
+
order: 30,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (registerWorkspaces) {
|
|
114
|
+
r.workspace({
|
|
115
|
+
id: tenantWsId,
|
|
116
|
+
label: "admin-shell:workspace.tenant",
|
|
117
|
+
icon: "users",
|
|
118
|
+
order: 1,
|
|
119
|
+
access: { roles: access.admin },
|
|
120
|
+
nav: [...tenantNav],
|
|
121
|
+
default: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
r.workspace({
|
|
125
|
+
id: platformWsId,
|
|
126
|
+
label: "admin-shell:workspace.platform",
|
|
127
|
+
icon: "shield",
|
|
128
|
+
order: 2,
|
|
129
|
+
access: { roles: access.systemAdmin },
|
|
130
|
+
nav: platformNav,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Pure-Data i18n keys for admin-shell (server r.translations + client pivot).
|
|
3
|
+
// Without these keys nav labels and workspace tabs render raw QNs in the shell.
|
|
4
|
+
|
|
5
|
+
type LocalizedString = { readonly de: string; readonly en: string };
|
|
6
|
+
|
|
7
|
+
export const ADMIN_SHELL_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
8
|
+
"screen:tenant-overview.title": { de: "Übersicht", en: "Overview" },
|
|
9
|
+
"screen:platform-overview.title": { de: "Übersicht", en: "Overview" },
|
|
10
|
+
"admin-shell:workspace.tenant": { de: "Administration", en: "Administration" },
|
|
11
|
+
"admin-shell:workspace.platform": { de: "Plattform", en: "Platform" },
|
|
12
|
+
"admin-shell:nav.tenantOverview": { de: "Übersicht", en: "Overview" },
|
|
13
|
+
"admin-shell:nav.platformOverview": { de: "Übersicht", en: "Overview" },
|
|
14
|
+
"admin-shell:nav.tenants": { de: "Mandanten", en: "Tenants" },
|
|
15
|
+
"admin-shell:nav.tierAdmin": { de: "Tier zuweisen", en: "Assign tier" },
|
|
16
|
+
"admin-shell:overview.tenantTitle": { de: "Administration", en: "Administration" },
|
|
17
|
+
"admin-shell:overview.platformTitle": { de: "Plattform", en: "Platform" },
|
|
18
|
+
"admin-shell:overview.loading": { de: "Lade…", en: "Loading…" },
|
|
19
|
+
"admin-shell:overview.pendingInvitations": {
|
|
20
|
+
de: "Ausstehende Einladungen",
|
|
21
|
+
en: "Pending invitations",
|
|
22
|
+
},
|
|
23
|
+
"admin-shell:overview.members": { de: "Mitglieder", en: "Members" },
|
|
24
|
+
"admin-shell:overview.missingConfig": {
|
|
25
|
+
de: "Fehlende Konfiguration",
|
|
26
|
+
en: "Missing configuration",
|
|
27
|
+
},
|
|
28
|
+
"admin-shell:overview.missingConfigHint": {
|
|
29
|
+
de: "Pflichtfelder in den Einstellungen prüfen",
|
|
30
|
+
en: "Check required settings",
|
|
31
|
+
},
|
|
32
|
+
"admin-shell:overview.tenants": { de: "Mandanten", en: "Tenants" },
|
|
33
|
+
"admin-shell:overview.failedJobs": { de: "Fehlgeschlagene Jobs", en: "Failed jobs" },
|
|
34
|
+
"admin-shell:overview.failedJobsHint": { de: "Job-Runs prüfen", en: "Review job runs" },
|
|
35
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ADMIN_SHELL_FEATURE,
|
|
3
|
+
DEFAULT_PLATFORM_WORKSPACE_ID,
|
|
4
|
+
DEFAULT_TENANT_WORKSPACE_ID,
|
|
5
|
+
PLATFORM_OVERVIEW_SCREEN_ID,
|
|
6
|
+
TENANT_OVERVIEW_SCREEN_ID,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
export { type CreateAdminShellOptions, createAdminShellFeature } from "./feature";
|
|
9
|
+
export { ADMIN_SHELL_I18N } from "./i18n";
|
|
10
|
+
export {
|
|
11
|
+
isOverviewQueryAllowed,
|
|
12
|
+
type OverviewWorkspaceKind,
|
|
13
|
+
overviewAllowedQueries,
|
|
14
|
+
PLATFORM_OVERVIEW_ALLOWED_QUERIES,
|
|
15
|
+
TENANT_OVERVIEW_ALLOWED_QUERIES,
|
|
16
|
+
TENANT_OVERVIEW_FORBIDDEN_QUERIES,
|
|
17
|
+
} from "./overview-allowlist";
|