@archiva/archiva-nextjs 0.1.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/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @archiva/archiva-nextjs
2
+
3
+ Next.js SDK for Archiva - Server Actions and Timeline Component
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @archiva/archiva-nextjs
9
+ # or
10
+ pnpm add @archiva/archiva-nextjs
11
+ # or
12
+ yarn add @archiva/archiva-nextjs
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ Set the `ARCHIVA_SECRET_KEY` environment variable in your `.env.local` file:
18
+
19
+ ```env
20
+ ARCHIVA_SECRET_KEY=pk_test_xxxxx
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### ArchivaProvider (Server Component)
26
+
27
+ Wrap your application (or specific routes) with the ArchivaProvider component:
28
+
29
+ ```tsx
30
+ import { ArchivaProvider } from '@archiva/archiva-nextjs';
31
+
32
+ export default function Layout({ children }) {
33
+ return (
34
+ <ArchivaProvider>
35
+ {children}
36
+ </ArchivaProvider>
37
+ );
38
+ }
39
+ ```
40
+
41
+ ### Server Actions
42
+
43
+ #### loadEvents
44
+
45
+ Load audit events with filtering and pagination:
46
+
47
+ ```tsx
48
+ import { loadEvents } from '@archiva/archiva-nextjs';
49
+
50
+ // In a Server Component or Server Action
51
+ const events = await loadEvents({
52
+ entityId: 'entity_123',
53
+ actorId: 'actor_456',
54
+ entityType: 'invoice',
55
+ limit: 25,
56
+ cursor: 'optional_cursor',
57
+ });
58
+ ```
59
+
60
+ #### createEvent
61
+
62
+ Create a single audit event:
63
+
64
+ ```tsx
65
+ import { createEvent } from '@archiva/archiva-nextjs';
66
+
67
+ const result = await createEvent({
68
+ action: 'update',
69
+ entityType: 'invoice',
70
+ entityId: 'inv_123',
71
+ actorType: 'user',
72
+ actorId: 'usr_123',
73
+ actorDisplay: 'John Doe',
74
+ occurredAt: new Date().toISOString(),
75
+ source: 'web',
76
+ context: {
77
+ requestId: 'req_123',
78
+ },
79
+ changes: [
80
+ {
81
+ op: 'set',
82
+ path: 'status',
83
+ before: 'draft',
84
+ after: 'sent',
85
+ },
86
+ ],
87
+ });
88
+ ```
89
+
90
+ #### createEvents (Bulk)
91
+
92
+ Create multiple events at once:
93
+
94
+ ```tsx
95
+ import { createEvents } from '@archiva/archiva-nextjs';
96
+
97
+ const result = await createEvents([
98
+ {
99
+ action: 'create',
100
+ entityType: 'invoice',
101
+ entityId: 'inv_123',
102
+ // ... other fields
103
+ },
104
+ {
105
+ action: 'update',
106
+ entityType: 'invoice',
107
+ entityId: 'inv_123',
108
+ // ... other fields
109
+ },
110
+ ]);
111
+ ```
112
+
113
+ ### Timeline Component (Client Component)
114
+
115
+ Display a timeline of audit events:
116
+
117
+ ```tsx
118
+ 'use client';
119
+
120
+ import { Timeline } from '@archiva/archiva-nextjs';
121
+
122
+ export function InvoiceTimeline({ invoiceId }: { invoiceId: string }) {
123
+ return (
124
+ <Timeline
125
+ entityId={invoiceId}
126
+ entityType="invoice"
127
+ initialLimit={25}
128
+ />
129
+ );
130
+ }
131
+ ```
132
+
133
+ ## API Reference
134
+
135
+ ### LoadEventsParams
136
+
137
+ ```ts
138
+ type LoadEventsParams = {
139
+ entityId?: string;
140
+ actorId?: string;
141
+ entityType?: string;
142
+ limit?: number;
143
+ cursor?: string;
144
+ };
145
+ ```
146
+
147
+ ### CreateEventInput
148
+
149
+ ```ts
150
+ type CreateEventInput = {
151
+ action: string;
152
+ entityType: string;
153
+ entityId: string;
154
+ actorType?: string;
155
+ actorId?: string;
156
+ actorDisplay?: string;
157
+ occurredAt?: string;
158
+ source?: string;
159
+ context?: Record<string, unknown>;
160
+ changes?: EventChange[];
161
+ };
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,125 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type ArchivaProviderProps = {
5
+ children: ReactNode;
6
+ apiKey?: string;
7
+ };
8
+ /**
9
+ * Server-only provider component for Archiva.
10
+ * This component validates the API key format and provides configuration
11
+ * for server actions (loadEvents, createEvents).
12
+ *
13
+ * The API key should be passed via props or set as ARCHIVA_SECRET_KEY environment variable.
14
+ */
15
+ declare function ArchivaProvider({ children }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
16
+
17
+ type EventChange = {
18
+ op: "set" | "unset" | "add" | "remove" | "replace" | string;
19
+ path: string;
20
+ before?: unknown;
21
+ after?: unknown;
22
+ };
23
+ type CreateEventInput = {
24
+ action: string;
25
+ entityType: string;
26
+ entityId: string;
27
+ actorType?: string;
28
+ actorId?: string;
29
+ actorDisplay?: string;
30
+ occurredAt?: string;
31
+ source?: string;
32
+ context?: Record<string, unknown>;
33
+ changes?: EventChange[];
34
+ };
35
+ type CreateEventOptions = {
36
+ idempotencyKey?: string;
37
+ requestId?: string;
38
+ };
39
+ type AuditEventListItem = {
40
+ id: string;
41
+ receivedAt: string;
42
+ action: string;
43
+ entityType: string;
44
+ entityId: string;
45
+ actorId: string | null;
46
+ source: string | null;
47
+ };
48
+ type PageResult<T> = {
49
+ items: T[];
50
+ nextCursor?: string;
51
+ };
52
+ type LoadEventsParams = {
53
+ entityId?: string;
54
+ actorId?: string;
55
+ entityType?: string;
56
+ limit?: number;
57
+ cursor?: string;
58
+ };
59
+ declare class ArchivaError extends Error {
60
+ statusCode: number;
61
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
62
+ retryAfterSeconds?: number;
63
+ details?: unknown;
64
+ constructor(params: {
65
+ statusCode: number;
66
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
67
+ message: string;
68
+ retryAfterSeconds?: number;
69
+ details?: unknown;
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Server action to load audit events
75
+ *
76
+ * @param params - Query parameters for filtering events
77
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
78
+ * @returns Paginated list of audit events
79
+ */
80
+ declare function loadEvents(params: LoadEventsParams, apiKey?: string): Promise<PageResult<AuditEventListItem>>;
81
+ /**
82
+ * Server action to create a single audit event
83
+ *
84
+ * @param event - Event data to create
85
+ * @param options - Optional idempotency and request ID options
86
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
87
+ * @returns Created event ID and replay status
88
+ */
89
+ declare function createEvent(event: CreateEventInput, options?: CreateEventOptions, apiKey?: string): Promise<{
90
+ eventId: string;
91
+ replayed: boolean;
92
+ }>;
93
+ /**
94
+ * Server action to create multiple audit events (bulk)
95
+ *
96
+ * @param events - Array of events to create
97
+ * @param options - Optional idempotency and request ID options
98
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
99
+ * @returns Array of created event IDs
100
+ */
101
+ declare function createEvents(events: CreateEventInput[], options?: CreateEventOptions, apiKey?: string): Promise<{
102
+ eventIds: string[];
103
+ }>;
104
+
105
+ type TimelineItem = {
106
+ id: string | number;
107
+ title: string;
108
+ description?: string;
109
+ timestamp: Date | string;
110
+ badge?: string | number;
111
+ data?: unknown;
112
+ className?: string;
113
+ };
114
+ type TimelineProps = {
115
+ entityId?: string;
116
+ actorId?: string;
117
+ entityType?: string;
118
+ initialLimit?: number;
119
+ className?: string;
120
+ emptyMessage?: string;
121
+ apiKey?: string;
122
+ };
123
+ declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, apiKey, }: TimelineProps): react_jsx_runtime.JSX.Element;
124
+
125
+ export { ArchivaError, ArchivaProvider, type ArchivaProviderProps, type AuditEventListItem, type CreateEventInput, type CreateEventOptions, type EventChange, type LoadEventsParams, type PageResult, Timeline, type TimelineItem, type TimelineProps, createEvent, createEvents, loadEvents };
@@ -0,0 +1,125 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ type ArchivaProviderProps = {
5
+ children: ReactNode;
6
+ apiKey?: string;
7
+ };
8
+ /**
9
+ * Server-only provider component for Archiva.
10
+ * This component validates the API key format and provides configuration
11
+ * for server actions (loadEvents, createEvents).
12
+ *
13
+ * The API key should be passed via props or set as ARCHIVA_SECRET_KEY environment variable.
14
+ */
15
+ declare function ArchivaProvider({ children }: ArchivaProviderProps): react_jsx_runtime.JSX.Element;
16
+
17
+ type EventChange = {
18
+ op: "set" | "unset" | "add" | "remove" | "replace" | string;
19
+ path: string;
20
+ before?: unknown;
21
+ after?: unknown;
22
+ };
23
+ type CreateEventInput = {
24
+ action: string;
25
+ entityType: string;
26
+ entityId: string;
27
+ actorType?: string;
28
+ actorId?: string;
29
+ actorDisplay?: string;
30
+ occurredAt?: string;
31
+ source?: string;
32
+ context?: Record<string, unknown>;
33
+ changes?: EventChange[];
34
+ };
35
+ type CreateEventOptions = {
36
+ idempotencyKey?: string;
37
+ requestId?: string;
38
+ };
39
+ type AuditEventListItem = {
40
+ id: string;
41
+ receivedAt: string;
42
+ action: string;
43
+ entityType: string;
44
+ entityId: string;
45
+ actorId: string | null;
46
+ source: string | null;
47
+ };
48
+ type PageResult<T> = {
49
+ items: T[];
50
+ nextCursor?: string;
51
+ };
52
+ type LoadEventsParams = {
53
+ entityId?: string;
54
+ actorId?: string;
55
+ entityType?: string;
56
+ limit?: number;
57
+ cursor?: string;
58
+ };
59
+ declare class ArchivaError extends Error {
60
+ statusCode: number;
61
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
62
+ retryAfterSeconds?: number;
63
+ details?: unknown;
64
+ constructor(params: {
65
+ statusCode: number;
66
+ code: "UNAUTHORIZED" | "FORBIDDEN" | "PAYLOAD_TOO_LARGE" | "RATE_LIMITED" | "IDEMPOTENCY_CONFLICT" | "HTTP_ERROR";
67
+ message: string;
68
+ retryAfterSeconds?: number;
69
+ details?: unknown;
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Server action to load audit events
75
+ *
76
+ * @param params - Query parameters for filtering events
77
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
78
+ * @returns Paginated list of audit events
79
+ */
80
+ declare function loadEvents(params: LoadEventsParams, apiKey?: string): Promise<PageResult<AuditEventListItem>>;
81
+ /**
82
+ * Server action to create a single audit event
83
+ *
84
+ * @param event - Event data to create
85
+ * @param options - Optional idempotency and request ID options
86
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
87
+ * @returns Created event ID and replay status
88
+ */
89
+ declare function createEvent(event: CreateEventInput, options?: CreateEventOptions, apiKey?: string): Promise<{
90
+ eventId: string;
91
+ replayed: boolean;
92
+ }>;
93
+ /**
94
+ * Server action to create multiple audit events (bulk)
95
+ *
96
+ * @param events - Array of events to create
97
+ * @param options - Optional idempotency and request ID options
98
+ * @param apiKey - Optional API key (otherwise uses ARCHIVA_SECRET_KEY env var)
99
+ * @returns Array of created event IDs
100
+ */
101
+ declare function createEvents(events: CreateEventInput[], options?: CreateEventOptions, apiKey?: string): Promise<{
102
+ eventIds: string[];
103
+ }>;
104
+
105
+ type TimelineItem = {
106
+ id: string | number;
107
+ title: string;
108
+ description?: string;
109
+ timestamp: Date | string;
110
+ badge?: string | number;
111
+ data?: unknown;
112
+ className?: string;
113
+ };
114
+ type TimelineProps = {
115
+ entityId?: string;
116
+ actorId?: string;
117
+ entityType?: string;
118
+ initialLimit?: number;
119
+ className?: string;
120
+ emptyMessage?: string;
121
+ apiKey?: string;
122
+ };
123
+ declare function Timeline({ entityId, actorId, entityType, initialLimit, className, emptyMessage, apiKey, }: TimelineProps): react_jsx_runtime.JSX.Element;
124
+
125
+ export { ArchivaError, ArchivaProvider, type ArchivaProviderProps, type AuditEventListItem, type CreateEventInput, type CreateEventOptions, type EventChange, type LoadEventsParams, type PageResult, Timeline, type TimelineItem, type TimelineProps, createEvent, createEvents, loadEvents };
package/dist/index.js ADDED
@@ -0,0 +1,382 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.tsx
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ArchivaProvider: () => ArchivaProvider,
34
+ Timeline: () => Timeline,
35
+ createEvent: () => createEvent2,
36
+ createEvents: () => createEvents2,
37
+ loadEvents: () => loadEvents2
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/provider.tsx
42
+ var import_jsx_runtime = require("react/jsx-runtime");
43
+ function ArchivaProvider({ children }) {
44
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
45
+ }
46
+
47
+ // src/client.ts
48
+ var DEFAULT_BASE_URL = "https://api.archiva.app";
49
+ function buildHeaders(apiKey, overrides) {
50
+ const headers = new Headers(overrides);
51
+ headers.set("X-Project-Key", apiKey);
52
+ return headers;
53
+ }
54
+ async function parseError(response) {
55
+ const retryAfterHeader = response.headers.get("Retry-After");
56
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : void 0;
57
+ let payload = void 0;
58
+ try {
59
+ payload = await response.json();
60
+ } catch {
61
+ payload = void 0;
62
+ }
63
+ const errorMessage = typeof payload === "object" && payload !== null && "error" in payload ? String(payload.error) : response.statusText;
64
+ let code = "HTTP_ERROR";
65
+ if (response.status === 401) {
66
+ code = "UNAUTHORIZED";
67
+ } else if (response.status === 403) {
68
+ code = "FORBIDDEN";
69
+ } else if (response.status === 413) {
70
+ code = "PAYLOAD_TOO_LARGE";
71
+ } else if (response.status === 429) {
72
+ code = "RATE_LIMITED";
73
+ } else if (response.status === 409) {
74
+ code = "IDEMPOTENCY_CONFLICT";
75
+ }
76
+ throw new ArchivaError({
77
+ statusCode: response.status,
78
+ code,
79
+ message: errorMessage,
80
+ retryAfterSeconds,
81
+ details: payload
82
+ });
83
+ }
84
+ function createRequestId() {
85
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
86
+ return crypto.randomUUID();
87
+ }
88
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
89
+ }
90
+ function createIdempotencyKey() {
91
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
92
+ return `idem_${crypto.randomUUID()}`;
93
+ }
94
+ return `idem_${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
95
+ }
96
+ async function loadEvents(apiKey, params, baseUrl = DEFAULT_BASE_URL) {
97
+ const url = new URL(`${baseUrl}/api/events`);
98
+ if (params.entityId) {
99
+ url.searchParams.set("entityId", params.entityId);
100
+ }
101
+ if (params.actorId) {
102
+ url.searchParams.set("actorId", params.actorId);
103
+ }
104
+ if (params.entityType) {
105
+ url.searchParams.set("entityType", params.entityType);
106
+ }
107
+ if (params.limit) {
108
+ url.searchParams.set("limit", String(params.limit));
109
+ }
110
+ if (params.cursor) {
111
+ url.searchParams.set("cursor", params.cursor);
112
+ }
113
+ const response = await fetch(url.toString(), {
114
+ headers: buildHeaders(apiKey)
115
+ });
116
+ if (!response.ok) {
117
+ await parseError(response);
118
+ }
119
+ const payload = await response.json();
120
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) {
121
+ throw new ArchivaError({
122
+ statusCode: response.status,
123
+ code: "HTTP_ERROR",
124
+ message: "Invalid response format",
125
+ details: payload
126
+ });
127
+ }
128
+ return {
129
+ items: payload.items,
130
+ nextCursor: typeof payload.nextCursor === "string" ? payload.nextCursor : void 0
131
+ };
132
+ }
133
+ async function createEvent(apiKey, event, options, baseUrl = DEFAULT_BASE_URL) {
134
+ const idempotencyKey = options?.idempotencyKey ?? createIdempotencyKey();
135
+ const requestId = options?.requestId ?? createRequestId();
136
+ const headers = buildHeaders(apiKey, {
137
+ "Content-Type": "application/json",
138
+ "Idempotency-Key": idempotencyKey,
139
+ "X-Request-Id": requestId
140
+ });
141
+ const response = await fetch(`${baseUrl}/api/ingest/event`, {
142
+ method: "POST",
143
+ headers,
144
+ body: JSON.stringify(event)
145
+ });
146
+ if (!response.ok) {
147
+ await parseError(response);
148
+ }
149
+ const payload = await response.json();
150
+ if (!payload || typeof payload !== "object" || typeof payload.eventId !== "string") {
151
+ throw new ArchivaError({
152
+ statusCode: response.status,
153
+ code: "HTTP_ERROR",
154
+ message: "Invalid response format",
155
+ details: payload
156
+ });
157
+ }
158
+ return {
159
+ eventId: payload.eventId,
160
+ replayed: response.status === 200
161
+ };
162
+ }
163
+ async function createEvents(apiKey, events, options, baseUrl = DEFAULT_BASE_URL) {
164
+ const results = await Promise.all(
165
+ events.map(
166
+ (event, index) => createEvent(
167
+ apiKey,
168
+ event,
169
+ {
170
+ ...options,
171
+ idempotencyKey: options?.idempotencyKey ? `${options.idempotencyKey}_${index}` : void 0
172
+ },
173
+ baseUrl
174
+ )
175
+ )
176
+ );
177
+ return {
178
+ eventIds: results.map((r) => r.eventId)
179
+ };
180
+ }
181
+
182
+ // src/actions.ts
183
+ var DEFAULT_BASE_URL2 = "https://api.archiva.app";
184
+ function getApiKey(apiKey) {
185
+ const resolvedKey = apiKey || process.env.ARCHIVA_SECRET_KEY;
186
+ if (!resolvedKey) {
187
+ throw new Error("ARCHIVA_SECRET_KEY environment variable is required, or provide apiKey prop to ArchivaProvider");
188
+ }
189
+ return resolvedKey;
190
+ }
191
+ async function loadEvents2(params, apiKey) {
192
+ const resolvedApiKey = getApiKey(apiKey);
193
+ return loadEvents(resolvedApiKey, params, DEFAULT_BASE_URL2);
194
+ }
195
+ async function createEvent2(event, options, apiKey) {
196
+ const resolvedApiKey = getApiKey(apiKey);
197
+ return createEvent(resolvedApiKey, event, options, DEFAULT_BASE_URL2);
198
+ }
199
+ async function createEvents2(events, options, apiKey) {
200
+ const resolvedApiKey = getApiKey(apiKey);
201
+ return createEvents(resolvedApiKey, events, options, DEFAULT_BASE_URL2);
202
+ }
203
+
204
+ // src/timeline.tsx
205
+ var React = __toESM(require("react"));
206
+ var import_jsx_runtime2 = require("react/jsx-runtime");
207
+ function formatTimestamp(timestamp) {
208
+ const date = typeof timestamp === "string" ? new Date(timestamp) : timestamp;
209
+ return date.toLocaleDateString(void 0, {
210
+ month: "short",
211
+ day: "numeric",
212
+ year: "numeric",
213
+ hour: "numeric",
214
+ minute: "2-digit"
215
+ });
216
+ }
217
+ function eventToTimelineItem(event) {
218
+ return {
219
+ id: event.id,
220
+ title: event.action,
221
+ description: `${event.entityType} ${event.entityId}`,
222
+ timestamp: event.receivedAt,
223
+ badge: event.actorId ?? void 0,
224
+ data: event
225
+ };
226
+ }
227
+ function Timeline({
228
+ entityId,
229
+ actorId,
230
+ entityType,
231
+ initialLimit = 25,
232
+ className,
233
+ emptyMessage = "No events yet.",
234
+ apiKey
235
+ }) {
236
+ const [items, setItems] = React.useState([]);
237
+ const [cursor, setCursor] = React.useState(void 0);
238
+ const [loading, setLoading] = React.useState(false);
239
+ const [error, setError] = React.useState(null);
240
+ const [hasMore, setHasMore] = React.useState(false);
241
+ const load = React.useCallback(async (options) => {
242
+ setLoading(true);
243
+ setError(null);
244
+ try {
245
+ const params = {
246
+ entityId,
247
+ actorId,
248
+ entityType,
249
+ limit: initialLimit,
250
+ cursor: options?.reset ? void 0 : options?.currentCursor ?? cursor
251
+ };
252
+ const response = await loadEvents2(params, apiKey);
253
+ const newItems = response.items.map(eventToTimelineItem);
254
+ setItems((prev) => options?.reset ? newItems : [...prev, ...newItems]);
255
+ setCursor(response.nextCursor);
256
+ setHasMore(Boolean(response.nextCursor));
257
+ } catch (err) {
258
+ setError(err.message);
259
+ } finally {
260
+ setLoading(false);
261
+ }
262
+ }, [entityId, actorId, entityType, initialLimit, apiKey]);
263
+ React.useEffect(() => {
264
+ setCursor(void 0);
265
+ setItems([]);
266
+ void load({ reset: true });
267
+ }, [entityId, actorId, entityType, load]);
268
+ if (loading && items.length === 0) {
269
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: "Loading events..." }) });
270
+ }
271
+ if (error) {
272
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { color: "red" }, children: [
273
+ "Error: ",
274
+ error
275
+ ] }) });
276
+ }
277
+ if (items.length === 0) {
278
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: className || "", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: emptyMessage }) });
279
+ }
280
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: className || "", style: { width: "100%" }, children: [
281
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { position: "relative" }, children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
282
+ "div",
283
+ {
284
+ style: {
285
+ position: "relative",
286
+ display: "flex",
287
+ gap: "1rem",
288
+ paddingBottom: index < items.length - 1 ? "2rem" : "0"
289
+ },
290
+ children: [
291
+ index < items.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
292
+ "div",
293
+ {
294
+ style: {
295
+ position: "absolute",
296
+ left: "1.25rem",
297
+ top: "3rem",
298
+ height: "calc(100% - 3rem)",
299
+ width: "2px",
300
+ backgroundColor: "#e5e7eb"
301
+ }
302
+ }
303
+ ),
304
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
305
+ "div",
306
+ {
307
+ style: {
308
+ position: "relative",
309
+ zIndex: 10,
310
+ width: "2.5rem",
311
+ height: "2.5rem",
312
+ borderRadius: "50%",
313
+ backgroundColor: "#f3f4f6",
314
+ border: "2px solid #fff",
315
+ display: "flex",
316
+ alignItems: "center",
317
+ justifyContent: "center",
318
+ flexShrink: 0
319
+ },
320
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
321
+ "div",
322
+ {
323
+ style: {
324
+ width: "0.75rem",
325
+ height: "0.75rem",
326
+ borderRadius: "50%",
327
+ backgroundColor: "#6b7280"
328
+ }
329
+ }
330
+ )
331
+ }
332
+ ),
333
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { flex: 1, paddingBottom: "2rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", alignItems: "start", justifyContent: "space-between", gap: "0.5rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { flex: 1 }, children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
335
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h4", { style: { fontWeight: 600, margin: 0 }, children: item.title }),
336
+ item.badge && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
337
+ "span",
338
+ {
339
+ style: {
340
+ fontSize: "0.75rem",
341
+ padding: "0.125rem 0.5rem",
342
+ borderRadius: "0.375rem",
343
+ backgroundColor: "#f3f4f6"
344
+ },
345
+ children: item.badge
346
+ }
347
+ )
348
+ ] }),
349
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontSize: "0.875rem", color: "#6b7280", margin: "0.25rem 0 0 0" }, children: formatTimestamp(item.timestamp) }),
350
+ item.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontSize: "0.875rem", color: "#6b7280", margin: "0.5rem 0 0 0" }, children: item.description })
351
+ ] }) }) })
352
+ ]
353
+ },
354
+ item.id
355
+ )) }),
356
+ hasMore && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginTop: "1rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
357
+ "button",
358
+ {
359
+ type: "button",
360
+ onClick: () => load(),
361
+ disabled: loading,
362
+ style: {
363
+ padding: "0.5rem 1rem",
364
+ backgroundColor: loading ? "#d1d5db" : "#3b82f6",
365
+ color: "white",
366
+ border: "none",
367
+ borderRadius: "0.375rem",
368
+ cursor: loading ? "not-allowed" : "pointer"
369
+ },
370
+ children: loading ? "Loading..." : "Load more"
371
+ }
372
+ ) })
373
+ ] });
374
+ }
375
+ // Annotate the CommonJS export names for ESM import in node:
376
+ 0 && (module.exports = {
377
+ ArchivaProvider,
378
+ Timeline,
379
+ createEvent,
380
+ createEvents,
381
+ loadEvents
382
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,341 @@
1
+ // src/provider.tsx
2
+ import { Fragment, jsx } from "react/jsx-runtime";
3
+ function ArchivaProvider({ children }) {
4
+ return /* @__PURE__ */ jsx(Fragment, { children });
5
+ }
6
+
7
+ // src/client.ts
8
+ var DEFAULT_BASE_URL = "https://api.archiva.app";
9
+ function buildHeaders(apiKey, overrides) {
10
+ const headers = new Headers(overrides);
11
+ headers.set("X-Project-Key", apiKey);
12
+ return headers;
13
+ }
14
+ async function parseError(response) {
15
+ const retryAfterHeader = response.headers.get("Retry-After");
16
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : void 0;
17
+ let payload = void 0;
18
+ try {
19
+ payload = await response.json();
20
+ } catch {
21
+ payload = void 0;
22
+ }
23
+ const errorMessage = typeof payload === "object" && payload !== null && "error" in payload ? String(payload.error) : response.statusText;
24
+ let code = "HTTP_ERROR";
25
+ if (response.status === 401) {
26
+ code = "UNAUTHORIZED";
27
+ } else if (response.status === 403) {
28
+ code = "FORBIDDEN";
29
+ } else if (response.status === 413) {
30
+ code = "PAYLOAD_TOO_LARGE";
31
+ } else if (response.status === 429) {
32
+ code = "RATE_LIMITED";
33
+ } else if (response.status === 409) {
34
+ code = "IDEMPOTENCY_CONFLICT";
35
+ }
36
+ throw new ArchivaError({
37
+ statusCode: response.status,
38
+ code,
39
+ message: errorMessage,
40
+ retryAfterSeconds,
41
+ details: payload
42
+ });
43
+ }
44
+ function createRequestId() {
45
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
46
+ return crypto.randomUUID();
47
+ }
48
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
49
+ }
50
+ function createIdempotencyKey() {
51
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
52
+ return `idem_${crypto.randomUUID()}`;
53
+ }
54
+ return `idem_${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
55
+ }
56
+ async function loadEvents(apiKey, params, baseUrl = DEFAULT_BASE_URL) {
57
+ const url = new URL(`${baseUrl}/api/events`);
58
+ if (params.entityId) {
59
+ url.searchParams.set("entityId", params.entityId);
60
+ }
61
+ if (params.actorId) {
62
+ url.searchParams.set("actorId", params.actorId);
63
+ }
64
+ if (params.entityType) {
65
+ url.searchParams.set("entityType", params.entityType);
66
+ }
67
+ if (params.limit) {
68
+ url.searchParams.set("limit", String(params.limit));
69
+ }
70
+ if (params.cursor) {
71
+ url.searchParams.set("cursor", params.cursor);
72
+ }
73
+ const response = await fetch(url.toString(), {
74
+ headers: buildHeaders(apiKey)
75
+ });
76
+ if (!response.ok) {
77
+ await parseError(response);
78
+ }
79
+ const payload = await response.json();
80
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) {
81
+ throw new ArchivaError({
82
+ statusCode: response.status,
83
+ code: "HTTP_ERROR",
84
+ message: "Invalid response format",
85
+ details: payload
86
+ });
87
+ }
88
+ return {
89
+ items: payload.items,
90
+ nextCursor: typeof payload.nextCursor === "string" ? payload.nextCursor : void 0
91
+ };
92
+ }
93
+ async function createEvent(apiKey, event, options, baseUrl = DEFAULT_BASE_URL) {
94
+ const idempotencyKey = options?.idempotencyKey ?? createIdempotencyKey();
95
+ const requestId = options?.requestId ?? createRequestId();
96
+ const headers = buildHeaders(apiKey, {
97
+ "Content-Type": "application/json",
98
+ "Idempotency-Key": idempotencyKey,
99
+ "X-Request-Id": requestId
100
+ });
101
+ const response = await fetch(`${baseUrl}/api/ingest/event`, {
102
+ method: "POST",
103
+ headers,
104
+ body: JSON.stringify(event)
105
+ });
106
+ if (!response.ok) {
107
+ await parseError(response);
108
+ }
109
+ const payload = await response.json();
110
+ if (!payload || typeof payload !== "object" || typeof payload.eventId !== "string") {
111
+ throw new ArchivaError({
112
+ statusCode: response.status,
113
+ code: "HTTP_ERROR",
114
+ message: "Invalid response format",
115
+ details: payload
116
+ });
117
+ }
118
+ return {
119
+ eventId: payload.eventId,
120
+ replayed: response.status === 200
121
+ };
122
+ }
123
+ async function createEvents(apiKey, events, options, baseUrl = DEFAULT_BASE_URL) {
124
+ const results = await Promise.all(
125
+ events.map(
126
+ (event, index) => createEvent(
127
+ apiKey,
128
+ event,
129
+ {
130
+ ...options,
131
+ idempotencyKey: options?.idempotencyKey ? `${options.idempotencyKey}_${index}` : void 0
132
+ },
133
+ baseUrl
134
+ )
135
+ )
136
+ );
137
+ return {
138
+ eventIds: results.map((r) => r.eventId)
139
+ };
140
+ }
141
+
142
+ // src/actions.ts
143
+ var DEFAULT_BASE_URL2 = "https://api.archiva.app";
144
+ function getApiKey(apiKey) {
145
+ const resolvedKey = apiKey || process.env.ARCHIVA_SECRET_KEY;
146
+ if (!resolvedKey) {
147
+ throw new Error("ARCHIVA_SECRET_KEY environment variable is required, or provide apiKey prop to ArchivaProvider");
148
+ }
149
+ return resolvedKey;
150
+ }
151
+ async function loadEvents2(params, apiKey) {
152
+ const resolvedApiKey = getApiKey(apiKey);
153
+ return loadEvents(resolvedApiKey, params, DEFAULT_BASE_URL2);
154
+ }
155
+ async function createEvent2(event, options, apiKey) {
156
+ const resolvedApiKey = getApiKey(apiKey);
157
+ return createEvent(resolvedApiKey, event, options, DEFAULT_BASE_URL2);
158
+ }
159
+ async function createEvents2(events, options, apiKey) {
160
+ const resolvedApiKey = getApiKey(apiKey);
161
+ return createEvents(resolvedApiKey, events, options, DEFAULT_BASE_URL2);
162
+ }
163
+
164
+ // src/timeline.tsx
165
+ import * as React from "react";
166
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
167
+ function formatTimestamp(timestamp) {
168
+ const date = typeof timestamp === "string" ? new Date(timestamp) : timestamp;
169
+ return date.toLocaleDateString(void 0, {
170
+ month: "short",
171
+ day: "numeric",
172
+ year: "numeric",
173
+ hour: "numeric",
174
+ minute: "2-digit"
175
+ });
176
+ }
177
+ function eventToTimelineItem(event) {
178
+ return {
179
+ id: event.id,
180
+ title: event.action,
181
+ description: `${event.entityType} ${event.entityId}`,
182
+ timestamp: event.receivedAt,
183
+ badge: event.actorId ?? void 0,
184
+ data: event
185
+ };
186
+ }
187
+ function Timeline({
188
+ entityId,
189
+ actorId,
190
+ entityType,
191
+ initialLimit = 25,
192
+ className,
193
+ emptyMessage = "No events yet.",
194
+ apiKey
195
+ }) {
196
+ const [items, setItems] = React.useState([]);
197
+ const [cursor, setCursor] = React.useState(void 0);
198
+ const [loading, setLoading] = React.useState(false);
199
+ const [error, setError] = React.useState(null);
200
+ const [hasMore, setHasMore] = React.useState(false);
201
+ const load = React.useCallback(async (options) => {
202
+ setLoading(true);
203
+ setError(null);
204
+ try {
205
+ const params = {
206
+ entityId,
207
+ actorId,
208
+ entityType,
209
+ limit: initialLimit,
210
+ cursor: options?.reset ? void 0 : options?.currentCursor ?? cursor
211
+ };
212
+ const response = await loadEvents2(params, apiKey);
213
+ const newItems = response.items.map(eventToTimelineItem);
214
+ setItems((prev) => options?.reset ? newItems : [...prev, ...newItems]);
215
+ setCursor(response.nextCursor);
216
+ setHasMore(Boolean(response.nextCursor));
217
+ } catch (err) {
218
+ setError(err.message);
219
+ } finally {
220
+ setLoading(false);
221
+ }
222
+ }, [entityId, actorId, entityType, initialLimit, apiKey]);
223
+ React.useEffect(() => {
224
+ setCursor(void 0);
225
+ setItems([]);
226
+ void load({ reset: true });
227
+ }, [entityId, actorId, entityType, load]);
228
+ if (loading && items.length === 0) {
229
+ return /* @__PURE__ */ jsx2("div", { className: className || "", children: /* @__PURE__ */ jsx2("div", { children: "Loading events..." }) });
230
+ }
231
+ if (error) {
232
+ return /* @__PURE__ */ jsx2("div", { className: className || "", children: /* @__PURE__ */ jsxs("div", { style: { color: "red" }, children: [
233
+ "Error: ",
234
+ error
235
+ ] }) });
236
+ }
237
+ if (items.length === 0) {
238
+ return /* @__PURE__ */ jsx2("div", { className: className || "", children: /* @__PURE__ */ jsx2("div", { children: emptyMessage }) });
239
+ }
240
+ return /* @__PURE__ */ jsxs("div", { className: className || "", style: { width: "100%" }, children: [
241
+ /* @__PURE__ */ jsx2("div", { style: { position: "relative" }, children: items.map((item, index) => /* @__PURE__ */ jsxs(
242
+ "div",
243
+ {
244
+ style: {
245
+ position: "relative",
246
+ display: "flex",
247
+ gap: "1rem",
248
+ paddingBottom: index < items.length - 1 ? "2rem" : "0"
249
+ },
250
+ children: [
251
+ index < items.length - 1 && /* @__PURE__ */ jsx2(
252
+ "div",
253
+ {
254
+ style: {
255
+ position: "absolute",
256
+ left: "1.25rem",
257
+ top: "3rem",
258
+ height: "calc(100% - 3rem)",
259
+ width: "2px",
260
+ backgroundColor: "#e5e7eb"
261
+ }
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx2(
265
+ "div",
266
+ {
267
+ style: {
268
+ position: "relative",
269
+ zIndex: 10,
270
+ width: "2.5rem",
271
+ height: "2.5rem",
272
+ borderRadius: "50%",
273
+ backgroundColor: "#f3f4f6",
274
+ border: "2px solid #fff",
275
+ display: "flex",
276
+ alignItems: "center",
277
+ justifyContent: "center",
278
+ flexShrink: 0
279
+ },
280
+ children: /* @__PURE__ */ jsx2(
281
+ "div",
282
+ {
283
+ style: {
284
+ width: "0.75rem",
285
+ height: "0.75rem",
286
+ borderRadius: "50%",
287
+ backgroundColor: "#6b7280"
288
+ }
289
+ }
290
+ )
291
+ }
292
+ ),
293
+ /* @__PURE__ */ jsx2("div", { style: { flex: 1, paddingBottom: "2rem" }, children: /* @__PURE__ */ jsx2("div", { style: { display: "flex", alignItems: "start", justifyContent: "space-between", gap: "0.5rem" }, children: /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
294
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [
295
+ /* @__PURE__ */ jsx2("h4", { style: { fontWeight: 600, margin: 0 }, children: item.title }),
296
+ item.badge && /* @__PURE__ */ jsx2(
297
+ "span",
298
+ {
299
+ style: {
300
+ fontSize: "0.75rem",
301
+ padding: "0.125rem 0.5rem",
302
+ borderRadius: "0.375rem",
303
+ backgroundColor: "#f3f4f6"
304
+ },
305
+ children: item.badge
306
+ }
307
+ )
308
+ ] }),
309
+ /* @__PURE__ */ jsx2("p", { style: { fontSize: "0.875rem", color: "#6b7280", margin: "0.25rem 0 0 0" }, children: formatTimestamp(item.timestamp) }),
310
+ item.description && /* @__PURE__ */ jsx2("p", { style: { fontSize: "0.875rem", color: "#6b7280", margin: "0.5rem 0 0 0" }, children: item.description })
311
+ ] }) }) })
312
+ ]
313
+ },
314
+ item.id
315
+ )) }),
316
+ hasMore && /* @__PURE__ */ jsx2("div", { style: { marginTop: "1rem" }, children: /* @__PURE__ */ jsx2(
317
+ "button",
318
+ {
319
+ type: "button",
320
+ onClick: () => load(),
321
+ disabled: loading,
322
+ style: {
323
+ padding: "0.5rem 1rem",
324
+ backgroundColor: loading ? "#d1d5db" : "#3b82f6",
325
+ color: "white",
326
+ border: "none",
327
+ borderRadius: "0.375rem",
328
+ cursor: loading ? "not-allowed" : "pointer"
329
+ },
330
+ children: loading ? "Loading..." : "Load more"
331
+ }
332
+ ) })
333
+ ] });
334
+ }
335
+ export {
336
+ ArchivaProvider,
337
+ Timeline,
338
+ createEvent2 as createEvent,
339
+ createEvents2 as createEvents,
340
+ loadEvents2 as loadEvents
341
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@archiva/archiva-nextjs",
3
+ "version": "0.1.0",
4
+ "description": "Archiva Next.js SDK - Server Actions and Timeline Component",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "scripts": {
17
+ "build": "tsup src/index.tsx --format esm,cjs --dts",
18
+ "test": "vitest run --config vitest.config.ts"
19
+ },
20
+ "peerDependencies": {
21
+ "react": ">=18",
22
+ "react-dom": ">=18",
23
+ "next": ">=13"
24
+ },
25
+ "dependencies": {
26
+ "zod": "^3.22.4"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.11.19",
30
+ "@types/react": "^18.2.55",
31
+ "@types/react-dom": "^18.2.19",
32
+ "react": "^18.2.0",
33
+ "react-dom": "^18.2.0",
34
+ "typescript": "^5.3.3",
35
+ "tsup": "^8.0.1",
36
+ "vitest": "^1.2.2"
37
+ }
38
+ }