@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/admin",
3
- "version": "15.0.0",
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": "15.0.0",
36
- "@ultimat3/ai": "15.0.0",
37
- "@ultimat3/cache": "15.0.0",
38
- "@ultimat3/core": "15.0.0",
39
- "@ultimat3/db": "15.0.0",
40
- "@ultimat3/entity": "15.0.0",
41
- "@ultimat3/i18n": "15.0.0",
42
- "@ultimat3/jobs": "15.0.0",
43
- "@ultimat3/mcp": "15.0.0",
44
- "@ultimat3/money": "15.0.0",
45
- "@ultimat3/policy": "15.0.0",
46
- "@ultimat3/query": "15.0.0",
47
- "@ultimat3/render": "15.0.0",
48
- "@ultimat3/schema": "15.0.0",
49
- "@ultimat3/ui": "15.0.0"
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
- const capacity = opts.capacity ?? 1000;
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
- return query.limit === undefined ? newestFirst : newestFirst.slice(0, query.limit);
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
- pageSize: opts.pageSize ?? DEFAULT_PAGE_SIZE,
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
- const limit = input.limitPerResource ?? DEFAULT_LIMIT_PER_RESOURCE;
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[] = [];