@archiva/archiva-nextjs 0.2.92 → 0.2.94

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.
@@ -0,0 +1,268 @@
1
+ import {
2
+ normalizeAuditEventItem
3
+ } from "./chunk-WA7TMG65.mjs";
4
+
5
+ // src/types.ts
6
+ var ArchivaError = class extends Error {
7
+ constructor(params) {
8
+ super(params.message);
9
+ this.statusCode = params.statusCode;
10
+ this.code = params.code;
11
+ this.retryAfterSeconds = params.retryAfterSeconds;
12
+ this.details = params.details;
13
+ }
14
+ };
15
+
16
+ // src/client.ts
17
+ var DEFAULT_BASE_URL = "https://api.archiva.app";
18
+ function buildHeaders(apiKey, overrides) {
19
+ const headers = new Headers(overrides);
20
+ headers.set("X-Project-Key", apiKey);
21
+ return headers;
22
+ }
23
+ async function parseError(response) {
24
+ const retryAfterHeader = response.headers.get("Retry-After");
25
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : void 0;
26
+ let payload = void 0;
27
+ try {
28
+ payload = await response.json();
29
+ } catch {
30
+ payload = void 0;
31
+ }
32
+ const errorMessage = typeof payload === "object" && payload !== null && "error" in payload ? String(payload.error) : response.statusText;
33
+ let code = "HTTP_ERROR";
34
+ if (response.status === 401) {
35
+ code = "UNAUTHORIZED";
36
+ } else if (response.status === 403) {
37
+ code = "FORBIDDEN";
38
+ } else if (response.status === 413) {
39
+ code = "PAYLOAD_TOO_LARGE";
40
+ } else if (response.status === 429) {
41
+ code = "RATE_LIMITED";
42
+ } else if (response.status === 409) {
43
+ code = "IDEMPOTENCY_CONFLICT";
44
+ }
45
+ throw new ArchivaError({
46
+ statusCode: response.status,
47
+ code,
48
+ message: errorMessage,
49
+ retryAfterSeconds,
50
+ details: payload
51
+ });
52
+ }
53
+ function createRequestId() {
54
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
55
+ return crypto.randomUUID();
56
+ }
57
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
58
+ }
59
+ function createIdempotencyKey() {
60
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
61
+ return `idem_${crypto.randomUUID()}`;
62
+ }
63
+ return `idem_${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
64
+ }
65
+ async function loadEvents(apiKey, params, baseUrl = DEFAULT_BASE_URL) {
66
+ const url = new URL(`${baseUrl}/api/events`);
67
+ if (params.entityId) {
68
+ url.searchParams.set("entityId", params.entityId);
69
+ }
70
+ if (params.actorId) {
71
+ url.searchParams.set("actorId", params.actorId);
72
+ }
73
+ if (params.entityType) {
74
+ url.searchParams.set("entityType", params.entityType);
75
+ }
76
+ if (params.actorType) {
77
+ url.searchParams.set("actorType", params.actorType);
78
+ }
79
+ if (params.limit) {
80
+ url.searchParams.set("limit", String(params.limit));
81
+ }
82
+ if (params.cursor) {
83
+ url.searchParams.set("cursor", params.cursor);
84
+ }
85
+ const response = await fetch(url.toString(), {
86
+ headers: buildHeaders(apiKey)
87
+ });
88
+ if (!response.ok) {
89
+ await parseError(response);
90
+ }
91
+ const payload = await response.json();
92
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) {
93
+ throw new ArchivaError({
94
+ statusCode: response.status,
95
+ code: "HTTP_ERROR",
96
+ message: "Invalid response format",
97
+ details: payload
98
+ });
99
+ }
100
+ const items = payload.items.map((item) => {
101
+ if (typeof item !== "object" || item === null) {
102
+ throw new ArchivaError({
103
+ statusCode: response.status,
104
+ code: "HTTP_ERROR",
105
+ message: "Invalid item format in response",
106
+ details: item
107
+ });
108
+ }
109
+ return normalizeAuditEventItem(item);
110
+ });
111
+ return {
112
+ items,
113
+ nextCursor: typeof payload.nextCursor === "string" ? payload.nextCursor : void 0
114
+ };
115
+ }
116
+ async function createEvent(apiKey, event, options, baseUrl = DEFAULT_BASE_URL) {
117
+ const idempotencyKey = options?.idempotencyKey ?? createIdempotencyKey();
118
+ const requestId = options?.requestId ?? createRequestId();
119
+ const headers = buildHeaders(apiKey, {
120
+ "Content-Type": "application/json",
121
+ "Idempotency-Key": idempotencyKey,
122
+ "X-Request-Id": requestId
123
+ });
124
+ const response = await fetch(`${baseUrl}/api/ingest/event`, {
125
+ method: "POST",
126
+ headers,
127
+ body: JSON.stringify(event)
128
+ });
129
+ if (!response.ok) {
130
+ await parseError(response);
131
+ }
132
+ const payload = await response.json();
133
+ if (!payload || typeof payload !== "object" || typeof payload.eventId !== "string") {
134
+ throw new ArchivaError({
135
+ statusCode: response.status,
136
+ code: "HTTP_ERROR",
137
+ message: "Invalid response format",
138
+ details: payload
139
+ });
140
+ }
141
+ return {
142
+ eventId: payload.eventId,
143
+ replayed: response.status === 200
144
+ };
145
+ }
146
+ async function createEvents(apiKey, events, options, baseUrl = DEFAULT_BASE_URL) {
147
+ const results = await Promise.all(
148
+ events.map(
149
+ (event, index) => createEvent(
150
+ apiKey,
151
+ event,
152
+ {
153
+ ...options,
154
+ idempotencyKey: options?.idempotencyKey ? `${options.idempotencyKey}_${index}` : void 0
155
+ },
156
+ baseUrl
157
+ )
158
+ )
159
+ );
160
+ return {
161
+ eventIds: results.map((r) => r.eventId)
162
+ };
163
+ }
164
+
165
+ // src/actions.ts
166
+ var DEFAULT_BASE_URL2 = "https://api.archiva.app";
167
+ function getApiKey(apiKey) {
168
+ const resolvedKey = apiKey || process.env.ARCHIVA_SECRET_KEY;
169
+ if (!resolvedKey) {
170
+ throw new Error("ARCHIVA_SECRET_KEY environment variable is required, or provide apiKey prop to ArchivaProvider");
171
+ }
172
+ return resolvedKey;
173
+ }
174
+ async function loadEvents2(params, apiKey) {
175
+ const resolvedApiKey = getApiKey(apiKey);
176
+ return loadEvents(resolvedApiKey, params, DEFAULT_BASE_URL2);
177
+ }
178
+ async function createEvent2(event, options, apiKey) {
179
+ const resolvedApiKey = getApiKey(apiKey);
180
+ return createEvent(resolvedApiKey, event, options, DEFAULT_BASE_URL2);
181
+ }
182
+ async function createEvents2(events, options, apiKey) {
183
+ const resolvedApiKey = getApiKey(apiKey);
184
+ return createEvents(resolvedApiKey, events, options, DEFAULT_BASE_URL2);
185
+ }
186
+
187
+ // src/server/frontendTokens.ts
188
+ import "server-only";
189
+ var DEFAULT_API_BASE_URL = "https://api.archiva.app";
190
+ async function createFrontendTokenGET(projectId, apiBaseUrl = DEFAULT_API_BASE_URL) {
191
+ const secretKey = process.env.ARCHIVA_SECRET_KEY;
192
+ if (!secretKey || secretKey.trim().length === 0) {
193
+ const exists = process.env.ARCHIVA_SECRET_KEY !== void 0;
194
+ throw new Error(
195
+ `ARCHIVA_SECRET_KEY environment variable is ${exists ? "empty" : "not configured"}. Please set it in your .env.local file (or .env) with a valid value and restart your Next.js dev server. The variable must be in the project root directory and must not be empty.`
196
+ );
197
+ }
198
+ const keyPrefix = secretKey.substring(0, 10);
199
+ console.log("[Archiva] Using API key prefix:", keyPrefix + "...", "Length:", secretKey.length);
200
+ const url = new URL(`${apiBaseUrl}/api/v1/frontend-tokens`);
201
+ const response = await fetch(url.toString(), {
202
+ method: "POST",
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ "Authorization": `Bearer ${secretKey}`
206
+ },
207
+ body: JSON.stringify({
208
+ scopes: ["timeline:read"],
209
+ ...projectId && { projectId }
210
+ })
211
+ });
212
+ if (!response.ok) {
213
+ const error = await response.json().catch(() => ({ error: response.statusText }));
214
+ const errorMessage = error.error || `Failed to fetch frontend token: ${response.status}`;
215
+ if (errorMessage.includes("ARCHIVA_SECRET_KEY") || response.status === 401 || response.status === 403) {
216
+ throw new Error(
217
+ `Archiva API authentication failed. Check that your ARCHIVA_SECRET_KEY is valid and has the correct permissions. API error: ${errorMessage}`
218
+ );
219
+ }
220
+ throw new Error(errorMessage);
221
+ }
222
+ const data = await response.json();
223
+ if (!data.token || typeof data.expiresAt !== "number") {
224
+ throw new Error("Invalid token response format");
225
+ }
226
+ return {
227
+ token: data.token,
228
+ expiresAt: data.expiresAt
229
+ };
230
+ }
231
+
232
+ // src/server/handlers/createFrontendTokenRoute.ts
233
+ import "server-only";
234
+ import { NextResponse } from "next/server";
235
+ var DEFAULT_API_BASE_URL2 = "https://api.archiva.app";
236
+ function createFrontendTokenRoute(options) {
237
+ return async function GET2(request) {
238
+ try {
239
+ const searchParams = request.nextUrl.searchParams;
240
+ const projectId = searchParams.get("projectId") || void 0;
241
+ const apiBaseUrl = options?.apiBaseUrl || DEFAULT_API_BASE_URL2;
242
+ const tokenResponse = await createFrontendTokenGET(projectId, apiBaseUrl);
243
+ return NextResponse.json(tokenResponse);
244
+ } catch (error) {
245
+ console.error("Error fetching frontend token:", error);
246
+ const message = error instanceof Error ? error.message : "Internal server error";
247
+ const statusCode = message.includes("ARCHIVA_SECRET_KEY") ? 500 : 500;
248
+ return NextResponse.json(
249
+ { error: message },
250
+ { status: statusCode }
251
+ );
252
+ }
253
+ };
254
+ }
255
+ async function GET(request) {
256
+ const handler = createFrontendTokenRoute();
257
+ return handler(request);
258
+ }
259
+
260
+ export {
261
+ ArchivaError,
262
+ loadEvents2 as loadEvents,
263
+ createEvent2 as createEvent,
264
+ createEvents2 as createEvents,
265
+ createFrontendTokenGET,
266
+ createFrontendTokenRoute,
267
+ GET
268
+ };
@@ -0,0 +1,81 @@
1
+ // src/eventNormalizer.ts
2
+ var coerceString = (value) => {
3
+ if (value === null || value === void 0) {
4
+ return void 0;
5
+ }
6
+ if (typeof value === "string") {
7
+ return value;
8
+ }
9
+ if (typeof value === "number" || typeof value === "boolean") {
10
+ return String(value);
11
+ }
12
+ return void 0;
13
+ };
14
+ var coerceNullableString = (value) => {
15
+ if (value === null || value === void 0) {
16
+ return null;
17
+ }
18
+ if (typeof value === "string") {
19
+ return value;
20
+ }
21
+ if (typeof value === "number" || typeof value === "boolean") {
22
+ return String(value);
23
+ }
24
+ return null;
25
+ };
26
+ var coerceActorType = (value) => {
27
+ if (typeof value !== "string") {
28
+ return void 0;
29
+ }
30
+ const normalized = value.toLowerCase();
31
+ if (normalized === "user" || normalized === "service" || normalized === "system") {
32
+ return normalized;
33
+ }
34
+ return void 0;
35
+ };
36
+ var getActorValue = (event, actorRecord, keys) => {
37
+ for (const key of keys) {
38
+ if (Object.prototype.hasOwnProperty.call(event, key)) {
39
+ return event[key];
40
+ }
41
+ if (actorRecord && Object.prototype.hasOwnProperty.call(actorRecord, key)) {
42
+ return actorRecord[key];
43
+ }
44
+ }
45
+ return void 0;
46
+ };
47
+ var normalizeAuditEventItem = (event) => {
48
+ const actorRecord = typeof event.actor === "object" && event.actor !== null ? event.actor : void 0;
49
+ const actorDisplayValue = getActorValue(event, actorRecord, [
50
+ "actorDisplay",
51
+ "actor_display",
52
+ "display",
53
+ "display_name",
54
+ "name"
55
+ ]);
56
+ const actorTypeValue = getActorValue(event, actorRecord, [
57
+ "actorType",
58
+ "actor_type",
59
+ "type"
60
+ ]);
61
+ const actorIdValue = getActorValue(event, actorRecord, [
62
+ "actorId",
63
+ "actor_id",
64
+ "id"
65
+ ]);
66
+ return {
67
+ id: coerceString(event.id) ?? "",
68
+ receivedAt: coerceString(event.receivedAt ?? event.received_at) ?? "",
69
+ action: coerceString(event.action) ?? "",
70
+ entityType: coerceString(event.entityType ?? event.entity_type) ?? "",
71
+ entityId: coerceString(event.entityId ?? event.entity_id) ?? "",
72
+ actorId: coerceNullableString(actorIdValue),
73
+ actorType: coerceActorType(actorTypeValue),
74
+ actorDisplay: coerceString(actorDisplayValue) ?? "",
75
+ source: coerceNullableString(event.source)
76
+ };
77
+ };
78
+
79
+ export {
80
+ normalizeAuditEventItem
81
+ };
@@ -0,0 +1,139 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ type EventChange = {
4
+ op: "set" | "unset" | "add" | "remove" | "replace" | string;
5
+ path: string;
6
+ before?: unknown;
7
+ after?: unknown;
8
+ };
9
+ type CreateEventInput = {
10
+ action: string;
11
+ entityType: string;
12
+ entityId: string;
13
+ actorType?: string;
14
+ actorId?: string;
15
+ actorDisplay?: string;
16
+ occurredAt?: string;
17
+ source?: string;
18
+ context?: Record<string, unknown>;
19
+ changes?: EventChange[];
20
+ };
21
+ type CreateEventOptions = {
22
+ idempotencyKey?: string;
23
+ requestId?: string;
24
+ };
25
+ type AuditEventListItem = {
26
+ id: string;
27
+ receivedAt: string;
28
+ action: string;
29
+ entityType: string;
30
+ entityId: string;
31
+ actorId: string | null;
32
+ actorType?: 'user' | 'service' | 'system';
33
+ actorDisplay: string;
34
+ source: string | null;
35
+ };
36
+ type PageResult<T> = {
37
+ items: T[];
38
+ nextCursor?: string;
39
+ };
40
+ type LoadEventsParams = {
41
+ entityId?: string;
42
+ actorId?: string;
43
+ entityType?: string;
44
+ actorType?: 'user' | 'service' | 'system';
45
+ limit?: number;
46
+ cursor?: string;
47
+ };
48
+ declare class ArchivaError extends Error {
49
+ statusCode: number;
50
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
51
+ retryAfterSeconds?: number;
52
+ details?: unknown;
53
+ constructor(params: {
54
+ statusCode: number;
55
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
56
+ message: string;
57
+ retryAfterSeconds?: number;
58
+ details?: unknown;
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Server action to load audit events
64
+ *
65
+ * @param params - Query parameters for filtering events
66
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
67
+ * @returns Paginated list of audit events
68
+ */
69
+ declare function loadEvents(params: LoadEventsParams, apiKey?: string): Promise<PageResult<AuditEventListItem>>;
70
+ /**
71
+ * Server action to create a single audit event
72
+ *
73
+ * @param event - Event data to create
74
+ * @param options - Optional idempotency and request ID options
75
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
76
+ * @returns Created event ID and replay status
77
+ */
78
+ declare function createEvent(event: CreateEventInput, options?: CreateEventOptions, apiKey?: string): Promise<{
79
+ eventId: string;
80
+ replayed: boolean;
81
+ }>;
82
+ /**
83
+ * Server action to create multiple audit events (bulk)
84
+ *
85
+ * @param events - Array of events to create
86
+ * @param options - Optional idempotency and request ID options
87
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
88
+ * @returns Array of created event IDs
89
+ */
90
+ declare function createEvents(events: CreateEventInput[], options?: CreateEventOptions, apiKey?: string): Promise<{
91
+ eventIds: string[];
92
+ }>;
93
+
94
+ interface FrontendTokenResponse {
95
+ token: string;
96
+ expiresAt: number;
97
+ }
98
+ /**
99
+ * Fetches a frontend token from the Archiva API
100
+ *
101
+ * @param projectId - Optional project ID for scoping
102
+ * @param apiBaseUrl - Archiva API base URL (defaults to https://api.archiva.app)
103
+ * @returns Frontend token response
104
+ */
105
+ declare function createFrontendTokenGET(projectId?: string, apiBaseUrl?: string): Promise<FrontendTokenResponse>;
106
+
107
+ /**
108
+ * Next.js route handler for GET /api/archiva/frontend-token
109
+ *
110
+ * This handler can be used in the host app's route file:
111
+ *
112
+ * ```ts
113
+ * // app/api/archiva/frontend-token/route.ts
114
+ * export { GET } from '@archiva/archiva-nextjs/server';
115
+ * ```
116
+ *
117
+ * Or with custom configuration:
118
+ *
119
+ * ```ts
120
+ * import { createFrontendTokenRoute } from '@archiva/archiva-nextjs/server';
121
+ *
122
+ * export const GET = createFrontendTokenRoute({
123
+ * apiBaseUrl: process.env.ARCHIVA_API_URL,
124
+ * });
125
+ * ```
126
+ */
127
+ declare function createFrontendTokenRoute(options?: {
128
+ apiBaseUrl?: string;
129
+ }): (request: NextRequest) => Promise<NextResponse<FrontendTokenResponse> | NextResponse<{
130
+ error: string;
131
+ }>>;
132
+ /**
133
+ * Default GET handler (for direct export)
134
+ */
135
+ declare function GET(request: NextRequest): Promise<NextResponse<FrontendTokenResponse> | NextResponse<{
136
+ error: string;
137
+ }>>;
138
+
139
+ export { ArchivaError as A, type CreateEventInput as C, type EventChange as E, type FrontendTokenResponse as F, GET as G, type LoadEventsParams as L, type PageResult as P, createEvents as a, createFrontendTokenGET as b, createEvent as c, createFrontendTokenRoute as d, type CreateEventOptions as e, type AuditEventListItem as f, loadEvents as l };
@@ -0,0 +1,139 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+
3
+ type EventChange = {
4
+ op: "set" | "unset" | "add" | "remove" | "replace" | string;
5
+ path: string;
6
+ before?: unknown;
7
+ after?: unknown;
8
+ };
9
+ type CreateEventInput = {
10
+ action: string;
11
+ entityType: string;
12
+ entityId: string;
13
+ actorType?: string;
14
+ actorId?: string;
15
+ actorDisplay?: string;
16
+ occurredAt?: string;
17
+ source?: string;
18
+ context?: Record<string, unknown>;
19
+ changes?: EventChange[];
20
+ };
21
+ type CreateEventOptions = {
22
+ idempotencyKey?: string;
23
+ requestId?: string;
24
+ };
25
+ type AuditEventListItem = {
26
+ id: string;
27
+ receivedAt: string;
28
+ action: string;
29
+ entityType: string;
30
+ entityId: string;
31
+ actorId: string | null;
32
+ actorType?: 'user' | 'service' | 'system';
33
+ actorDisplay: string;
34
+ source: string | null;
35
+ };
36
+ type PageResult<T> = {
37
+ items: T[];
38
+ nextCursor?: string;
39
+ };
40
+ type LoadEventsParams = {
41
+ entityId?: string;
42
+ actorId?: string;
43
+ entityType?: string;
44
+ actorType?: 'user' | 'service' | 'system';
45
+ limit?: number;
46
+ cursor?: string;
47
+ };
48
+ declare class ArchivaError extends Error {
49
+ statusCode: number;
50
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
51
+ retryAfterSeconds?: number;
52
+ details?: unknown;
53
+ constructor(params: {
54
+ statusCode: number;
55
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
56
+ message: string;
57
+ retryAfterSeconds?: number;
58
+ details?: unknown;
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Server action to load audit events
64
+ *
65
+ * @param params - Query parameters for filtering events
66
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
67
+ * @returns Paginated list of audit events
68
+ */
69
+ declare function loadEvents(params: LoadEventsParams, apiKey?: string): Promise<PageResult<AuditEventListItem>>;
70
+ /**
71
+ * Server action to create a single audit event
72
+ *
73
+ * @param event - Event data to create
74
+ * @param options - Optional idempotency and request ID options
75
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
76
+ * @returns Created event ID and replay status
77
+ */
78
+ declare function createEvent(event: CreateEventInput, options?: CreateEventOptions, apiKey?: string): Promise<{
79
+ eventId: string;
80
+ replayed: boolean;
81
+ }>;
82
+ /**
83
+ * Server action to create multiple audit events (bulk)
84
+ *
85
+ * @param events - Array of events to create
86
+ * @param options - Optional idempotency and request ID options
87
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
88
+ * @returns Array of created event IDs
89
+ */
90
+ declare function createEvents(events: CreateEventInput[], options?: CreateEventOptions, apiKey?: string): Promise<{
91
+ eventIds: string[];
92
+ }>;
93
+
94
+ interface FrontendTokenResponse {
95
+ token: string;
96
+ expiresAt: number;
97
+ }
98
+ /**
99
+ * Fetches a frontend token from the Archiva API
100
+ *
101
+ * @param projectId - Optional project ID for scoping
102
+ * @param apiBaseUrl - Archiva API base URL (defaults to https://api.archiva.app)
103
+ * @returns Frontend token response
104
+ */
105
+ declare function createFrontendTokenGET(projectId?: string, apiBaseUrl?: string): Promise<FrontendTokenResponse>;
106
+
107
+ /**
108
+ * Next.js route handler for GET /api/archiva/frontend-token
109
+ *
110
+ * This handler can be used in the host app's route file:
111
+ *
112
+ * ```ts
113
+ * // app/api/archiva/frontend-token/route.ts
114
+ * export { GET } from '@archiva/archiva-nextjs/server';
115
+ * ```
116
+ *
117
+ * Or with custom configuration:
118
+ *
119
+ * ```ts
120
+ * import { createFrontendTokenRoute } from '@archiva/archiva-nextjs/server';
121
+ *
122
+ * export const GET = createFrontendTokenRoute({
123
+ * apiBaseUrl: process.env.ARCHIVA_API_URL,
124
+ * });
125
+ * ```
126
+ */
127
+ declare function createFrontendTokenRoute(options?: {
128
+ apiBaseUrl?: string;
129
+ }): (request: NextRequest) => Promise<NextResponse<FrontendTokenResponse> | NextResponse<{
130
+ error: string;
131
+ }>>;
132
+ /**
133
+ * Default GET handler (for direct export)
134
+ */
135
+ declare function GET(request: NextRequest): Promise<NextResponse<FrontendTokenResponse> | NextResponse<{
136
+ error: string;
137
+ }>>;
138
+
139
+ export { ArchivaError as A, type CreateEventInput as C, type EventChange as E, type FrontendTokenResponse as F, GET as G, type LoadEventsParams as L, type PageResult as P, createEvents as a, createFrontendTokenGET as b, createEvent as c, createFrontendTokenRoute as d, type CreateEventOptions as e, type AuditEventListItem as f, loadEvents as l };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ArchivaProvider, ArchivaProviderProps } from './react/index.mjs';
2
- export { A as ArchivaError, f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, E as EventChange, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from './index-BJ8aJsbs.mjs';
2
+ export { A as ArchivaError, f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, E as EventChange, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from './index-Bk4DxULy.mjs';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
5
5
  import 'next/server';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ArchivaProvider, ArchivaProviderProps } from './react/index.js';
2
- export { A as ArchivaError, f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, E as EventChange, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from './index-BJ8aJsbs.js';
2
+ export { A as ArchivaError, f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, E as EventChange, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from './index-Bk4DxULy.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
5
5
  import 'next/server';
package/dist/index.js CHANGED
@@ -239,6 +239,84 @@ var ArchivaError = class extends Error {
239
239
  }
240
240
  };
241
241
 
242
+ // src/eventNormalizer.ts
243
+ var coerceString = (value) => {
244
+ if (value === null || value === void 0) {
245
+ return void 0;
246
+ }
247
+ if (typeof value === "string") {
248
+ return value;
249
+ }
250
+ if (typeof value === "number" || typeof value === "boolean") {
251
+ return String(value);
252
+ }
253
+ return void 0;
254
+ };
255
+ var coerceNullableString = (value) => {
256
+ if (value === null || value === void 0) {
257
+ return null;
258
+ }
259
+ if (typeof value === "string") {
260
+ return value;
261
+ }
262
+ if (typeof value === "number" || typeof value === "boolean") {
263
+ return String(value);
264
+ }
265
+ return null;
266
+ };
267
+ var coerceActorType = (value) => {
268
+ if (typeof value !== "string") {
269
+ return void 0;
270
+ }
271
+ const normalized = value.toLowerCase();
272
+ if (normalized === "user" || normalized === "service" || normalized === "system") {
273
+ return normalized;
274
+ }
275
+ return void 0;
276
+ };
277
+ var getActorValue = (event, actorRecord, keys) => {
278
+ for (const key of keys) {
279
+ if (Object.prototype.hasOwnProperty.call(event, key)) {
280
+ return event[key];
281
+ }
282
+ if (actorRecord && Object.prototype.hasOwnProperty.call(actorRecord, key)) {
283
+ return actorRecord[key];
284
+ }
285
+ }
286
+ return void 0;
287
+ };
288
+ var normalizeAuditEventItem = (event) => {
289
+ const actorRecord = typeof event.actor === "object" && event.actor !== null ? event.actor : void 0;
290
+ const actorDisplayValue = getActorValue(event, actorRecord, [
291
+ "actorDisplay",
292
+ "actor_display",
293
+ "display",
294
+ "display_name",
295
+ "name"
296
+ ]);
297
+ const actorTypeValue = getActorValue(event, actorRecord, [
298
+ "actorType",
299
+ "actor_type",
300
+ "type"
301
+ ]);
302
+ const actorIdValue = getActorValue(event, actorRecord, [
303
+ "actorId",
304
+ "actor_id",
305
+ "id"
306
+ ]);
307
+ return {
308
+ id: coerceString(event.id) ?? "",
309
+ receivedAt: coerceString(event.receivedAt ?? event.received_at) ?? "",
310
+ action: coerceString(event.action) ?? "",
311
+ entityType: coerceString(event.entityType ?? event.entity_type) ?? "",
312
+ entityId: coerceString(event.entityId ?? event.entity_id) ?? "",
313
+ actorId: coerceNullableString(actorIdValue),
314
+ actorType: coerceActorType(actorTypeValue),
315
+ actorDisplay: coerceString(actorDisplayValue) ?? "",
316
+ source: coerceNullableString(event.source)
317
+ };
318
+ };
319
+
242
320
  // src/client.ts
243
321
  var DEFAULT_BASE_URL = "https://api.archiva.app";
244
322
  function buildHeaders(apiKey, overrides) {
@@ -332,20 +410,7 @@ async function loadEvents(apiKey, params, baseUrl = DEFAULT_BASE_URL) {
332
410
  details: item
333
411
  });
334
412
  }
335
- const event = item;
336
- const actorDisplay = event.actorDisplay !== null && event.actorDisplay !== void 0 ? event.actorDisplay : event.actor_display !== null && event.actor_display !== void 0 ? event.actor_display : void 0;
337
- const actorType = event.actorType !== null && event.actorType !== void 0 ? event.actorType : event.actor_type !== null && event.actor_type !== void 0 ? event.actor_type : void 0;
338
- return {
339
- id: String(event.id ?? ""),
340
- receivedAt: String(event.receivedAt ?? event.received_at ?? ""),
341
- action: String(event.action ?? ""),
342
- entityType: String(event.entityType ?? event.entity_type ?? ""),
343
- entityId: String(event.entityId ?? event.entity_id ?? ""),
344
- actorId: event.actorId !== null && event.actorId !== void 0 ? String(event.actorId) : event.actor_id !== null && event.actor_id !== void 0 ? String(event.actor_id) : null,
345
- actorType: actorType && (actorType === "user" || actorType === "service" || actorType === "system") ? actorType : void 0,
346
- actorDisplay: actorDisplay !== null && actorDisplay !== void 0 ? String(actorDisplay) : void 0,
347
- source: event.source !== null && event.source !== void 0 ? String(event.source) : null
348
- };
413
+ return normalizeAuditEventItem(item);
349
414
  });
350
415
  return {
351
416
  items,
package/dist/index.mjs CHANGED
@@ -9,7 +9,8 @@ import {
9
9
  createFrontendTokenGET,
10
10
  createFrontendTokenRoute,
11
11
  loadEvents
12
- } from "./chunk-2YLLG2IF.mjs";
12
+ } from "./chunk-A3Q4YKTK.mjs";
13
+ import "./chunk-WA7TMG65.mjs";
13
14
  export {
14
15
  ArchivaError,
15
16
  ArchivaProvider,
@@ -56,6 +56,84 @@ function useArchiva() {
56
56
  return useArchivaContext();
57
57
  }
58
58
 
59
+ // src/eventNormalizer.ts
60
+ var coerceString = (value) => {
61
+ if (value === null || value === void 0) {
62
+ return void 0;
63
+ }
64
+ if (typeof value === "string") {
65
+ return value;
66
+ }
67
+ if (typeof value === "number" || typeof value === "boolean") {
68
+ return String(value);
69
+ }
70
+ return void 0;
71
+ };
72
+ var coerceNullableString = (value) => {
73
+ if (value === null || value === void 0) {
74
+ return null;
75
+ }
76
+ if (typeof value === "string") {
77
+ return value;
78
+ }
79
+ if (typeof value === "number" || typeof value === "boolean") {
80
+ return String(value);
81
+ }
82
+ return null;
83
+ };
84
+ var coerceActorType = (value) => {
85
+ if (typeof value !== "string") {
86
+ return void 0;
87
+ }
88
+ const normalized = value.toLowerCase();
89
+ if (normalized === "user" || normalized === "service" || normalized === "system") {
90
+ return normalized;
91
+ }
92
+ return void 0;
93
+ };
94
+ var getActorValue = (event, actorRecord, keys) => {
95
+ for (const key of keys) {
96
+ if (Object.prototype.hasOwnProperty.call(event, key)) {
97
+ return event[key];
98
+ }
99
+ if (actorRecord && Object.prototype.hasOwnProperty.call(actorRecord, key)) {
100
+ return actorRecord[key];
101
+ }
102
+ }
103
+ return void 0;
104
+ };
105
+ var normalizeAuditEventItem = (event) => {
106
+ const actorRecord = typeof event.actor === "object" && event.actor !== null ? event.actor : void 0;
107
+ const actorDisplayValue = getActorValue(event, actorRecord, [
108
+ "actorDisplay",
109
+ "actor_display",
110
+ "display",
111
+ "display_name",
112
+ "name"
113
+ ]);
114
+ const actorTypeValue = getActorValue(event, actorRecord, [
115
+ "actorType",
116
+ "actor_type",
117
+ "type"
118
+ ]);
119
+ const actorIdValue = getActorValue(event, actorRecord, [
120
+ "actorId",
121
+ "actor_id",
122
+ "id"
123
+ ]);
124
+ return {
125
+ id: coerceString(event.id) ?? "",
126
+ receivedAt: coerceString(event.receivedAt ?? event.received_at) ?? "",
127
+ action: coerceString(event.action) ?? "",
128
+ entityType: coerceString(event.entityType ?? event.entity_type) ?? "",
129
+ entityId: coerceString(event.entityId ?? event.entity_id) ?? "",
130
+ actorId: coerceNullableString(actorIdValue),
131
+ actorType: coerceActorType(actorTypeValue),
132
+ actorDisplay: coerceString(actorDisplayValue) ?? "",
133
+ source: coerceNullableString(event.source)
134
+ };
135
+ };
136
+
59
137
  // src/react/Timeline.tsx
60
138
  var import_jsx_runtime2 = require("react/jsx-runtime");
61
139
  function formatTimestamp(timestamp, format = "default") {
@@ -148,7 +226,7 @@ function eventToTimelineItem(event, getActorAvatar) {
148
226
  // Required by TimelineItem type, but not used in activity layout - matches ActivityLog.tsx line 352
149
227
  userName: userName.charAt(0).toUpperCase() + userName.slice(1),
150
228
  // Matches ActivityLog.tsx line 353
151
- actorDisplay: event.actorDisplay ?? void 0,
229
+ actorDisplay: event.actorDisplay,
152
230
  // Matches ActivityLog.tsx line 354
153
231
  userHandle,
154
232
  // Matches ActivityLog.tsx line 355
@@ -217,20 +295,7 @@ async function fetchEventsWithRetry(apiBaseUrl, getToken, forceRefreshToken, par
217
295
  if (typeof item !== "object" || item === null) {
218
296
  throw new Error("Invalid item format in response");
219
297
  }
220
- const event = item;
221
- const actorDisplay = event.actorDisplay !== null && event.actorDisplay !== void 0 ? event.actorDisplay : event.actor_display !== null && event.actor_display !== void 0 ? event.actor_display : void 0;
222
- const actorType = event.actorType !== null && event.actorType !== void 0 ? event.actorType : event.actor_type !== null && event.actor_type !== void 0 ? event.actor_type : void 0;
223
- return {
224
- id: String(event.id ?? ""),
225
- receivedAt: String(event.receivedAt ?? event.received_at ?? ""),
226
- action: String(event.action ?? ""),
227
- entityType: String(event.entityType ?? event.entity_type ?? ""),
228
- entityId: String(event.entityId ?? event.entity_id ?? ""),
229
- actorId: event.actorId !== null && event.actorId !== void 0 ? String(event.actorId) : event.actor_id !== null && event.actor_id !== void 0 ? String(event.actor_id) : null,
230
- actorType: actorType && (actorType === "user" || actorType === "service" || actorType === "system") ? actorType : void 0,
231
- actorDisplay: actorDisplay !== null && actorDisplay !== void 0 ? String(actorDisplay) : void 0,
232
- source: event.source !== null && event.source !== void 0 ? String(event.source) : null
233
- };
298
+ return normalizeAuditEventItem(item);
234
299
  });
235
300
  return {
236
301
  items,
@@ -370,23 +435,45 @@ function Timeline({
370
435
  }) {
371
436
  const { apiBaseUrl, getToken, forceRefreshToken } = useArchiva();
372
437
  const [allEvents, setAllEvents] = React2.useState([]);
438
+ const [previousEvents, setPreviousEvents] = React2.useState([]);
373
439
  const [cursor, setCursor] = React2.useState(void 0);
374
440
  const [loading, setLoading] = React2.useState(false);
375
441
  const [error, setError] = React2.useState(null);
376
442
  const [hasMore, setHasMore] = React2.useState(false);
377
443
  const [searchQuery, setSearchQuery] = React2.useState("");
444
+ const queryParamsRef = React2.useRef({});
445
+ const queryParamsKeyRef = React2.useRef("");
446
+ const queryParams = React2.useMemo(() => {
447
+ const params = {
448
+ entityId,
449
+ actorId,
450
+ entityType,
451
+ // Filter by actorType on API side
452
+ actorType: showSystemAndServices ? void 0 : "user",
453
+ limit: initialLimit
454
+ };
455
+ const key = JSON.stringify(params);
456
+ if (key !== queryParamsKeyRef.current) {
457
+ queryParamsKeyRef.current = key;
458
+ queryParamsRef.current = params;
459
+ return params;
460
+ }
461
+ return queryParamsRef.current;
462
+ }, [entityId, actorId, entityType, initialLimit, showSystemAndServices]);
463
+ const allEventsRef = React2.useRef([]);
464
+ React2.useEffect(() => {
465
+ allEventsRef.current = allEvents;
466
+ }, [allEvents]);
378
467
  const load = React2.useCallback(
379
468
  async (options) => {
380
469
  setLoading(true);
381
470
  setError(null);
471
+ if (options?.reset && allEventsRef.current.length > 0) {
472
+ setPreviousEvents(allEventsRef.current);
473
+ }
382
474
  try {
383
475
  const params = {
384
- entityId,
385
- actorId,
386
- entityType,
387
- // Filter by actorType on API side
388
- actorType: showSystemAndServices ? void 0 : "user",
389
- limit: initialLimit,
476
+ ...queryParams,
390
477
  cursor: options?.reset ? void 0 : options?.currentCursor ?? cursor
391
478
  };
392
479
  const response = await fetchEventsWithRetry(
@@ -395,34 +482,54 @@ function Timeline({
395
482
  forceRefreshToken,
396
483
  params
397
484
  );
398
- setAllEvents((prev) => options?.reset ? response.items : [...prev, ...response.items]);
485
+ setAllEvents((prev) => {
486
+ const newEvents = options?.reset ? response.items : [...prev, ...response.items];
487
+ if (options?.reset) {
488
+ setPreviousEvents([]);
489
+ }
490
+ return newEvents;
491
+ });
399
492
  setCursor(response.nextCursor);
400
493
  setHasMore(Boolean(response.nextCursor));
401
494
  } catch (err) {
402
495
  setError(err.message);
496
+ setPreviousEvents((prev) => {
497
+ if (options?.reset && prev.length > 0) {
498
+ setAllEvents(prev);
499
+ return [];
500
+ }
501
+ return prev;
502
+ });
403
503
  } finally {
404
504
  setLoading(false);
405
505
  }
406
506
  },
407
- [entityId, actorId, entityType, initialLimit, apiBaseUrl, getToken, forceRefreshToken, cursor, showSystemAndServices]
507
+ [queryParams, apiBaseUrl, getToken, forceRefreshToken, cursor]
408
508
  );
409
509
  React2.useEffect(() => {
410
510
  setCursor(void 0);
411
511
  setAllEvents([]);
512
+ setPreviousEvents([]);
412
513
  void load({ reset: true });
413
- }, [entityId, actorId, entityType, showSystemAndServices]);
514
+ }, [entityId, actorId, entityType, showSystemAndServices, initialLimit]);
515
+ const eventsToFilter = React2.useMemo(() => {
516
+ if (loading && allEvents.length === 0 && previousEvents.length > 0) {
517
+ return previousEvents;
518
+ }
519
+ return allEvents;
520
+ }, [allEvents, loading, previousEvents]);
414
521
  const filteredEvents = React2.useMemo(() => {
415
- return applyClientSideFilters(allEvents, searchQuery, showSystemAndServices);
416
- }, [allEvents, searchQuery, showSystemAndServices]);
522
+ return applyClientSideFilters(eventsToFilter, searchQuery, showSystemAndServices);
523
+ }, [eventsToFilter, searchQuery, showSystemAndServices]);
417
524
  const timelineItems = React2.useMemo(() => {
418
525
  return filteredEvents.slice(0, 10).map(
419
526
  (event) => eventToTimelineItem(event, getActorAvatar)
420
527
  );
421
528
  }, [filteredEvents, getActorAvatar]);
422
- if (loading && allEvents.length === 0) {
529
+ if (loading && allEvents.length === 0 && previousEvents.length === 0) {
423
530
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", style: { width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { padding: "3rem", textAlign: "center", color: "#6b7280" }, children: "Loading events..." }) });
424
531
  }
425
- if (error) {
532
+ if (error && allEvents.length === 0) {
426
533
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", style: { width: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "1rem", color: "#dc2626", backgroundColor: "#fef2f2", borderRadius: "0.375rem" }, children: [
427
534
  "Error: ",
428
535
  error
@@ -438,13 +545,17 @@ function Timeline({
438
545
  placeholder: "Search actions, entities, IDs, actors, sources..."
439
546
  }
440
547
  ) }),
441
- searchQuery && filteredEvents.length !== allEvents.length && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: "0.5rem", fontSize: "0.875rem", color: "#6b7280" }, children: [
548
+ (searchQuery || filteredEvents.length !== eventsToFilter.length) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: "0.5rem", fontSize: "0.875rem", color: "#6b7280" }, children: [
442
549
  "Showing ",
443
550
  filteredEvents.length,
444
551
  " of ",
445
- allEvents.length,
552
+ eventsToFilter.length,
446
553
  " events"
447
554
  ] }),
555
+ error && allEvents.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { marginBottom: "1rem", padding: "0.75rem", color: "#dc2626", backgroundColor: "#fef2f2", borderRadius: "0.375rem", fontSize: "0.875rem" }, children: [
556
+ "Error: ",
557
+ error
558
+ ] }),
448
559
  timelineItems.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { padding: "3rem", textAlign: "center", color: "#6b7280" }, children: searchQuery ? "No events match your search." : emptyMessage }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { position: "relative", width: "100%" }, children: timelineItems.map((item, index) => {
449
560
  const useActivityLayout = !!(item.actorDisplay || item.userName || item.userHandle);
450
561
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
@@ -1,4 +1,7 @@
1
1
  "use client";
2
+ import {
3
+ normalizeAuditEventItem
4
+ } from "../chunk-WA7TMG65.mjs";
2
5
  import {
3
6
  useArchivaContext
4
7
  } from "../chunk-H4TGL57C.mjs";
@@ -103,7 +106,7 @@ function eventToTimelineItem(event, getActorAvatar) {
103
106
  // Required by TimelineItem type, but not used in activity layout - matches ActivityLog.tsx line 352
104
107
  userName: userName.charAt(0).toUpperCase() + userName.slice(1),
105
108
  // Matches ActivityLog.tsx line 353
106
- actorDisplay: event.actorDisplay ?? void 0,
109
+ actorDisplay: event.actorDisplay,
107
110
  // Matches ActivityLog.tsx line 354
108
111
  userHandle,
109
112
  // Matches ActivityLog.tsx line 355
@@ -172,20 +175,7 @@ async function fetchEventsWithRetry(apiBaseUrl, getToken, forceRefreshToken, par
172
175
  if (typeof item !== "object" || item === null) {
173
176
  throw new Error("Invalid item format in response");
174
177
  }
175
- const event = item;
176
- const actorDisplay = event.actorDisplay !== null && event.actorDisplay !== void 0 ? event.actorDisplay : event.actor_display !== null && event.actor_display !== void 0 ? event.actor_display : void 0;
177
- const actorType = event.actorType !== null && event.actorType !== void 0 ? event.actorType : event.actor_type !== null && event.actor_type !== void 0 ? event.actor_type : void 0;
178
- return {
179
- id: String(event.id ?? ""),
180
- receivedAt: String(event.receivedAt ?? event.received_at ?? ""),
181
- action: String(event.action ?? ""),
182
- entityType: String(event.entityType ?? event.entity_type ?? ""),
183
- entityId: String(event.entityId ?? event.entity_id ?? ""),
184
- actorId: event.actorId !== null && event.actorId !== void 0 ? String(event.actorId) : event.actor_id !== null && event.actor_id !== void 0 ? String(event.actor_id) : null,
185
- actorType: actorType && (actorType === "user" || actorType === "service" || actorType === "system") ? actorType : void 0,
186
- actorDisplay: actorDisplay !== null && actorDisplay !== void 0 ? String(actorDisplay) : void 0,
187
- source: event.source !== null && event.source !== void 0 ? String(event.source) : null
188
- };
178
+ return normalizeAuditEventItem(item);
189
179
  });
190
180
  return {
191
181
  items,
@@ -325,23 +315,45 @@ function Timeline({
325
315
  }) {
326
316
  const { apiBaseUrl, getToken, forceRefreshToken } = useArchiva();
327
317
  const [allEvents, setAllEvents] = React.useState([]);
318
+ const [previousEvents, setPreviousEvents] = React.useState([]);
328
319
  const [cursor, setCursor] = React.useState(void 0);
329
320
  const [loading, setLoading] = React.useState(false);
330
321
  const [error, setError] = React.useState(null);
331
322
  const [hasMore, setHasMore] = React.useState(false);
332
323
  const [searchQuery, setSearchQuery] = React.useState("");
324
+ const queryParamsRef = React.useRef({});
325
+ const queryParamsKeyRef = React.useRef("");
326
+ const queryParams = React.useMemo(() => {
327
+ const params = {
328
+ entityId,
329
+ actorId,
330
+ entityType,
331
+ // Filter by actorType on API side
332
+ actorType: showSystemAndServices ? void 0 : "user",
333
+ limit: initialLimit
334
+ };
335
+ const key = JSON.stringify(params);
336
+ if (key !== queryParamsKeyRef.current) {
337
+ queryParamsKeyRef.current = key;
338
+ queryParamsRef.current = params;
339
+ return params;
340
+ }
341
+ return queryParamsRef.current;
342
+ }, [entityId, actorId, entityType, initialLimit, showSystemAndServices]);
343
+ const allEventsRef = React.useRef([]);
344
+ React.useEffect(() => {
345
+ allEventsRef.current = allEvents;
346
+ }, [allEvents]);
333
347
  const load = React.useCallback(
334
348
  async (options) => {
335
349
  setLoading(true);
336
350
  setError(null);
351
+ if (options?.reset && allEventsRef.current.length > 0) {
352
+ setPreviousEvents(allEventsRef.current);
353
+ }
337
354
  try {
338
355
  const params = {
339
- entityId,
340
- actorId,
341
- entityType,
342
- // Filter by actorType on API side
343
- actorType: showSystemAndServices ? void 0 : "user",
344
- limit: initialLimit,
356
+ ...queryParams,
345
357
  cursor: options?.reset ? void 0 : options?.currentCursor ?? cursor
346
358
  };
347
359
  const response = await fetchEventsWithRetry(
@@ -350,34 +362,54 @@ function Timeline({
350
362
  forceRefreshToken,
351
363
  params
352
364
  );
353
- setAllEvents((prev) => options?.reset ? response.items : [...prev, ...response.items]);
365
+ setAllEvents((prev) => {
366
+ const newEvents = options?.reset ? response.items : [...prev, ...response.items];
367
+ if (options?.reset) {
368
+ setPreviousEvents([]);
369
+ }
370
+ return newEvents;
371
+ });
354
372
  setCursor(response.nextCursor);
355
373
  setHasMore(Boolean(response.nextCursor));
356
374
  } catch (err) {
357
375
  setError(err.message);
376
+ setPreviousEvents((prev) => {
377
+ if (options?.reset && prev.length > 0) {
378
+ setAllEvents(prev);
379
+ return [];
380
+ }
381
+ return prev;
382
+ });
358
383
  } finally {
359
384
  setLoading(false);
360
385
  }
361
386
  },
362
- [entityId, actorId, entityType, initialLimit, apiBaseUrl, getToken, forceRefreshToken, cursor, showSystemAndServices]
387
+ [queryParams, apiBaseUrl, getToken, forceRefreshToken, cursor]
363
388
  );
364
389
  React.useEffect(() => {
365
390
  setCursor(void 0);
366
391
  setAllEvents([]);
392
+ setPreviousEvents([]);
367
393
  void load({ reset: true });
368
- }, [entityId, actorId, entityType, showSystemAndServices]);
394
+ }, [entityId, actorId, entityType, showSystemAndServices, initialLimit]);
395
+ const eventsToFilter = React.useMemo(() => {
396
+ if (loading && allEvents.length === 0 && previousEvents.length > 0) {
397
+ return previousEvents;
398
+ }
399
+ return allEvents;
400
+ }, [allEvents, loading, previousEvents]);
369
401
  const filteredEvents = React.useMemo(() => {
370
- return applyClientSideFilters(allEvents, searchQuery, showSystemAndServices);
371
- }, [allEvents, searchQuery, showSystemAndServices]);
402
+ return applyClientSideFilters(eventsToFilter, searchQuery, showSystemAndServices);
403
+ }, [eventsToFilter, searchQuery, showSystemAndServices]);
372
404
  const timelineItems = React.useMemo(() => {
373
405
  return filteredEvents.slice(0, 10).map(
374
406
  (event) => eventToTimelineItem(event, getActorAvatar)
375
407
  );
376
408
  }, [filteredEvents, getActorAvatar]);
377
- if (loading && allEvents.length === 0) {
409
+ if (loading && allEvents.length === 0 && previousEvents.length === 0) {
378
410
  return /* @__PURE__ */ jsx("div", { className: className || "", style: { width: "100%" }, children: /* @__PURE__ */ jsx("div", { style: { padding: "3rem", textAlign: "center", color: "#6b7280" }, children: "Loading events..." }) });
379
411
  }
380
- if (error) {
412
+ if (error && allEvents.length === 0) {
381
413
  return /* @__PURE__ */ jsx("div", { className: className || "", style: { width: "100%" }, children: /* @__PURE__ */ jsxs("div", { style: { padding: "1rem", color: "#dc2626", backgroundColor: "#fef2f2", borderRadius: "0.375rem" }, children: [
382
414
  "Error: ",
383
415
  error
@@ -393,13 +425,17 @@ function Timeline({
393
425
  placeholder: "Search actions, entities, IDs, actors, sources..."
394
426
  }
395
427
  ) }),
396
- searchQuery && filteredEvents.length !== allEvents.length && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "0.5rem", fontSize: "0.875rem", color: "#6b7280" }, children: [
428
+ (searchQuery || filteredEvents.length !== eventsToFilter.length) && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "0.5rem", fontSize: "0.875rem", color: "#6b7280" }, children: [
397
429
  "Showing ",
398
430
  filteredEvents.length,
399
431
  " of ",
400
- allEvents.length,
432
+ eventsToFilter.length,
401
433
  " events"
402
434
  ] }),
435
+ error && allEvents.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginBottom: "1rem", padding: "0.75rem", color: "#dc2626", backgroundColor: "#fef2f2", borderRadius: "0.375rem", fontSize: "0.875rem" }, children: [
436
+ "Error: ",
437
+ error
438
+ ] }),
403
439
  timelineItems.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "3rem", textAlign: "center", color: "#6b7280" }, children: searchQuery ? "No events match your search." : emptyMessage }) : /* @__PURE__ */ jsx("div", { style: { position: "relative", width: "100%" }, children: timelineItems.map((item, index) => {
404
440
  const useActivityLayout = !!(item.actorDisplay || item.userName || item.userHandle);
405
441
  return /* @__PURE__ */ jsxs(
@@ -1,2 +1,2 @@
1
- export { f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from '../index-BJ8aJsbs.mjs';
1
+ export { f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from '../index-Bk4DxULy.mjs';
2
2
  import 'next/server';
@@ -1,2 +1,2 @@
1
- export { f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from '../index-BJ8aJsbs.js';
1
+ export { f as AuditEventListItem, C as CreateEventInput, e as CreateEventOptions, F as FrontendTokenResponse, G as GET, L as LoadEventsParams, P as PageResult, c as createEvent, a as createEvents, b as createFrontendTokenGET, d as createFrontendTokenRoute, l as loadEvents } from '../index-Bk4DxULy.js';
2
2
  import 'next/server';
@@ -113,6 +113,84 @@ var ArchivaError = class extends Error {
113
113
  }
114
114
  };
115
115
 
116
+ // src/eventNormalizer.ts
117
+ var coerceString = (value) => {
118
+ if (value === null || value === void 0) {
119
+ return void 0;
120
+ }
121
+ if (typeof value === "string") {
122
+ return value;
123
+ }
124
+ if (typeof value === "number" || typeof value === "boolean") {
125
+ return String(value);
126
+ }
127
+ return void 0;
128
+ };
129
+ var coerceNullableString = (value) => {
130
+ if (value === null || value === void 0) {
131
+ return null;
132
+ }
133
+ if (typeof value === "string") {
134
+ return value;
135
+ }
136
+ if (typeof value === "number" || typeof value === "boolean") {
137
+ return String(value);
138
+ }
139
+ return null;
140
+ };
141
+ var coerceActorType = (value) => {
142
+ if (typeof value !== "string") {
143
+ return void 0;
144
+ }
145
+ const normalized = value.toLowerCase();
146
+ if (normalized === "user" || normalized === "service" || normalized === "system") {
147
+ return normalized;
148
+ }
149
+ return void 0;
150
+ };
151
+ var getActorValue = (event, actorRecord, keys) => {
152
+ for (const key of keys) {
153
+ if (Object.prototype.hasOwnProperty.call(event, key)) {
154
+ return event[key];
155
+ }
156
+ if (actorRecord && Object.prototype.hasOwnProperty.call(actorRecord, key)) {
157
+ return actorRecord[key];
158
+ }
159
+ }
160
+ return void 0;
161
+ };
162
+ var normalizeAuditEventItem = (event) => {
163
+ const actorRecord = typeof event.actor === "object" && event.actor !== null ? event.actor : void 0;
164
+ const actorDisplayValue = getActorValue(event, actorRecord, [
165
+ "actorDisplay",
166
+ "actor_display",
167
+ "display",
168
+ "display_name",
169
+ "name"
170
+ ]);
171
+ const actorTypeValue = getActorValue(event, actorRecord, [
172
+ "actorType",
173
+ "actor_type",
174
+ "type"
175
+ ]);
176
+ const actorIdValue = getActorValue(event, actorRecord, [
177
+ "actorId",
178
+ "actor_id",
179
+ "id"
180
+ ]);
181
+ return {
182
+ id: coerceString(event.id) ?? "",
183
+ receivedAt: coerceString(event.receivedAt ?? event.received_at) ?? "",
184
+ action: coerceString(event.action) ?? "",
185
+ entityType: coerceString(event.entityType ?? event.entity_type) ?? "",
186
+ entityId: coerceString(event.entityId ?? event.entity_id) ?? "",
187
+ actorId: coerceNullableString(actorIdValue),
188
+ actorType: coerceActorType(actorTypeValue),
189
+ actorDisplay: coerceString(actorDisplayValue) ?? "",
190
+ source: coerceNullableString(event.source)
191
+ };
192
+ };
193
+
116
194
  // src/client.ts
117
195
  var DEFAULT_BASE_URL = "https://api.archiva.app";
118
196
  function buildHeaders(apiKey, overrides) {
@@ -206,20 +284,7 @@ async function loadEvents(apiKey, params, baseUrl = DEFAULT_BASE_URL) {
206
284
  details: item
207
285
  });
208
286
  }
209
- const event = item;
210
- const actorDisplay = event.actorDisplay !== null && event.actorDisplay !== void 0 ? event.actorDisplay : event.actor_display !== null && event.actor_display !== void 0 ? event.actor_display : void 0;
211
- const actorType = event.actorType !== null && event.actorType !== void 0 ? event.actorType : event.actor_type !== null && event.actor_type !== void 0 ? event.actor_type : void 0;
212
- return {
213
- id: String(event.id ?? ""),
214
- receivedAt: String(event.receivedAt ?? event.received_at ?? ""),
215
- action: String(event.action ?? ""),
216
- entityType: String(event.entityType ?? event.entity_type ?? ""),
217
- entityId: String(event.entityId ?? event.entity_id ?? ""),
218
- actorId: event.actorId !== null && event.actorId !== void 0 ? String(event.actorId) : event.actor_id !== null && event.actor_id !== void 0 ? String(event.actor_id) : null,
219
- actorType: actorType && (actorType === "user" || actorType === "service" || actorType === "system") ? actorType : void 0,
220
- actorDisplay: actorDisplay !== null && actorDisplay !== void 0 ? String(actorDisplay) : void 0,
221
- source: event.source !== null && event.source !== void 0 ? String(event.source) : null
222
- };
287
+ return normalizeAuditEventItem(item);
223
288
  });
224
289
  return {
225
290
  items,
@@ -5,7 +5,8 @@ import {
5
5
  createFrontendTokenGET,
6
6
  createFrontendTokenRoute,
7
7
  loadEvents
8
- } from "../chunk-2YLLG2IF.mjs";
8
+ } from "../chunk-A3Q4YKTK.mjs";
9
+ import "../chunk-WA7TMG65.mjs";
9
10
  export {
10
11
  GET,
11
12
  createEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archiva/archiva-nextjs",
3
- "version": "0.2.92",
3
+ "version": "0.2.94",
4
4
  "description": "Archiva Next.js SDK - Server Actions and Timeline Component",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",