@ultimat3/admin 15.0.0 → 17.0.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 +16 -16
- package/src/audit.ts +14 -3
- package/src/resource.ts +6 -1
- package/src/search.ts +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/admin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"description": "Two dashboards: the /_x framework dev panels and the generated, AI-first app admin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,20 +32,20 @@
|
|
|
32
32
|
"test": "bun test"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ultimat3/action": "
|
|
36
|
-
"@ultimat3/ai": "
|
|
37
|
-
"@ultimat3/cache": "
|
|
38
|
-
"@ultimat3/core": "
|
|
39
|
-
"@ultimat3/db": "
|
|
40
|
-
"@ultimat3/entity": "
|
|
41
|
-
"@ultimat3/i18n": "
|
|
42
|
-
"@ultimat3/jobs": "
|
|
43
|
-
"@ultimat3/mcp": "
|
|
44
|
-
"@ultimat3/money": "
|
|
45
|
-
"@ultimat3/policy": "
|
|
46
|
-
"@ultimat3/query": "
|
|
47
|
-
"@ultimat3/render": "
|
|
48
|
-
"@ultimat3/schema": "
|
|
49
|
-
"@ultimat3/ui": "
|
|
35
|
+
"@ultimat3/action": "17.0.0",
|
|
36
|
+
"@ultimat3/ai": "17.0.0",
|
|
37
|
+
"@ultimat3/cache": "17.0.0",
|
|
38
|
+
"@ultimat3/core": "17.0.0",
|
|
39
|
+
"@ultimat3/db": "17.0.0",
|
|
40
|
+
"@ultimat3/entity": "17.0.0",
|
|
41
|
+
"@ultimat3/i18n": "17.0.0",
|
|
42
|
+
"@ultimat3/jobs": "17.0.0",
|
|
43
|
+
"@ultimat3/mcp": "17.0.0",
|
|
44
|
+
"@ultimat3/money": "17.0.0",
|
|
45
|
+
"@ultimat3/policy": "17.0.0",
|
|
46
|
+
"@ultimat3/query": "17.0.0",
|
|
47
|
+
"@ultimat3/render": "17.0.0",
|
|
48
|
+
"@ultimat3/schema": "17.0.0",
|
|
49
|
+
"@ultimat3/ui": "17.0.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/audit.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// If it isn't logged, it didn't happen — so denied and failed attempts are logged too, and
|
|
3
3
|
// there is deliberately no update or delete on this interface.
|
|
4
4
|
|
|
5
|
-
import { canonicalJson } from '@ultimat3/core';
|
|
5
|
+
import { canonicalJson, finiteCount } from '@ultimat3/core';
|
|
6
6
|
import type { AdminActor, AdminDecision } from './authz';
|
|
7
7
|
import type { AdminRow } from './registry';
|
|
8
8
|
|
|
9
9
|
export const REDACTED = '[redacted]';
|
|
10
10
|
|
|
11
|
+
/** Named once, so both refusals below say the same thing about the same call. */
|
|
12
|
+
const SUBJECT = 'memoryAuditLog';
|
|
13
|
+
|
|
11
14
|
export interface AuditFieldDiff {
|
|
12
15
|
readonly field: string;
|
|
13
16
|
readonly before: unknown;
|
|
@@ -91,7 +94,10 @@ export function auditEntry(draft: AuditDraft, id: string, at: Date): AuditEntry
|
|
|
91
94
|
export function memoryAuditLog(opts: AuditLogOptions = {}): AuditLog {
|
|
92
95
|
const now = opts.now ?? ((): Date => new Date());
|
|
93
96
|
const nextId = opts.nextId ?? ((): string => crypto.randomUUID());
|
|
94
|
-
|
|
97
|
+
// `log.length > NaN` is false for every length, so a capacity that is not a number does not make
|
|
98
|
+
// the ring bigger — it removes the ring, and this buffer then grows for the life of the process.
|
|
99
|
+
// At least 1, because a ring that keeps nothing is an audit log that records nothing.
|
|
100
|
+
const capacity = finiteCount(SUBJECT, 'capacity', opts.capacity ?? 1000, 1);
|
|
95
101
|
const sinks = opts.sinks ?? [];
|
|
96
102
|
const log: AuditEntry[] = [];
|
|
97
103
|
|
|
@@ -113,7 +119,12 @@ export function memoryAuditLog(opts: AuditLogOptions = {}): AuditLog {
|
|
|
113
119
|
(query.actorId === undefined || entry.actor.id === query.actorId),
|
|
114
120
|
);
|
|
115
121
|
const newestFirst = [...filtered].reverse();
|
|
116
|
-
|
|
122
|
+
// The opposite failure to `capacity`, from the same missing check: `slice(0, NaN)` is `[]`,
|
|
123
|
+
// so an unreadable limit answers "nothing was ever logged" and reads as a successful read.
|
|
124
|
+
// 0 stays legal — asking for none is a coherent request.
|
|
125
|
+
return query.limit === undefined
|
|
126
|
+
? newestFirst
|
|
127
|
+
: newestFirst.slice(0, finiteCount(SUBJECT, 'entries limit', query.limit));
|
|
117
128
|
},
|
|
118
129
|
};
|
|
119
130
|
}
|
package/src/resource.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// the default sort, i18n keys become labels. Every derived decision is overridable per
|
|
4
4
|
// field, but the zero-config result is the one the generator emits and the one the docs show.
|
|
5
5
|
|
|
6
|
+
import { finiteCount } from '@ultimat3/core';
|
|
6
7
|
import { type AdminColumnFacts, adminColumnsOf } from './entity-columns';
|
|
7
8
|
import {
|
|
8
9
|
AdminEntityUnknownError,
|
|
@@ -278,7 +279,11 @@ export function adminResource<Row extends AdminRow = AdminRow>(
|
|
|
278
279
|
filters: fields.filter((field) => field.filterable),
|
|
279
280
|
searchFields: fields.filter((field) => field.searchable),
|
|
280
281
|
defaultSort: opts.defaultSort ?? defaultSortOf(fields, idField),
|
|
281
|
-
|
|
282
|
+
// Refused here rather than at the first listing, because `pagination.ts` clamps with
|
|
283
|
+
// `Math.max(1, Math.min(x, 200))` and neither of those validates: a `NaN` survives both, so
|
|
284
|
+
// `fetched.length > NaN` is false, the page is never trimmed, `hasMore` is false and the repo
|
|
285
|
+
// is asked for `limit: NaN`. At least 1 — a page with no rows on it is not a page.
|
|
286
|
+
pageSize: finiteCount('adminResource', 'pageSize', opts.pageSize ?? DEFAULT_PAGE_SIZE, 1),
|
|
282
287
|
operations: opts.operations ?? ADMIN_OPERATIONS,
|
|
283
288
|
actions,
|
|
284
289
|
...(opts.repo === undefined ? {} : { repo: opts.repo }),
|
package/src/search.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// search config to drift from the entities, and no result an actor could not have opened —
|
|
3
3
|
// the same `admin:read` + `<entity>:read` pair gates the hit and the detail page it links to.
|
|
4
4
|
|
|
5
|
+
import { finiteCount } from '@ultimat3/core';
|
|
5
6
|
import { expectedQueryLoop } from '@ultimat3/db';
|
|
6
7
|
import { type AuditEntry, deniedDraft } from './audit';
|
|
7
8
|
import { denied } from './authz';
|
|
@@ -104,7 +105,16 @@ const SKIPPED_FORBIDDEN = 'admin.search.skipped.forbidden';
|
|
|
104
105
|
export async function adminSearch(input: AdminSearchInput): Promise<AdminSearchResult> {
|
|
105
106
|
const { ctx } = input;
|
|
106
107
|
const term = input.term.trim();
|
|
107
|
-
|
|
108
|
+
// Both consumers of this number fail silently on a `NaN`, in opposite directions:
|
|
109
|
+
// `hits.length >= NaN` is false, so the early return never fires and every field of every
|
|
110
|
+
// resource is queried, and `repo.list({ limit: NaN })` hands the repo a limit no `LIMIT` clause
|
|
111
|
+
// carries. At least 1 — a cap of zero is a search that is guaranteed to find nothing.
|
|
112
|
+
const limit = finiteCount(
|
|
113
|
+
'adminSearch',
|
|
114
|
+
'limitPerResource',
|
|
115
|
+
input.limitPerResource ?? DEFAULT_LIMIT_PER_RESOURCE,
|
|
116
|
+
1,
|
|
117
|
+
);
|
|
108
118
|
const searched: string[] = [];
|
|
109
119
|
const skipped: { entity: string; reason: string }[] = [];
|
|
110
120
|
const hits: AdminSearchHit[] = [];
|