@usebetterdev/audit-core 0.7.0 → 0.8.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/index.cjs +184 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -8
- package/dist/index.d.ts +20 -8
- package/dist/index.js +184 -56
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -53,6 +53,8 @@ interface AuditQueryResult {
|
|
|
53
53
|
entries: AuditLog[];
|
|
54
54
|
/** If present, more results are available. Pass to `.after()` for the next page. */
|
|
55
55
|
nextCursor?: string;
|
|
56
|
+
/** If present, more results are available in the reverse direction. */
|
|
57
|
+
prevCursor?: string;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Callback that executes a query spec against the adapter. */
|
|
@@ -170,12 +172,6 @@ interface AuditContext {
|
|
|
170
172
|
/** Arbitrary key-value metadata. Redaction rules do not apply to metadata. */
|
|
171
173
|
metadata?: Record<string, unknown>;
|
|
172
174
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Database adapter provided by ORM packages (e.g. drizzle, prisma).
|
|
175
|
-
* Passed as the `database` field in `BetterAuditConfig`.
|
|
176
|
-
*
|
|
177
|
-
* `writeLog` is required. `queryLogs` is optional — only needed when `query()` is used.
|
|
178
|
-
*/
|
|
179
175
|
/** Aggregated statistics returned by `AuditApi.getStats()`. */
|
|
180
176
|
interface AuditStats {
|
|
181
177
|
totalLogs: number;
|
|
@@ -195,12 +191,19 @@ interface AuditStats {
|
|
|
195
191
|
operationBreakdown: Record<string, number>;
|
|
196
192
|
severityBreakdown: Record<string, number>;
|
|
197
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Database adapter provided by ORM packages (e.g. drizzle, prisma).
|
|
196
|
+
* Passed as the `database` field in `BetterAuditConfig`.
|
|
197
|
+
*
|
|
198
|
+
* `writeLog` is required. `queryLogs` is optional — only needed when `query()` is used.
|
|
199
|
+
*/
|
|
198
200
|
interface AuditDatabaseAdapter {
|
|
199
201
|
writeLog(log: AuditLog): Promise<void>;
|
|
200
202
|
queryLogs?(spec: AuditQuerySpec): Promise<AuditQueryResult>;
|
|
201
203
|
getLogById?(id: string): Promise<AuditLog | null>;
|
|
202
204
|
getStats?(options?: {
|
|
203
205
|
since?: Date;
|
|
206
|
+
until?: Date;
|
|
204
207
|
}): Promise<AuditStats>;
|
|
205
208
|
purgeLogs?(options: {
|
|
206
209
|
before: Date;
|
|
@@ -414,7 +417,7 @@ declare function normalizeInput(operation: AuditOperation, before: Record<string
|
|
|
414
417
|
* ORM adapters translate this into their own migration format.
|
|
415
418
|
* Core never runs SQL — this is a declarative data structure only.
|
|
416
419
|
*/
|
|
417
|
-
type ColumnType = "uuid" | "timestamptz" | "text" | "jsonb" | "boolean";
|
|
420
|
+
type ColumnType = "uuid" | "timestamptz" | "text" | "jsonb" | "boolean" | "integer";
|
|
418
421
|
interface ColumnDefinition {
|
|
419
422
|
type: ColumnType;
|
|
420
423
|
nullable: boolean;
|
|
@@ -596,6 +599,7 @@ declare class EnrichmentRegistry {
|
|
|
596
599
|
/** Flat console-friendly query filters. Single-value fields translated to multi-value internal filters. */
|
|
597
600
|
interface ConsoleQueryFilters {
|
|
598
601
|
tableName?: string;
|
|
602
|
+
recordId?: string;
|
|
599
603
|
operation?: string;
|
|
600
604
|
actorId?: string;
|
|
601
605
|
severity?: string;
|
|
@@ -605,6 +609,10 @@ interface ConsoleQueryFilters {
|
|
|
605
609
|
search?: string;
|
|
606
610
|
limit?: number;
|
|
607
611
|
cursor?: string;
|
|
612
|
+
/** Pagination direction: "after" fetches older entries (default), "before" fetches newer entries. */
|
|
613
|
+
direction?: "after" | "before";
|
|
614
|
+
/** Sort order: "asc" (oldest first) or "desc" (newest first). Overrides direction-based sort when both present. */
|
|
615
|
+
order?: "asc" | "desc";
|
|
608
616
|
}
|
|
609
617
|
/** Serializable summary of an enrichment config (function fields stripped). */
|
|
610
618
|
interface EnrichmentSummary {
|
|
@@ -617,9 +625,10 @@ interface EnrichmentSummary {
|
|
|
617
625
|
redact?: string[];
|
|
618
626
|
include?: string[];
|
|
619
627
|
}
|
|
620
|
-
/** Query result extended with
|
|
628
|
+
/** Query result extended with convenience pagination flags. */
|
|
621
629
|
interface ConsoleQueryResult extends AuditQueryResult {
|
|
622
630
|
hasNextPage: boolean;
|
|
631
|
+
hasPrevPage: boolean;
|
|
623
632
|
}
|
|
624
633
|
/**
|
|
625
634
|
* High-level API consumed by console endpoints.
|
|
@@ -630,11 +639,14 @@ interface ConsoleQueryResult extends AuditQueryResult {
|
|
|
630
639
|
interface AuditApi {
|
|
631
640
|
/** Query audit log entries with optional flat filters and cursor-based pagination. */
|
|
632
641
|
queryLogs(filters?: ConsoleQueryFilters): Promise<ConsoleQueryResult>;
|
|
642
|
+
/** Query audit log entries centered around a specific log ID. */
|
|
643
|
+
queryLogsAround(anchorId: string, filters?: ConsoleQueryFilters): Promise<ConsoleQueryResult>;
|
|
633
644
|
/** Retrieve a single audit log entry by its ID. Returns `null` when not found. */
|
|
634
645
|
getLog(id: string): Promise<AuditLog | null>;
|
|
635
646
|
/** Get aggregated audit statistics. Requires adapter.getStats. */
|
|
636
647
|
getStats(options?: {
|
|
637
648
|
since?: Date;
|
|
649
|
+
until?: Date;
|
|
638
650
|
}): Promise<AuditStats>;
|
|
639
651
|
/** Get serializable summaries of all registered enrichments. */
|
|
640
652
|
getEnrichments(): EnrichmentSummary[];
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ interface AuditQueryResult {
|
|
|
53
53
|
entries: AuditLog[];
|
|
54
54
|
/** If present, more results are available. Pass to `.after()` for the next page. */
|
|
55
55
|
nextCursor?: string;
|
|
56
|
+
/** If present, more results are available in the reverse direction. */
|
|
57
|
+
prevCursor?: string;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/** Callback that executes a query spec against the adapter. */
|
|
@@ -170,12 +172,6 @@ interface AuditContext {
|
|
|
170
172
|
/** Arbitrary key-value metadata. Redaction rules do not apply to metadata. */
|
|
171
173
|
metadata?: Record<string, unknown>;
|
|
172
174
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Database adapter provided by ORM packages (e.g. drizzle, prisma).
|
|
175
|
-
* Passed as the `database` field in `BetterAuditConfig`.
|
|
176
|
-
*
|
|
177
|
-
* `writeLog` is required. `queryLogs` is optional — only needed when `query()` is used.
|
|
178
|
-
*/
|
|
179
175
|
/** Aggregated statistics returned by `AuditApi.getStats()`. */
|
|
180
176
|
interface AuditStats {
|
|
181
177
|
totalLogs: number;
|
|
@@ -195,12 +191,19 @@ interface AuditStats {
|
|
|
195
191
|
operationBreakdown: Record<string, number>;
|
|
196
192
|
severityBreakdown: Record<string, number>;
|
|
197
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Database adapter provided by ORM packages (e.g. drizzle, prisma).
|
|
196
|
+
* Passed as the `database` field in `BetterAuditConfig`.
|
|
197
|
+
*
|
|
198
|
+
* `writeLog` is required. `queryLogs` is optional — only needed when `query()` is used.
|
|
199
|
+
*/
|
|
198
200
|
interface AuditDatabaseAdapter {
|
|
199
201
|
writeLog(log: AuditLog): Promise<void>;
|
|
200
202
|
queryLogs?(spec: AuditQuerySpec): Promise<AuditQueryResult>;
|
|
201
203
|
getLogById?(id: string): Promise<AuditLog | null>;
|
|
202
204
|
getStats?(options?: {
|
|
203
205
|
since?: Date;
|
|
206
|
+
until?: Date;
|
|
204
207
|
}): Promise<AuditStats>;
|
|
205
208
|
purgeLogs?(options: {
|
|
206
209
|
before: Date;
|
|
@@ -414,7 +417,7 @@ declare function normalizeInput(operation: AuditOperation, before: Record<string
|
|
|
414
417
|
* ORM adapters translate this into their own migration format.
|
|
415
418
|
* Core never runs SQL — this is a declarative data structure only.
|
|
416
419
|
*/
|
|
417
|
-
type ColumnType = "uuid" | "timestamptz" | "text" | "jsonb" | "boolean";
|
|
420
|
+
type ColumnType = "uuid" | "timestamptz" | "text" | "jsonb" | "boolean" | "integer";
|
|
418
421
|
interface ColumnDefinition {
|
|
419
422
|
type: ColumnType;
|
|
420
423
|
nullable: boolean;
|
|
@@ -596,6 +599,7 @@ declare class EnrichmentRegistry {
|
|
|
596
599
|
/** Flat console-friendly query filters. Single-value fields translated to multi-value internal filters. */
|
|
597
600
|
interface ConsoleQueryFilters {
|
|
598
601
|
tableName?: string;
|
|
602
|
+
recordId?: string;
|
|
599
603
|
operation?: string;
|
|
600
604
|
actorId?: string;
|
|
601
605
|
severity?: string;
|
|
@@ -605,6 +609,10 @@ interface ConsoleQueryFilters {
|
|
|
605
609
|
search?: string;
|
|
606
610
|
limit?: number;
|
|
607
611
|
cursor?: string;
|
|
612
|
+
/** Pagination direction: "after" fetches older entries (default), "before" fetches newer entries. */
|
|
613
|
+
direction?: "after" | "before";
|
|
614
|
+
/** Sort order: "asc" (oldest first) or "desc" (newest first). Overrides direction-based sort when both present. */
|
|
615
|
+
order?: "asc" | "desc";
|
|
608
616
|
}
|
|
609
617
|
/** Serializable summary of an enrichment config (function fields stripped). */
|
|
610
618
|
interface EnrichmentSummary {
|
|
@@ -617,9 +625,10 @@ interface EnrichmentSummary {
|
|
|
617
625
|
redact?: string[];
|
|
618
626
|
include?: string[];
|
|
619
627
|
}
|
|
620
|
-
/** Query result extended with
|
|
628
|
+
/** Query result extended with convenience pagination flags. */
|
|
621
629
|
interface ConsoleQueryResult extends AuditQueryResult {
|
|
622
630
|
hasNextPage: boolean;
|
|
631
|
+
hasPrevPage: boolean;
|
|
623
632
|
}
|
|
624
633
|
/**
|
|
625
634
|
* High-level API consumed by console endpoints.
|
|
@@ -630,11 +639,14 @@ interface ConsoleQueryResult extends AuditQueryResult {
|
|
|
630
639
|
interface AuditApi {
|
|
631
640
|
/** Query audit log entries with optional flat filters and cursor-based pagination. */
|
|
632
641
|
queryLogs(filters?: ConsoleQueryFilters): Promise<ConsoleQueryResult>;
|
|
642
|
+
/** Query audit log entries centered around a specific log ID. */
|
|
643
|
+
queryLogsAround(anchorId: string, filters?: ConsoleQueryFilters): Promise<ConsoleQueryResult>;
|
|
633
644
|
/** Retrieve a single audit log entry by its ID. Returns `null` when not found. */
|
|
634
645
|
getLog(id: string): Promise<AuditLog | null>;
|
|
635
646
|
/** Get aggregated audit statistics. Requires adapter.getStats. */
|
|
636
647
|
getStats(options?: {
|
|
637
648
|
since?: Date;
|
|
649
|
+
until?: Date;
|
|
638
650
|
}): Promise<AuditStats>;
|
|
639
651
|
/** Get serializable summaries of all registered enrichments. */
|
|
640
652
|
getEnrichments(): EnrichmentSummary[];
|
package/dist/index.js
CHANGED
|
@@ -725,8 +725,41 @@ async function runExport(executor, options) {
|
|
|
725
725
|
return { rowCount };
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
// src/cursor.ts
|
|
729
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
730
|
+
function encodeCursor(timestamp, id) {
|
|
731
|
+
const payload = JSON.stringify({ t: timestamp.toISOString(), i: id });
|
|
732
|
+
return btoa(payload);
|
|
733
|
+
}
|
|
734
|
+
function decodeCursor(cursor) {
|
|
735
|
+
let parsed;
|
|
736
|
+
try {
|
|
737
|
+
parsed = JSON.parse(atob(cursor));
|
|
738
|
+
} catch {
|
|
739
|
+
throw new Error("Invalid cursor: failed to decode");
|
|
740
|
+
}
|
|
741
|
+
if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
|
|
742
|
+
throw new Error("Invalid cursor: missing required fields");
|
|
743
|
+
}
|
|
744
|
+
const record = parsed;
|
|
745
|
+
const t = record["t"];
|
|
746
|
+
const i = record["i"];
|
|
747
|
+
if (typeof t !== "string" || typeof i !== "string") {
|
|
748
|
+
throw new Error("Invalid cursor: fields must be strings");
|
|
749
|
+
}
|
|
750
|
+
const timestamp = new Date(t);
|
|
751
|
+
if (isNaN(timestamp.getTime())) {
|
|
752
|
+
throw new Error("Invalid cursor: invalid timestamp");
|
|
753
|
+
}
|
|
754
|
+
if (!UUID_PATTERN.test(i)) {
|
|
755
|
+
throw new Error("Invalid cursor: id must be a valid UUID");
|
|
756
|
+
}
|
|
757
|
+
return { timestamp, id: i };
|
|
758
|
+
}
|
|
759
|
+
|
|
728
760
|
// src/audit-api.ts
|
|
729
|
-
var
|
|
761
|
+
var SEVERITY_VALUES = ["low", "medium", "high", "critical"];
|
|
762
|
+
var VALID_SEVERITIES = new Set(SEVERITY_VALUES);
|
|
730
763
|
var VALID_OPERATIONS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE"]);
|
|
731
764
|
function toTimeFilter(date) {
|
|
732
765
|
return { date };
|
|
@@ -738,18 +771,23 @@ function buildQuerySpec(filters, effectiveLimit) {
|
|
|
738
771
|
limit
|
|
739
772
|
};
|
|
740
773
|
if (filters.tableName !== void 0) {
|
|
741
|
-
spec.filters.resource = { tableName: filters.tableName };
|
|
774
|
+
spec.filters.resource = filters.recordId !== void 0 ? { tableName: filters.tableName, recordId: filters.recordId } : { tableName: filters.tableName };
|
|
742
775
|
}
|
|
743
776
|
if (filters.actorId !== void 0) {
|
|
744
|
-
|
|
777
|
+
const raw = filters.actorId.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
778
|
+
spec.filters.actorIds = [...new Set(raw)];
|
|
745
779
|
}
|
|
746
780
|
if (filters.severity !== void 0) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
)
|
|
781
|
+
const raw = filters.severity.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
782
|
+
const unique = [...new Set(raw)];
|
|
783
|
+
for (const value of unique) {
|
|
784
|
+
if (!VALID_SEVERITIES.has(value)) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`Invalid severity "${value}". Must be one of: low, medium, high, critical`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
751
789
|
}
|
|
752
|
-
spec.filters.severities =
|
|
790
|
+
spec.filters.severities = unique.filter((v) => VALID_SEVERITIES.has(v));
|
|
753
791
|
}
|
|
754
792
|
if (filters.compliance !== void 0) {
|
|
755
793
|
spec.filters.compliance = [filters.compliance];
|
|
@@ -775,6 +813,12 @@ function buildQuerySpec(filters, effectiveLimit) {
|
|
|
775
813
|
if (filters.cursor !== void 0) {
|
|
776
814
|
spec.cursor = filters.cursor;
|
|
777
815
|
}
|
|
816
|
+
if (filters.direction === "before") {
|
|
817
|
+
spec.sortOrder = "asc";
|
|
818
|
+
}
|
|
819
|
+
if (filters.order !== void 0) {
|
|
820
|
+
spec.sortOrder = filters.order;
|
|
821
|
+
}
|
|
778
822
|
return spec;
|
|
779
823
|
}
|
|
780
824
|
function createAuditApi(adapter, registry, maxQueryLimit) {
|
|
@@ -813,12 +857,66 @@ function createAuditApi(adapter, registry, maxQueryLimit) {
|
|
|
813
857
|
}
|
|
814
858
|
async function queryLogs(filters) {
|
|
815
859
|
const queryFn = requireQueryLogs();
|
|
816
|
-
const
|
|
860
|
+
const resolved = filters ?? {};
|
|
861
|
+
const spec = buildQuerySpec(resolved, effectiveLimit);
|
|
817
862
|
const result = await queryFn(spec);
|
|
863
|
+
if (resolved.direction === "before") {
|
|
864
|
+
const entries = [...result.entries].reverse();
|
|
865
|
+
const lastEntry = entries[entries.length - 1];
|
|
866
|
+
return {
|
|
867
|
+
entries,
|
|
868
|
+
// Adapter's "next" in ASC = newer entries = our prev
|
|
869
|
+
...result.nextCursor !== void 0 && { prevCursor: result.nextCursor },
|
|
870
|
+
hasPrevPage: result.nextCursor !== void 0,
|
|
871
|
+
// Since we started from a cursor, there are older entries behind it
|
|
872
|
+
...resolved.cursor !== void 0 && lastEntry !== void 0 && { nextCursor: encodeCursor(lastEntry.timestamp, lastEntry.id) },
|
|
873
|
+
hasNextPage: resolved.cursor !== void 0
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
const firstEntry = result.entries[0];
|
|
877
|
+
const prevCursor = resolved.cursor !== void 0 && firstEntry !== void 0 ? encodeCursor(firstEntry.timestamp, firstEntry.id) : void 0;
|
|
818
878
|
return {
|
|
819
879
|
entries: result.entries,
|
|
820
880
|
...result.nextCursor !== void 0 && { nextCursor: result.nextCursor },
|
|
821
|
-
hasNextPage: result.nextCursor !== void 0
|
|
881
|
+
hasNextPage: result.nextCursor !== void 0,
|
|
882
|
+
hasPrevPage: resolved.cursor !== void 0,
|
|
883
|
+
...prevCursor !== void 0 && { prevCursor }
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async function queryLogsAround(anchorId, filters) {
|
|
887
|
+
const queryFn = requireQueryLogs();
|
|
888
|
+
const getLogFn = requireGetLogById();
|
|
889
|
+
const anchor = await getLogFn(anchorId);
|
|
890
|
+
if (anchor === null) {
|
|
891
|
+
return { entries: [], hasNextPage: false, hasPrevPage: false };
|
|
892
|
+
}
|
|
893
|
+
const resolved = filters ?? {};
|
|
894
|
+
const limit = Math.min(resolved.limit ?? effectiveLimit, effectiveLimit);
|
|
895
|
+
const remaining = Math.max(limit - 1, 0);
|
|
896
|
+
const olderCount = Math.ceil(remaining / 2);
|
|
897
|
+
const newerCount = Math.floor(remaining / 2);
|
|
898
|
+
const anchorCursor = encodeCursor(anchor.timestamp, anchor.id);
|
|
899
|
+
const { direction: _dir, ...baseFilters } = resolved;
|
|
900
|
+
const olderSpec = buildQuerySpec({ ...baseFilters, cursor: anchorCursor, limit: olderCount }, effectiveLimit);
|
|
901
|
+
olderSpec.sortOrder = "desc";
|
|
902
|
+
const newerSpec = buildQuerySpec({ ...baseFilters, cursor: anchorCursor, limit: newerCount }, effectiveLimit);
|
|
903
|
+
newerSpec.sortOrder = "asc";
|
|
904
|
+
const [olderResult, newerResult] = await Promise.all([
|
|
905
|
+
queryFn(olderSpec),
|
|
906
|
+
queryFn(newerSpec)
|
|
907
|
+
]);
|
|
908
|
+
const newerEntries = [...newerResult.entries].reverse();
|
|
909
|
+
const entries = [...newerEntries, anchor, ...olderResult.entries];
|
|
910
|
+
const hasNextPage = olderResult.nextCursor !== void 0;
|
|
911
|
+
const hasPrevPage = newerResult.nextCursor !== void 0;
|
|
912
|
+
const oldest = entries[entries.length - 1];
|
|
913
|
+
const newest = entries[0];
|
|
914
|
+
return {
|
|
915
|
+
entries,
|
|
916
|
+
hasNextPage,
|
|
917
|
+
hasPrevPage,
|
|
918
|
+
...hasNextPage && oldest !== void 0 && { nextCursor: encodeCursor(oldest.timestamp, oldest.id) },
|
|
919
|
+
...hasPrevPage && newest !== void 0 && { prevCursor: encodeCursor(newest.timestamp, newest.id) }
|
|
822
920
|
};
|
|
823
921
|
}
|
|
824
922
|
async function getLog(id) {
|
|
@@ -863,7 +961,7 @@ function createAuditApi(adapter, registry, maxQueryLimit) {
|
|
|
863
961
|
}
|
|
864
962
|
async function exportLogs(filters, format) {
|
|
865
963
|
const queryFn = requireQueryLogs();
|
|
866
|
-
const exportFormat = format ?? "
|
|
964
|
+
const exportFormat = format ?? "csv";
|
|
867
965
|
const spec = buildQuerySpec(filters ?? {}, effectiveLimit);
|
|
868
966
|
const queryBuilder = new AuditQueryBuilder(
|
|
869
967
|
(s) => queryFn(s),
|
|
@@ -889,7 +987,7 @@ function createAuditApi(adapter, registry, maxQueryLimit) {
|
|
|
889
987
|
const purgeFn = requirePurgeLogs();
|
|
890
988
|
return purgeFn(options);
|
|
891
989
|
}
|
|
892
|
-
return { queryLogs, getLog, getStats, getEnrichments, exportLogs, purgeLogs };
|
|
990
|
+
return { queryLogs, queryLogsAround, getLog, getStats, getEnrichments, exportLogs, purgeLogs };
|
|
893
991
|
}
|
|
894
992
|
|
|
895
993
|
// ../../shared/console-utils/src/index.ts
|
|
@@ -923,7 +1021,7 @@ function exceedsMaxLength(value) {
|
|
|
923
1021
|
return value !== void 0 && value.length > MAX_PARAM_LENGTH;
|
|
924
1022
|
}
|
|
925
1023
|
function hasLongQueryParam(query) {
|
|
926
|
-
return exceedsMaxLength(query.tableName) || exceedsMaxLength(query.actorId) || exceedsMaxLength(query.cursor) || exceedsMaxLength(query.operation) || exceedsMaxLength(query.severity) || exceedsMaxLength(query.compliance) || exceedsMaxLength(query.search);
|
|
1024
|
+
return exceedsMaxLength(query.tableName) || exceedsMaxLength(query.recordId) || exceedsMaxLength(query.actorId) || exceedsMaxLength(query.cursor) || exceedsMaxLength(query.operation) || exceedsMaxLength(query.severity) || exceedsMaxLength(query.compliance) || exceedsMaxLength(query.search) || exceedsMaxLength(query.around) || exceedsMaxLength(query.direction) || exceedsMaxLength(query.order);
|
|
927
1025
|
}
|
|
928
1026
|
function parseConsoleQueryFilters(query) {
|
|
929
1027
|
const filters = {};
|
|
@@ -937,6 +1035,12 @@ function parseConsoleQueryFilters(query) {
|
|
|
937
1035
|
if (query.tableName !== void 0) {
|
|
938
1036
|
filters.tableName = query.tableName;
|
|
939
1037
|
}
|
|
1038
|
+
if (query.recordId !== void 0) {
|
|
1039
|
+
if (query.tableName === void 0) {
|
|
1040
|
+
return { error: "'recordId' requires 'tableName'" };
|
|
1041
|
+
}
|
|
1042
|
+
filters.recordId = query.recordId;
|
|
1043
|
+
}
|
|
940
1044
|
if (query.operation !== void 0) {
|
|
941
1045
|
filters.operation = query.operation;
|
|
942
1046
|
}
|
|
@@ -955,6 +1059,18 @@ function parseConsoleQueryFilters(query) {
|
|
|
955
1059
|
if (query.cursor !== void 0) {
|
|
956
1060
|
filters.cursor = query.cursor;
|
|
957
1061
|
}
|
|
1062
|
+
if (query.direction !== void 0) {
|
|
1063
|
+
if (query.direction !== "after" && query.direction !== "before") {
|
|
1064
|
+
return { error: "Invalid 'direction': must be 'after' or 'before'" };
|
|
1065
|
+
}
|
|
1066
|
+
filters.direction = query.direction;
|
|
1067
|
+
}
|
|
1068
|
+
if (query.order !== void 0) {
|
|
1069
|
+
if (query.order !== "asc" && query.order !== "desc") {
|
|
1070
|
+
return { error: "Invalid 'order': must be 'asc' or 'desc'" };
|
|
1071
|
+
}
|
|
1072
|
+
filters.order = query.order;
|
|
1073
|
+
}
|
|
958
1074
|
if (query.since !== void 0) {
|
|
959
1075
|
const since = parseIsoDate(query.since);
|
|
960
1076
|
if (since === void 0) {
|
|
@@ -969,6 +1085,9 @@ function parseConsoleQueryFilters(query) {
|
|
|
969
1085
|
}
|
|
970
1086
|
filters.until = until;
|
|
971
1087
|
}
|
|
1088
|
+
if (filters.since !== void 0 && filters.until !== void 0 && filters.since >= filters.until) {
|
|
1089
|
+
return { error: "'since' must be before 'until'" };
|
|
1090
|
+
}
|
|
972
1091
|
return { filters };
|
|
973
1092
|
}
|
|
974
1093
|
function serializeLog(log) {
|
|
@@ -985,14 +1104,22 @@ function createAuditConsoleEndpoints(api) {
|
|
|
985
1104
|
requiredPermission: "read",
|
|
986
1105
|
async handler(request) {
|
|
987
1106
|
try {
|
|
988
|
-
|
|
1107
|
+
const query = request.query;
|
|
1108
|
+
if (hasLongQueryParam(query)) {
|
|
989
1109
|
return { status: 400, body: { error: "Query parameter exceeds maximum length" } };
|
|
990
1110
|
}
|
|
991
|
-
const parsed = parseConsoleQueryFilters(
|
|
1111
|
+
const parsed = parseConsoleQueryFilters(query);
|
|
992
1112
|
if ("error" in parsed) {
|
|
993
1113
|
return { status: 400, body: { error: parsed.error } };
|
|
994
1114
|
}
|
|
995
|
-
|
|
1115
|
+
if (parsed.filters.order !== void 0 && parsed.filters.direction !== void 0) {
|
|
1116
|
+
return { status: 400, body: { error: "'order' cannot be combined with 'direction'" } };
|
|
1117
|
+
}
|
|
1118
|
+
const around = query.around;
|
|
1119
|
+
if (around !== void 0 && (parsed.filters.cursor !== void 0 || parsed.filters.direction !== void 0 || parsed.filters.order !== void 0)) {
|
|
1120
|
+
return { status: 400, body: { error: "'around' cannot be combined with 'cursor', 'direction', or 'order'" } };
|
|
1121
|
+
}
|
|
1122
|
+
const result = around !== void 0 ? await api.queryLogsAround(around, parsed.filters) : await api.queryLogs(parsed.filters);
|
|
996
1123
|
const body = {
|
|
997
1124
|
entries: result.entries.map(serializeLog),
|
|
998
1125
|
hasNextPage: result.hasNextPage
|
|
@@ -1000,6 +1127,12 @@ function createAuditConsoleEndpoints(api) {
|
|
|
1000
1127
|
if (result.nextCursor !== void 0) {
|
|
1001
1128
|
body.nextCursor = result.nextCursor;
|
|
1002
1129
|
}
|
|
1130
|
+
if (result.prevCursor !== void 0) {
|
|
1131
|
+
body.prevCursor = result.prevCursor;
|
|
1132
|
+
}
|
|
1133
|
+
if (result.hasPrevPage) {
|
|
1134
|
+
body.hasPrevPage = result.hasPrevPage;
|
|
1135
|
+
}
|
|
1003
1136
|
return { status: 200, body };
|
|
1004
1137
|
} catch {
|
|
1005
1138
|
return { status: 500, body: { error: "Internal server error" } };
|
|
@@ -1032,14 +1165,28 @@ function createAuditConsoleEndpoints(api) {
|
|
|
1032
1165
|
requiredPermission: "read",
|
|
1033
1166
|
async handler(request) {
|
|
1034
1167
|
try {
|
|
1168
|
+
const query = request.query;
|
|
1169
|
+
if (exceedsMaxLength(query.since) || exceedsMaxLength(query.until)) {
|
|
1170
|
+
return { status: 400, body: { error: "Query parameter exceeds maximum length" } };
|
|
1171
|
+
}
|
|
1035
1172
|
const options = {};
|
|
1036
|
-
if (
|
|
1037
|
-
const since = parseIsoDate(
|
|
1173
|
+
if (query.since !== void 0) {
|
|
1174
|
+
const since = parseIsoDate(query.since);
|
|
1038
1175
|
if (since === void 0) {
|
|
1039
1176
|
return { status: 400, body: { error: "Invalid 'since': must be an ISO-8601 date" } };
|
|
1040
1177
|
}
|
|
1041
1178
|
options.since = since;
|
|
1042
1179
|
}
|
|
1180
|
+
if (query.until !== void 0) {
|
|
1181
|
+
const until = parseIsoDate(query.until);
|
|
1182
|
+
if (until === void 0) {
|
|
1183
|
+
return { status: 400, body: { error: "Invalid 'until': must be an ISO-8601 date" } };
|
|
1184
|
+
}
|
|
1185
|
+
options.until = until;
|
|
1186
|
+
}
|
|
1187
|
+
if (options.since !== void 0 && options.until !== void 0 && options.since >= options.until) {
|
|
1188
|
+
return { status: 400, body: { error: "'since' must be before 'until'" } };
|
|
1189
|
+
}
|
|
1043
1190
|
const stats = await api.getStats(options);
|
|
1044
1191
|
return { status: 200, body: stats };
|
|
1045
1192
|
} catch {
|
|
@@ -1066,20 +1213,33 @@ function createAuditConsoleEndpoints(api) {
|
|
|
1066
1213
|
requiredPermission: "read",
|
|
1067
1214
|
async handler(request) {
|
|
1068
1215
|
try {
|
|
1069
|
-
const
|
|
1216
|
+
const query = request.query;
|
|
1217
|
+
const format = query.format;
|
|
1070
1218
|
if (format !== void 0 && format !== "csv" && format !== "json") {
|
|
1071
1219
|
return { status: 400, body: { error: "Invalid format. Must be 'csv' or 'json'" } };
|
|
1072
1220
|
}
|
|
1073
|
-
if (hasLongQueryParam(
|
|
1221
|
+
if (hasLongQueryParam(query)) {
|
|
1074
1222
|
return { status: 400, body: { error: "Query parameter exceeds maximum length" } };
|
|
1075
1223
|
}
|
|
1076
|
-
const parsed = parseConsoleQueryFilters(
|
|
1224
|
+
const parsed = parseConsoleQueryFilters(query);
|
|
1077
1225
|
if ("error" in parsed) {
|
|
1078
1226
|
return { status: 400, body: { error: parsed.error } };
|
|
1079
1227
|
}
|
|
1080
|
-
const
|
|
1081
|
-
const data = await api.exportLogs(parsed.filters,
|
|
1082
|
-
|
|
1228
|
+
const effectiveFormat = format ?? "csv";
|
|
1229
|
+
const data = await api.exportLogs(parsed.filters, effectiveFormat);
|
|
1230
|
+
const contentType = effectiveFormat === "csv" ? "text/csv; charset=utf-8" : "application/json; charset=utf-8";
|
|
1231
|
+
const ext = effectiveFormat === "csv" ? "csv" : "json";
|
|
1232
|
+
const dateStamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1233
|
+
const filename = `audit-export-${dateStamp}.${ext}`;
|
|
1234
|
+
return {
|
|
1235
|
+
status: 200,
|
|
1236
|
+
body: data,
|
|
1237
|
+
headers: {
|
|
1238
|
+
"content-type": contentType,
|
|
1239
|
+
"content-disposition": `attachment; filename="${filename}"`,
|
|
1240
|
+
"cache-control": "no-cache"
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1083
1243
|
} catch {
|
|
1084
1244
|
return { status: 500, body: { error: "Internal server error" } };
|
|
1085
1245
|
}
|
|
@@ -1468,38 +1628,6 @@ var AUDIT_LOG_SCHEMA = {
|
|
|
1468
1628
|
}
|
|
1469
1629
|
};
|
|
1470
1630
|
|
|
1471
|
-
// src/cursor.ts
|
|
1472
|
-
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1473
|
-
function encodeCursor(timestamp, id) {
|
|
1474
|
-
const payload = JSON.stringify({ t: timestamp.toISOString(), i: id });
|
|
1475
|
-
return btoa(payload);
|
|
1476
|
-
}
|
|
1477
|
-
function decodeCursor(cursor) {
|
|
1478
|
-
let parsed;
|
|
1479
|
-
try {
|
|
1480
|
-
parsed = JSON.parse(atob(cursor));
|
|
1481
|
-
} catch {
|
|
1482
|
-
throw new Error("Invalid cursor: failed to decode");
|
|
1483
|
-
}
|
|
1484
|
-
if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
|
|
1485
|
-
throw new Error("Invalid cursor: missing required fields");
|
|
1486
|
-
}
|
|
1487
|
-
const record = parsed;
|
|
1488
|
-
const t = record["t"];
|
|
1489
|
-
const i = record["i"];
|
|
1490
|
-
if (typeof t !== "string" || typeof i !== "string") {
|
|
1491
|
-
throw new Error("Invalid cursor: fields must be strings");
|
|
1492
|
-
}
|
|
1493
|
-
const timestamp = new Date(t);
|
|
1494
|
-
if (isNaN(timestamp.getTime())) {
|
|
1495
|
-
throw new Error("Invalid cursor: invalid timestamp");
|
|
1496
|
-
}
|
|
1497
|
-
if (!UUID_PATTERN.test(i)) {
|
|
1498
|
-
throw new Error("Invalid cursor: id must be a valid UUID");
|
|
1499
|
-
}
|
|
1500
|
-
return { timestamp, id: i };
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
1631
|
// src/escape.ts
|
|
1504
1632
|
function escapeLikePattern(input) {
|
|
1505
1633
|
return input.replace(/[%_\\]/g, "\\$&");
|