@basaltkit/audit-viewer 1.2.0 → 1.3.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/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createToken, ctx, definePlugin } from '@basaltkit/core';
1
+ import { createToken, ctx, definePlugin, ensureMetadata } from '@basaltkit/core';
2
2
  import { AUDIT } from '@basaltkit/audit';
3
3
  import { route } from '@basaltkit/http';
4
4
  import { z } from 'zod';
@@ -9,7 +9,10 @@ export function auditViewerPlugin(options = {}) {
9
9
  return definePlugin({
10
10
  name: 'basalt:audit-viewer',
11
11
  register({ container }) {
12
- container.singleton(AUDIT_VIEWER, () => new AuditViewer(container.get(AUDIT), options));
12
+ // 'tenancy:active' is tenancyPlugin's marker: how a generic package
13
+ // learns the app is multi-tenant without importing @basaltkit/tenancy.
14
+ const metadata = ensureMetadata(container);
15
+ container.singleton(AUDIT_VIEWER, () => new AuditViewer(container.get(AUDIT), options, () => metadata.get('tenancy:active').length > 0));
13
16
  },
14
17
  });
15
18
  }
package/dist/viewer.d.ts CHANGED
@@ -64,10 +64,28 @@ export interface AuditViewerOptions {
64
64
  */
65
65
  export declare class AuditViewer {
66
66
  private readonly audit;
67
+ /**
68
+ * Whether the host app is multi-tenant, i.e. whether `@basaltkit/tenancy`
69
+ * is registered. `auditViewerPlugin` wires this to the container's
70
+ * `'tenancy:active'` metadata marker — a signal, not an import, so this
71
+ * generic package never depends on the opt-in SaaS layer.
72
+ *
73
+ * Defaults to `false`: a hand-built viewer behaves single-tenant.
74
+ */
75
+ private readonly tenancyActive;
67
76
  private readonly bucketMs;
68
77
  private readonly topN;
69
78
  private readonly maxScan;
70
- constructor(audit: Audit, options?: AuditViewerOptions);
79
+ constructor(audit: Audit, options?: AuditViewerOptions,
80
+ /**
81
+ * Whether the host app is multi-tenant, i.e. whether `@basaltkit/tenancy`
82
+ * is registered. `auditViewerPlugin` wires this to the container's
83
+ * `'tenancy:active'` metadata marker — a signal, not an import, so this
84
+ * generic package never depends on the opt-in SaaS layer.
85
+ *
86
+ * Defaults to `false`: a hand-built viewer behaves single-tenant.
87
+ */
88
+ tenancyActive?: () => boolean);
71
89
  page(query?: ViewerQuery): Promise<AuditPage>;
72
90
  get(id: string, tenantId?: string): Promise<AuditEntry | null>;
73
91
  stats(query?: ViewerQuery): Promise<AuditStats>;
@@ -75,5 +93,15 @@ export declare class AuditViewer {
75
93
  /** Reads at most `maxScan` rows and reports whether the trail had more. */
76
94
  private match;
77
95
  private top;
96
+ /**
97
+ * The tenant to scope a read to, or `undefined` when the app has no tenant
98
+ * dimension at all.
99
+ *
100
+ * In a multi-tenant app an unresolvable tenant is an error — an unscoped read
101
+ * would cross tenants. In a single-tenant app (no `tenancyPlugin`) there is
102
+ * nothing to scope to and nothing to cross, so the read proceeds unscoped;
103
+ * `Audit.trail()` applies the same rule one layer down and still forces the
104
+ * ambient tenant whenever one exists.
105
+ */
78
106
  private tenant;
79
107
  }
package/dist/viewer.js CHANGED
@@ -14,11 +14,22 @@ const DEFAULT_MAX_SCAN = 10_000;
14
14
  */
15
15
  export class AuditViewer {
16
16
  audit;
17
+ tenancyActive;
17
18
  bucketMs;
18
19
  topN;
19
20
  maxScan;
20
- constructor(audit, options = {}) {
21
+ constructor(audit, options = {},
22
+ /**
23
+ * Whether the host app is multi-tenant, i.e. whether `@basaltkit/tenancy`
24
+ * is registered. `auditViewerPlugin` wires this to the container's
25
+ * `'tenancy:active'` metadata marker — a signal, not an import, so this
26
+ * generic package never depends on the opt-in SaaS layer.
27
+ *
28
+ * Defaults to `false`: a hand-built viewer behaves single-tenant.
29
+ */
30
+ tenancyActive = () => false) {
21
31
  this.audit = audit;
32
+ this.tenancyActive = tenancyActive;
22
33
  this.bucketMs = options.bucketMs ?? DAY;
23
34
  this.topN = options.topN ?? 20;
24
35
  this.maxScan = options.maxScan ?? DEFAULT_MAX_SCAN;
@@ -61,7 +72,7 @@ export class AuditViewer {
61
72
  async match(query) {
62
73
  const tenantId = this.tenant(query.tenantId);
63
74
  const trail = await this.audit.trail({
64
- tenantId,
75
+ ...(tenantId !== undefined ? { tenantId } : {}),
65
76
  limit: this.maxScan,
66
77
  ...(query.event !== undefined ? { event: query.event } : {}),
67
78
  ...(query.actorId !== undefined ? { actorId: query.actorId } : {}),
@@ -74,10 +85,22 @@ export class AuditViewer {
74
85
  top(counts) {
75
86
  return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, this.topN);
76
87
  }
88
+ /**
89
+ * The tenant to scope a read to, or `undefined` when the app has no tenant
90
+ * dimension at all.
91
+ *
92
+ * In a multi-tenant app an unresolvable tenant is an error — an unscoped read
93
+ * would cross tenants. In a single-tenant app (no `tenancyPlugin`) there is
94
+ * nothing to scope to and nothing to cross, so the read proceeds unscoped;
95
+ * `Audit.trail()` applies the same rule one layer down and still forces the
96
+ * ambient tenant whenever one exists.
97
+ */
77
98
  tenant(explicit) {
78
99
  const id = explicit ?? tryCtx()?.['tenant']?.id;
79
- if (!id)
100
+ if (id)
101
+ return id;
102
+ if (this.tenancyActive())
80
103
  throw new AuditTenantRequiredError();
81
- return id;
104
+ return undefined;
82
105
  }
83
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/audit-viewer",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },
@@ -18,7 +18,7 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@basaltkit/audit": "^1.3.0",
21
+ "@basaltkit/audit": "^1.4.0",
22
22
  "@basaltkit/core": "^1.3.1",
23
23
  "@basaltkit/http": "^1.14.0"
24
24
  },