@valentinkolb/cloud 0.3.1 → 0.5.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 +18 -8
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +119 -47
- package/src/_internal/runtime-context.ts +1 -0
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +15 -25
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +4 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +4 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { sql } from "bun";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { auth, type AuthContext } from "../server/middleware/auth";
|
|
5
|
+
import { accounts } from "./accounts";
|
|
6
|
+
import { serviceAccountCredentials } from "./service-account-credentials";
|
|
7
|
+
import { serviceAccounts } from "./service-accounts";
|
|
8
|
+
|
|
9
|
+
const canUseDatabase = async () => {
|
|
10
|
+
try {
|
|
11
|
+
const [row] = await sql<{
|
|
12
|
+
users: string | null;
|
|
13
|
+
service_accounts: string | null;
|
|
14
|
+
credentials: string | null;
|
|
15
|
+
audit_events: string | null;
|
|
16
|
+
ipa_effective_groups: string | null;
|
|
17
|
+
}[]>`
|
|
18
|
+
SELECT
|
|
19
|
+
to_regclass('auth.users')::text AS users,
|
|
20
|
+
to_regclass('auth.service_accounts')::text AS service_accounts,
|
|
21
|
+
to_regclass('auth.service_account_credentials')::text AS credentials,
|
|
22
|
+
to_regclass('audit.events')::text AS audit_events,
|
|
23
|
+
to_regclass('auth.ipa_user_effective_groups')::text AS ipa_effective_groups
|
|
24
|
+
`;
|
|
25
|
+
return Boolean(row?.users && row.service_accounts && row.credentials && row.audit_events && row.ipa_effective_groups);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const insertUser = async () => {
|
|
32
|
+
const suffix = crypto.randomUUID();
|
|
33
|
+
const [row] = await sql<{ id: string }[]>`
|
|
34
|
+
INSERT INTO auth.users (uid, provider, profile, display_name, mail, given_name, sn)
|
|
35
|
+
VALUES (${`api-key-${suffix}`}, 'local', 'user', 'API Key Test', ${`api-key-${suffix}@example.test`}, 'API', 'Key')
|
|
36
|
+
RETURNING id
|
|
37
|
+
`;
|
|
38
|
+
return row!.id;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("serviceAccountCredentials", () => {
|
|
42
|
+
test("creates, authenticates, lists, and revokes user delegated API keys", async () => {
|
|
43
|
+
if (!(await canUseDatabase())) {
|
|
44
|
+
console.warn("Skipping service account credential DB test: auth/audit tables are not available.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const userId = await insertUser();
|
|
49
|
+
try {
|
|
50
|
+
const user = await accounts.users.get({ id: userId });
|
|
51
|
+
expect(user).not.toBeNull();
|
|
52
|
+
if (!user) return;
|
|
53
|
+
|
|
54
|
+
const created = await serviceAccountCredentials.createUserApiToken({
|
|
55
|
+
user,
|
|
56
|
+
name: "Test key",
|
|
57
|
+
expiresAt: null,
|
|
58
|
+
});
|
|
59
|
+
expect(created.ok).toBe(true);
|
|
60
|
+
if (!created.ok) return;
|
|
61
|
+
expect(created.data.token).toMatch(/^cld_[0-9a-f]{24}_[0-9a-f]{64}$/);
|
|
62
|
+
expect(created.data.credential.name).toBe("Test key");
|
|
63
|
+
|
|
64
|
+
const authenticated = await serviceAccountCredentials.authenticateApiToken(created.data.token);
|
|
65
|
+
expect(authenticated?.delegatedUser?.id).toBe(user.id);
|
|
66
|
+
expect(authenticated?.serviceAccount.kind).toBe("user_delegated");
|
|
67
|
+
|
|
68
|
+
const app = new Hono<AuthContext>()
|
|
69
|
+
.use(auth.requireRole("authenticated"))
|
|
70
|
+
.get("/me", (c) => c.json({
|
|
71
|
+
actorKind: c.get("actor").kind,
|
|
72
|
+
userId: c.get("user").id,
|
|
73
|
+
accessSubject: c.get("accessSubject"),
|
|
74
|
+
}));
|
|
75
|
+
const response = await app.request("/me", {
|
|
76
|
+
headers: { Authorization: `Bearer ${created.data.token}` },
|
|
77
|
+
});
|
|
78
|
+
expect(response.status).toBe(200);
|
|
79
|
+
expect(await response.json()).toEqual({
|
|
80
|
+
actorKind: "service_account",
|
|
81
|
+
userId: user.id,
|
|
82
|
+
accessSubject: { type: "user", userId: user.id },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const listed = await serviceAccountCredentials.listForDelegatedUser({ userId: user.id });
|
|
86
|
+
expect(listed.map((key) => key.id)).toContain(created.data.credential.id);
|
|
87
|
+
|
|
88
|
+
const overview = await serviceAccountCredentials.listOverview({
|
|
89
|
+
filter: { userId: user.id, serviceAccountKind: "user_delegated", credentialStatus: "active" },
|
|
90
|
+
});
|
|
91
|
+
expect(overview.items.map((key) => key.id)).toContain(created.data.credential.id);
|
|
92
|
+
expect(overview.items.find((key) => key.id === created.data.credential.id)?.owner).toMatchObject({
|
|
93
|
+
type: "user",
|
|
94
|
+
userId: user.id,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const adminRevoked = await serviceAccountCredentials.revoke({
|
|
98
|
+
credentialId: created.data.credential.id,
|
|
99
|
+
actor: user,
|
|
100
|
+
});
|
|
101
|
+
expect(adminRevoked.ok).toBe(true);
|
|
102
|
+
|
|
103
|
+
const afterAdminRevoke = await serviceAccountCredentials.authenticateApiToken(created.data.token);
|
|
104
|
+
expect(afterAdminRevoke).toBeNull();
|
|
105
|
+
|
|
106
|
+
const second = await serviceAccountCredentials.createUserApiToken({
|
|
107
|
+
user,
|
|
108
|
+
name: "Second test key",
|
|
109
|
+
expiresAt: null,
|
|
110
|
+
});
|
|
111
|
+
expect(second.ok).toBe(true);
|
|
112
|
+
if (!second.ok) return;
|
|
113
|
+
|
|
114
|
+
const revoked = await serviceAccountCredentials.revokeForDelegatedUser({
|
|
115
|
+
credentialId: second.data.credential.id,
|
|
116
|
+
user,
|
|
117
|
+
});
|
|
118
|
+
expect(revoked.ok).toBe(true);
|
|
119
|
+
|
|
120
|
+
const afterRevoke = await serviceAccountCredentials.authenticateApiToken(second.data.token);
|
|
121
|
+
expect(afterRevoke).toBeNull();
|
|
122
|
+
} finally {
|
|
123
|
+
await sql`DELETE FROM auth.users WHERE id = ${userId}::uuid`;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("creates, authenticates, lists, and revokes resource-bound API keys", async () => {
|
|
128
|
+
if (!(await canUseDatabase())) {
|
|
129
|
+
console.warn("Skipping resource service account credential DB test: auth/audit tables are not available.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const userId = await insertUser();
|
|
134
|
+
const resourceId = crypto.randomUUID();
|
|
135
|
+
let serviceAccountId: string | null = null;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const user = await accounts.users.get({ id: userId });
|
|
139
|
+
expect(user).not.toBeNull();
|
|
140
|
+
if (!user) return;
|
|
141
|
+
|
|
142
|
+
const serviceAccount = await serviceAccounts.getOrCreateResourceBound({
|
|
143
|
+
name: "Test notebook integration",
|
|
144
|
+
appId: "notebooks",
|
|
145
|
+
resourceType: "notebook",
|
|
146
|
+
resourceId,
|
|
147
|
+
createdBy: user.id,
|
|
148
|
+
});
|
|
149
|
+
expect(serviceAccount.ok).toBe(true);
|
|
150
|
+
if (!serviceAccount.ok) return;
|
|
151
|
+
serviceAccountId = serviceAccount.data.id;
|
|
152
|
+
|
|
153
|
+
const sameServiceAccount = await serviceAccounts.getOrCreateResourceBound({
|
|
154
|
+
name: "Ignored duplicate name",
|
|
155
|
+
appId: "notebooks",
|
|
156
|
+
resourceType: "notebook",
|
|
157
|
+
resourceId,
|
|
158
|
+
createdBy: user.id,
|
|
159
|
+
});
|
|
160
|
+
expect(sameServiceAccount.ok).toBe(true);
|
|
161
|
+
expect(sameServiceAccount.ok ? sameServiceAccount.data.id : null).toBe(serviceAccount.data.id);
|
|
162
|
+
|
|
163
|
+
const created = await serviceAccountCredentials.createResourceApiToken({
|
|
164
|
+
serviceAccountId: serviceAccount.data.id,
|
|
165
|
+
actor: user,
|
|
166
|
+
name: "Resource key",
|
|
167
|
+
expiresAt: null,
|
|
168
|
+
});
|
|
169
|
+
expect(created.ok).toBe(true);
|
|
170
|
+
if (!created.ok) return;
|
|
171
|
+
expect(created.data.token).toMatch(/^cld_[0-9a-f]{24}_[0-9a-f]{64}$/);
|
|
172
|
+
|
|
173
|
+
const authenticated = await serviceAccountCredentials.authenticateApiToken(created.data.token);
|
|
174
|
+
expect(authenticated?.delegatedUser).toBeNull();
|
|
175
|
+
expect(authenticated?.serviceAccount).toMatchObject({
|
|
176
|
+
kind: "resource_bound",
|
|
177
|
+
appId: "notebooks",
|
|
178
|
+
resourceType: "notebook",
|
|
179
|
+
resourceId,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const overview = await serviceAccountCredentials.listOverview({
|
|
183
|
+
filter: {
|
|
184
|
+
appId: "notebooks",
|
|
185
|
+
resourceType: "notebook",
|
|
186
|
+
resourceId,
|
|
187
|
+
serviceAccountKind: "resource_bound",
|
|
188
|
+
credentialStatus: "active",
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
expect(overview.items.map((key) => key.id)).toContain(created.data.credential.id);
|
|
192
|
+
expect(overview.items.find((key) => key.id === created.data.credential.id)?.owner).toEqual({
|
|
193
|
+
type: "resource",
|
|
194
|
+
appId: "notebooks",
|
|
195
|
+
resourceType: "notebook",
|
|
196
|
+
resourceId,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const revoked = await serviceAccountCredentials.revoke({
|
|
200
|
+
credentialId: created.data.credential.id,
|
|
201
|
+
actor: user,
|
|
202
|
+
});
|
|
203
|
+
expect(revoked.ok).toBe(true);
|
|
204
|
+
expect(await serviceAccountCredentials.authenticateApiToken(created.data.token)).toBeNull();
|
|
205
|
+
} finally {
|
|
206
|
+
if (serviceAccountId) await sql`DELETE FROM auth.service_accounts WHERE id = ${serviceAccountId}::uuid`;
|
|
207
|
+
await sql`DELETE FROM auth.users WHERE id = ${userId}::uuid`;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|