@cosmicdrift/kumiko-bundled-features 0.372.0 → 0.374.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/dist/auth-email-password/i18n.js +1 -0
- package/dist/auth-email-password/web/client-actions.d.ts +2 -0
- package/dist/auth-email-password/web/client-actions.js +23 -0
- package/dist/auth-email-password/web/client-plugin.d.ts +2 -1
- package/dist/auth-email-password/web/client-plugin.js +2 -0
- package/dist/cap-overview/handlers/tenant-caps-list.query.d.ts +9 -4
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +14 -61
- package/dist/cap-overview/i18n.js +0 -2
- package/package.json +11 -11
- package/src/auth-email-password/changes.json +6 -0
- package/src/cap-overview/changes.json +7 -0
|
@@ -37,6 +37,7 @@ export const defaultTranslations = {
|
|
|
37
37
|
"auth.errors.invalidSignupToken": "Activation link is invalid or expired. Please request a new one.",
|
|
38
38
|
"auth.errors.signupEmailAlreadyRegistered": "An account already exists for this email. Please sign in or reset your password.",
|
|
39
39
|
"auth.errors.unknownError": "Something went wrong. Please try again.",
|
|
40
|
+
"auth.errors.tooManyRequests": "Too many requests. Please wait briefly.",
|
|
40
41
|
"auth.errors.originNotAllowed": "Requests from this origin are not allowed.",
|
|
41
42
|
"auth.forgotPassword.title": "Reset password",
|
|
42
43
|
"auth.forgotPassword.intro": "Enter your email. If an account exists, we'll send you a reset link.",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Handlers for RowAction `kind: "clientAction"`: an admin screen lists users and
|
|
3
|
+
// triggers the same public mail flows the login screens use.
|
|
4
|
+
import { requestEmailVerification, requestPasswordReset, } from "./auth-client.js";
|
|
5
|
+
function failureMessageKey(failure) {
|
|
6
|
+
return failure.reason === "rate_limited"
|
|
7
|
+
? "auth.errors.tooManyRequests"
|
|
8
|
+
: "auth.errors.unknownError";
|
|
9
|
+
}
|
|
10
|
+
function emailFlowHandler(flow) {
|
|
11
|
+
return async (payload) => {
|
|
12
|
+
const email = payload["email"];
|
|
13
|
+
if (typeof email !== "string" || email === "") {
|
|
14
|
+
return { ok: false, message: "auth.errors.invalidBody" };
|
|
15
|
+
}
|
|
16
|
+
const result = await flow(email);
|
|
17
|
+
return result.ok ? { ok: true } : { ok: false, message: failureMessageKey(result.error) };
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const emailPasswordClientActions = {
|
|
21
|
+
"auth-email-password:request-password-reset": emailFlowHandler(requestPasswordReset),
|
|
22
|
+
"auth-email-password:request-email-verification": emailFlowHandler(requestEmailVerification),
|
|
23
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
1
|
+
import type { ClientActionHandler, TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
2
2
|
import type { ComponentType, ReactNode } from "react";
|
|
3
3
|
import { type AuthGateOptions, type MfaSetupComponentProps, type MfaVerifyComponentProps } from "./auth-gate.tsx";
|
|
4
4
|
import type { LoginScreenProps } from "./login-screen.tsx";
|
|
@@ -41,5 +41,6 @@ export type EmailPasswordClientFeature = {
|
|
|
41
41
|
children: ReactNode;
|
|
42
42
|
}>[];
|
|
43
43
|
readonly translations: TranslationsByLocale;
|
|
44
|
+
readonly clientActions: Readonly<Record<string, ClientActionHandler>>;
|
|
44
45
|
};
|
|
45
46
|
export declare function emailPasswordClient(options?: EmailPasswordClientOptions): EmailPasswordClientFeature;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Strings pro Locale, pro Key.
|
|
7
7
|
import { defaultTranslations, mergeTranslations } from "../i18n.js";
|
|
8
8
|
import { makeSessionAuthGate, } from "./auth-gate.js";
|
|
9
|
+
import { emailPasswordClientActions } from "./client-actions.js";
|
|
9
10
|
export function emailPasswordClient(options = {}) {
|
|
10
11
|
const translations = mergeTranslations(defaultTranslations, options.translations ?? {});
|
|
11
12
|
return {
|
|
@@ -22,5 +23,6 @@ export function emailPasswordClient(options = {}) {
|
|
|
22
23
|
}),
|
|
23
24
|
],
|
|
24
25
|
translations,
|
|
26
|
+
clientActions: emailPasswordClientActions,
|
|
25
27
|
};
|
|
26
28
|
}
|
|
@@ -8,14 +8,20 @@ type TenantCapsListRow = {
|
|
|
8
8
|
readonly [capField: string]: unknown;
|
|
9
9
|
};
|
|
10
10
|
export declare function createTenantCapsListQuery(caps: readonly CapSpec[], listCaps: readonly string[]): import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"tenant-caps:list", z.ZodObject<{
|
|
11
|
-
cursor: z.ZodOptional<z.ZodString>;
|
|
12
11
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
13
|
-
|
|
12
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
search: z.ZodOptional<z.ZodString>;
|
|
14
|
+
sort: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
billing: "billing";
|
|
16
|
+
name: "name";
|
|
17
|
+
tier: "tier";
|
|
18
|
+
}>>;
|
|
14
19
|
sortDirection: z.ZodOptional<z.ZodEnum<{
|
|
15
20
|
asc: "asc";
|
|
16
21
|
desc: "desc";
|
|
17
22
|
}>>;
|
|
18
|
-
|
|
23
|
+
totalCount: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
cursor: z.ZodOptional<z.ZodNever>;
|
|
19
25
|
filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
20
26
|
field: z.ZodString;
|
|
21
27
|
op: z.ZodEnum<{
|
|
@@ -27,6 +33,5 @@ export declare function createTenantCapsListQuery(caps: readonly CapSpec[], list
|
|
|
27
33
|
}>;
|
|
28
34
|
value: z.ZodUnknown;
|
|
29
35
|
}, z.core.$strip>>>;
|
|
30
|
-
totalCount: z.ZodOptional<z.ZodBoolean>;
|
|
31
36
|
}, z.core.$strip>, TenantCapsListRow, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
32
37
|
export {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { buildEntityTable,
|
|
2
|
-
import { definePagedQueryHandler,
|
|
1
|
+
import { buildEntityTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { definePagedQueryHandler, pagedListSchema, pageRows, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
4
|
import * as z from "zod";
|
|
6
5
|
import { subscriptionsProjectionTable } from "../../billing-foundation/index.js";
|
|
7
6
|
import { decryptTenantNames, tenantTable } from "../../tenant/index.js";
|
|
@@ -33,9 +32,6 @@ async function resolveLimitsByTenantAndCap(caps, tenants, configFor) {
|
|
|
33
32
|
}));
|
|
34
33
|
return new Map(entries);
|
|
35
34
|
}
|
|
36
|
-
function isSortableField(field) {
|
|
37
|
-
return isOneOf(SORTABLE_FIELDS, field);
|
|
38
|
-
}
|
|
39
35
|
// tier is a plain string with no defined ordering — lt/gt have no meaning
|
|
40
36
|
// on it and are rejected rather than silently treated as "no filter".
|
|
41
37
|
function assertSupportedTierFilterOp(op) {
|
|
@@ -93,16 +89,10 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
93
89
|
return definePagedQueryHandler({
|
|
94
90
|
name: "tenant-caps:list",
|
|
95
91
|
description: "Lists every tenant on the platform with its tier, billing provider/status and per-cap usage, paged, searchable by name and filterable by tier; use it as the operator-wide capacity overview.",
|
|
96
|
-
schema:
|
|
97
|
-
cursor: z.string().optional(),
|
|
98
|
-
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(50),
|
|
99
|
-
sort: z.string().optional(),
|
|
100
|
-
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
101
|
-
search: z.string().optional(),
|
|
92
|
+
schema: pagedListSchema(SORTABLE_FIELDS).extend({
|
|
102
93
|
filters: z
|
|
103
94
|
.array(z.object({ field: z.string(), op: FILTER_OP, value: z.unknown() }))
|
|
104
95
|
.optional(),
|
|
105
|
-
totalCount: z.boolean().optional(),
|
|
106
96
|
}),
|
|
107
97
|
access: { roles: ["SystemAdmin"] },
|
|
108
98
|
escapeHatch: {
|
|
@@ -122,60 +112,28 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
122
112
|
const subscriptions = await selectMany(db, subscriptionsProjectionTable, {});
|
|
123
113
|
const assignmentByTenant = new Map(assignments.map((row) => [row.tenantId, row]));
|
|
124
114
|
const subscriptionByTenant = new Map(subscriptions.map((row) => [row.tenantId, row]));
|
|
125
|
-
// Case-insensitive substring search has no `ilike` operator in the
|
|
126
|
-
// query builder (only `like`, SQL-LIKE case-sensitive) — filtered here
|
|
127
|
-
// in JS, same as tenant/team-list.query.ts's search.
|
|
128
|
-
const search = query.payload.search?.trim().toLowerCase();
|
|
129
115
|
const tierFilter = extractTierFilter(query.payload.filters ?? []);
|
|
130
116
|
const merged = tenants
|
|
131
|
-
.filter((tenant) => search === undefined || search === "" || tenant.name.toLowerCase().includes(search))
|
|
132
117
|
.map((tenant) => {
|
|
133
118
|
const assignment = assignmentByTenant.get(tenant.id);
|
|
134
119
|
return {
|
|
120
|
+
id: tenant.id,
|
|
135
121
|
tenantId: tenant.id,
|
|
136
122
|
name: tenant.name,
|
|
137
123
|
tier: assignment?.tier ?? "",
|
|
138
|
-
source: assignment?.source ?? null,
|
|
139
124
|
billing: billingLabel(subscriptionByTenant.get(tenant.id), assignment?.source ?? null),
|
|
140
125
|
};
|
|
141
126
|
})
|
|
142
127
|
.filter((row) => matchesTierFilter(row.tier, tierFilter));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
const sortDirection = query.payload.sortDirection ?? "asc";
|
|
157
|
-
const directionMultiplier = sortDirection === "asc" ? 1 : -1;
|
|
158
|
-
const sorted = [...merged].sort((a, b) => a[sortField].localeCompare(b[sortField]) * directionMultiplier);
|
|
159
|
-
// ponytail: base64-wrapped offset, not a SQL keyset — rows come from a
|
|
160
|
-
// JS merge of three tables (tenant/tier-assignment/subscription), same
|
|
161
|
-
// rationale as tenant/team-list.query.ts. Holds up to tier-engine's
|
|
162
|
-
// documented "single-pass scan" scale (a few thousand tenants); past
|
|
163
|
-
// that the fix is a real combined read-projection, not a cursor
|
|
164
|
-
// bolted onto this merge.
|
|
165
|
-
const offset = query.payload.cursor ? Number(decodeCursor(query.payload.cursor)) : 0;
|
|
166
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
167
|
-
throw new ValidationError({
|
|
168
|
-
fields: [
|
|
169
|
-
{
|
|
170
|
-
path: "cursor",
|
|
171
|
-
code: "invalid_cursor",
|
|
172
|
-
i18nKey: "cap-overview.errors.invalidCursor",
|
|
173
|
-
},
|
|
174
|
-
],
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
const limit = query.payload.limit;
|
|
178
|
-
const page = sorted.slice(offset, offset + limit);
|
|
128
|
+
// Search and paging run in memory because the rows are a JS merge of
|
|
129
|
+
// three tables (tenant/tier-assignment/subscription), same rationale as
|
|
130
|
+
// tenant/team-list.query.ts. Holds up to tier-engine's documented
|
|
131
|
+
// "single-pass scan" scale (a few thousand tenants); past that the fix
|
|
132
|
+
// is a real combined read-projection.
|
|
133
|
+
const { rows: page, total } = pageRows(merged, query.payload, {
|
|
134
|
+
searchFields: ["name"],
|
|
135
|
+
defaultSort: "name",
|
|
136
|
+
});
|
|
179
137
|
// Usage is computed only for this page's tenant ids, never for the
|
|
180
138
|
// full tenant set; caps without usageBatch still cost one query per
|
|
181
139
|
// tenant of the page, so those run in parallel.
|
|
@@ -212,12 +170,7 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
212
170
|
...capFields,
|
|
213
171
|
};
|
|
214
172
|
});
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
rows,
|
|
218
|
-
nextCursor,
|
|
219
|
-
...(query.payload.totalCount === true && { total: merged.length }),
|
|
220
|
-
};
|
|
173
|
+
return { rows, nextCursor: null, ...(total !== undefined && { total }) };
|
|
221
174
|
},
|
|
222
175
|
});
|
|
223
176
|
}
|
|
@@ -15,11 +15,9 @@ export const CAP_OVERVIEW_I18N = {
|
|
|
15
15
|
"cap-overview.errors.progressPrimitiveMissing": {
|
|
16
16
|
en: "Usage bar unavailable — Progress primitive is not registered.",
|
|
17
17
|
},
|
|
18
|
-
"cap-overview.errors.invalidCursor": { en: "The pagination cursor is invalid." },
|
|
19
18
|
"cap-overview.errors.filterFieldUnsupported": {
|
|
20
19
|
en: "Filtering by this field is not supported, or tier was filtered more than once.",
|
|
21
20
|
},
|
|
22
|
-
"cap-overview.errors.sortFieldUnsupported": { en: "Sorting by this field is not supported." },
|
|
23
21
|
"cap-overview.errors.tenantOverrideRequiresSystemAdmin": {
|
|
24
22
|
en: "Only SystemAdmin may query another tenant's data.",
|
|
25
23
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.374.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>",
|
|
@@ -536,12 +536,12 @@
|
|
|
536
536
|
}
|
|
537
537
|
},
|
|
538
538
|
"dependencies": {
|
|
539
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
540
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
541
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
542
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
543
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
544
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
539
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.374.0",
|
|
540
|
+
"@cosmicdrift/kumiko-framework": "0.374.0",
|
|
541
|
+
"@cosmicdrift/kumiko-headless": "0.374.0",
|
|
542
|
+
"@cosmicdrift/kumiko-renderer": "0.374.0",
|
|
543
|
+
"@cosmicdrift/kumiko-renderer-web": "0.374.0",
|
|
544
|
+
"@cosmicdrift/kumiko-types": "0.374.0",
|
|
545
545
|
"@mollie/api-client": "^4.5.0",
|
|
546
546
|
"@node-rs/argon2": "^2.0.2",
|
|
547
547
|
"@tiptap/core": "3.31.3",
|
|
@@ -1102,12 +1102,12 @@
|
|
|
1102
1102
|
],
|
|
1103
1103
|
"devDependencies": {
|
|
1104
1104
|
"@testing-library/user-event": "^14.6.1",
|
|
1105
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
1106
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
1107
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.
|
|
1105
|
+
"@cosmicdrift/kumiko-locale-de": "0.374.0",
|
|
1106
|
+
"@cosmicdrift/kumiko-locale-es": "0.374.0",
|
|
1107
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.374.0",
|
|
1108
1108
|
"jsqr": "^1.4.0",
|
|
1109
1109
|
"unpdf": "^1.8.1",
|
|
1110
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
1110
|
+
"@cosmicdrift/kumiko-testing": "0.374.0"
|
|
1111
1111
|
},
|
|
1112
1112
|
"peerDependencies": {
|
|
1113
1113
|
"@cosmicdrift/kumiko-testing": ">=0.366.0 <1.0.0"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.374.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Client actions for password reset and email verification",
|
|
6
|
+
"detail": "emailPasswordClient registers auth-email-password:request-password-reset and auth-email-password:request-email-verification for clientAction row actions."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.370.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.374.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "tenant-caps:list pages by offset and rejects cursor",
|
|
6
|
+
"detail": "The cap overview list showed page 1 on every page because the pager sends offset and the query only read cursor. tenant-caps:list now takes offset like the pager; a cursor in the payload is rejected with a 400 validation error and sort is an enum of the sortable fields. The unused cap-overview.errors.invalidCursor and sortFieldUnsupported keys are gone.",
|
|
7
|
+
"migration": "Callers of cap-overview:query:tenant-caps:list must send offset instead of cursor, and only a sortable field name as sort. Apps that override the removed i18n keys cap-overview.errors.invalidCursor or cap-overview.errors.sortFieldUnsupported can delete them. No caller in the kumiko-enterprise, kumiko-platform, kumiko-studio or app repos sends a cursor or defines these keys."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.365.0",
|
|
4
11
|
"type": "fix",
|