@denlabs/feedback-client 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,96 @@
1
+ # @denlabs/feedback-client
2
+
3
+ Typed client for the [DenLabs Event Feedback Ops](https://feedback-ops.vercel.app) API.
4
+
5
+ Zero runtime dependencies — uses native `fetch`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @denlabs/feedback-client
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { FeedbackClient } from '@denlabs/feedback-client'
17
+
18
+ const client = new FeedbackClient({
19
+ baseUrl: 'https://feedback-ops.vercel.app',
20
+ adminToken: process.env.EFO_ADMIN_TOKEN, // required for admin endpoints
21
+ })
22
+
23
+ // List labs
24
+ const { data: labs, meta } = await client.listLabs({ limit: 10 })
25
+
26
+ // Get a lab
27
+ const { data: lab } = await client.getLab('my-lab')
28
+
29
+ // Submit feedback (public, no admin token needed)
30
+ const publicClient = new FeedbackClient({
31
+ baseUrl: 'https://feedback-ops.vercel.app',
32
+ })
33
+ const { data: feedback } = await publicClient.submitFeedback('my-lab', {
34
+ message: 'The onboarding flow is confusing',
35
+ tags: ['ux', 'onboarding'],
36
+ route: '/labs/my-lab',
37
+ })
38
+
39
+ // Create a lab (admin)
40
+ const { data: newLab } = await client.createLab({
41
+ name: 'ETH Denver 2026',
42
+ startDate: '2026-02-23T00:00:00Z',
43
+ endDate: '2026-03-02T00:00:00Z',
44
+ surfacesToObserve: ['registration', 'schedule', 'networking'],
45
+ })
46
+
47
+ // Triage feedback (admin)
48
+ const { data: triaged } = await client.triageFeedback('my-lab', feedback.id, {
49
+ status: 'triaged',
50
+ priority: 'P1',
51
+ })
52
+ ```
53
+
54
+ ## Error Handling
55
+
56
+ ```typescript
57
+ import { FeedbackClient, FeedbackClientError } from '@denlabs/feedback-client'
58
+
59
+ try {
60
+ await client.getLab('nonexistent')
61
+ } catch (err) {
62
+ if (err instanceof FeedbackClientError) {
63
+ console.log(err.code) // 'NOT_FOUND'
64
+ console.log(err.status) // 404
65
+ console.log(err.message) // 'Lab not found'
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Error Codes
71
+
72
+ | Code | HTTP | Description |
73
+ |------|------|-------------|
74
+ | `VALIDATION_ERROR` | 400 | Invalid request parameters |
75
+ | `UNAUTHORIZED` | 401 | Missing or invalid admin token |
76
+ | `NOT_FOUND` | 404 | Resource not found |
77
+ | `LAB_NOT_ACTIVE` | 409 | Lab is not accepting feedback |
78
+ | `SLUG_EXHAUSTED` | 409 | Could not generate unique slug |
79
+ | `RATE_LIMITED` | 429 | Too many requests |
80
+ | `INTERNAL_ERROR` | 500 | Server error |
81
+
82
+ ## API Reference
83
+
84
+ | Method | Auth | Description |
85
+ |--------|------|-------------|
86
+ | `listLabs(opts?)` | Public | Paginated list of labs |
87
+ | `createLab(input)` | Admin | Create a new lab |
88
+ | `getLab(slug)` | Public | Get lab by slug |
89
+ | `updateLabStatus(slug, input)` | Admin | Update lab status |
90
+ | `listFeedback(slug, opts?)` | Public | Paginated feedback for a lab |
91
+ | `submitFeedback(slug, input)` | Public | Submit feedback |
92
+ | `triageFeedback(slug, id, input)` | Admin | Triage feedback |
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,29 @@
1
+ import type { FeedbackClientConfig, Lab, FeedbackItem, FeedbackItemDetail, PublicFeedbackItem, PaginationMeta, CreateLabInput, UpdateLabStatusInput, CreateFeedbackInput, TriageFeedbackInput, PaginationInput, ListFeedbackInput } from './types.js';
2
+ export declare class FeedbackClientError extends Error {
3
+ readonly code: string;
4
+ readonly status: number;
5
+ readonly details?: unknown | undefined;
6
+ constructor(code: string, message: string, status: number, details?: unknown | undefined);
7
+ }
8
+ type DataResult<T> = {
9
+ data: T;
10
+ };
11
+ type ListResult<T> = {
12
+ data: T[];
13
+ meta: PaginationMeta;
14
+ };
15
+ export declare class FeedbackClient {
16
+ private readonly baseUrl;
17
+ private readonly adminToken?;
18
+ private readonly _fetch;
19
+ constructor(config: FeedbackClientConfig);
20
+ listLabs(opts?: PaginationInput): Promise<ListResult<Lab>>;
21
+ createLab(input: CreateLabInput): Promise<DataResult<Lab>>;
22
+ getLab(slug: string): Promise<DataResult<Lab>>;
23
+ updateLabStatus(slug: string, input: UpdateLabStatusInput): Promise<DataResult<Lab>>;
24
+ listFeedback(slug: string, opts?: ListFeedbackInput): Promise<ListResult<PublicFeedbackItem>>;
25
+ submitFeedback(slug: string, input: CreateFeedbackInput): Promise<DataResult<FeedbackItem>>;
26
+ triageFeedback(slug: string, id: string, input: TriageFeedbackInput): Promise<DataResult<FeedbackItemDetail>>;
27
+ private request;
28
+ }
29
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,83 @@
1
+ export class FeedbackClientError extends Error {
2
+ code;
3
+ status;
4
+ details;
5
+ constructor(code, message, status, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.status = status;
9
+ this.details = details;
10
+ this.name = 'FeedbackClientError';
11
+ }
12
+ }
13
+ export class FeedbackClient {
14
+ baseUrl;
15
+ adminToken;
16
+ _fetch;
17
+ constructor(config) {
18
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
19
+ this.adminToken = config.adminToken;
20
+ this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
21
+ }
22
+ // --- Labs ---
23
+ async listLabs(opts) {
24
+ const params = new URLSearchParams();
25
+ if (opts?.limit !== undefined)
26
+ params.set('limit', String(opts.limit));
27
+ if (opts?.offset !== undefined)
28
+ params.set('offset', String(opts.offset));
29
+ const qs = params.toString();
30
+ return this.request('GET', `/api/labs${qs ? `?${qs}` : ''}`);
31
+ }
32
+ async createLab(input) {
33
+ return this.request('POST', '/api/labs', input, true);
34
+ }
35
+ async getLab(slug) {
36
+ return this.request('GET', `/api/labs/${encodeURIComponent(slug)}`);
37
+ }
38
+ async updateLabStatus(slug, input) {
39
+ return this.request('PATCH', `/api/labs/${encodeURIComponent(slug)}`, input, true);
40
+ }
41
+ // --- Feedback ---
42
+ async listFeedback(slug, opts) {
43
+ const params = new URLSearchParams();
44
+ if (opts?.limit !== undefined)
45
+ params.set('limit', String(opts.limit));
46
+ if (opts?.offset !== undefined)
47
+ params.set('offset', String(opts.offset));
48
+ if (opts?.status)
49
+ params.set('status', opts.status);
50
+ const qs = params.toString();
51
+ return this.request('GET', `/api/labs/${encodeURIComponent(slug)}/feedback${qs ? `?${qs}` : ''}`);
52
+ }
53
+ async submitFeedback(slug, input) {
54
+ return this.request('POST', `/api/labs/${encodeURIComponent(slug)}/feedback`, input);
55
+ }
56
+ async triageFeedback(slug, id, input) {
57
+ return this.request('PATCH', `/api/labs/${encodeURIComponent(slug)}/feedback/${encodeURIComponent(id)}`, input, true);
58
+ }
59
+ // --- Internal ---
60
+ async request(method, path, body, requireAdmin = false) {
61
+ const headers = {};
62
+ if (body !== undefined) {
63
+ headers['Content-Type'] = 'application/json';
64
+ }
65
+ if (requireAdmin) {
66
+ if (!this.adminToken) {
67
+ throw new FeedbackClientError('UNAUTHORIZED', 'Admin token required', 401);
68
+ }
69
+ headers['X-Admin-Token'] = this.adminToken;
70
+ }
71
+ const res = await this._fetch(`${this.baseUrl}${path}`, {
72
+ method,
73
+ headers,
74
+ body: body !== undefined ? JSON.stringify(body) : undefined,
75
+ });
76
+ const json = await res.json();
77
+ if (!res.ok) {
78
+ const err = json.error;
79
+ throw new FeedbackClientError(err?.code ?? 'UNKNOWN', err?.message ?? res.statusText, res.status, err?.details);
80
+ }
81
+ return json;
82
+ }
83
+ }
@@ -0,0 +1,2 @@
1
+ export { FeedbackClient, FeedbackClientError } from './client.js';
2
+ export type { FeedbackClientConfig, Lab, LabStatus, FeedbackItem, FeedbackItemDetail, PublicFeedbackItem, FeedbackStatus, FeedbackPriority, ErrorCode, PaginationMeta, ApiError, ApiDataResponse, ApiErrorResponse, ApiResponse, CreateLabInput, UpdateLabStatusInput, CreateFeedbackInput, TriageFeedbackInput, PaginationInput, ListFeedbackInput, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { FeedbackClient, FeedbackClientError } from './client.js';
@@ -0,0 +1,97 @@
1
+ export type LabStatus = 'active' | 'paused' | 'completed' | 'archived';
2
+ export type FeedbackStatus = 'new' | 'triaged' | 'done' | 'spam';
3
+ export type FeedbackPriority = 'P0' | 'P1' | 'P2' | 'P3';
4
+ export type ErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'UNAUTHORIZED' | 'RATE_LIMITED' | 'CONFLICT' | 'LAB_NOT_ACTIVE' | 'SLUG_EXHAUSTED' | 'INTERNAL_ERROR';
5
+ export type Lab = {
6
+ id: string;
7
+ slug: string;
8
+ name: string;
9
+ objective: string | null;
10
+ surfaces_to_observe: string[];
11
+ status: LabStatus;
12
+ start_date: string;
13
+ end_date: string | null;
14
+ metadata: Record<string, unknown>;
15
+ created_at: string;
16
+ };
17
+ export type FeedbackItem = {
18
+ id: string;
19
+ message: string;
20
+ status: FeedbackStatus;
21
+ tags: string[];
22
+ created_at: string;
23
+ };
24
+ export type FeedbackItemDetail = {
25
+ id: string;
26
+ lab_id: string;
27
+ message: string;
28
+ status: FeedbackStatus;
29
+ priority: FeedbackPriority | null;
30
+ tags: string[];
31
+ route: string | null;
32
+ step: string | null;
33
+ event_type: string | null;
34
+ metadata: Record<string, unknown>;
35
+ created_at: string;
36
+ };
37
+ export type PublicFeedbackItem = {
38
+ id: string;
39
+ lab_slug: string;
40
+ message: string;
41
+ status: FeedbackStatus;
42
+ tags: string[];
43
+ created_at: string;
44
+ };
45
+ export type PaginationMeta = {
46
+ total: number;
47
+ limit: number;
48
+ offset: number;
49
+ };
50
+ export type ApiError = {
51
+ code: ErrorCode;
52
+ message: string;
53
+ details?: unknown;
54
+ };
55
+ export type ApiDataResponse<T> = {
56
+ data: T;
57
+ meta?: PaginationMeta;
58
+ };
59
+ export type ApiErrorResponse = {
60
+ error: ApiError;
61
+ };
62
+ export type ApiResponse<T> = ApiDataResponse<T> | ApiErrorResponse;
63
+ export type CreateLabInput = {
64
+ name: string;
65
+ objective?: string | null;
66
+ surfacesToObserve?: string[];
67
+ startDate: string;
68
+ endDate?: string | null;
69
+ metadata?: Record<string, unknown>;
70
+ };
71
+ export type UpdateLabStatusInput = {
72
+ status: LabStatus;
73
+ };
74
+ export type CreateFeedbackInput = {
75
+ message: string;
76
+ tags?: string[];
77
+ route?: string;
78
+ step?: string;
79
+ eventType?: string;
80
+ metadata?: Record<string, unknown>;
81
+ };
82
+ export type TriageFeedbackInput = {
83
+ status?: FeedbackStatus;
84
+ priority?: FeedbackPriority | null;
85
+ };
86
+ export type PaginationInput = {
87
+ limit?: number;
88
+ offset?: number;
89
+ };
90
+ export type ListFeedbackInput = PaginationInput & {
91
+ status?: FeedbackStatus;
92
+ };
93
+ export type FeedbackClientConfig = {
94
+ baseUrl: string;
95
+ adminToken?: string;
96
+ fetch?: typeof globalThis.fetch;
97
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // --- Enums ---
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@denlabs/feedback-client",
3
+ "version": "0.1.0",
4
+ "description": "Typed client for the DenLabs Event Feedback Ops API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "prepublishOnly": "tsc"
23
+ },
24
+ "keywords": ["denlabs", "feedback", "api-client"],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.8.0",
31
+ "vitest": "^4.1.0"
32
+ }
33
+ }