@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 +96 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +2 -0
- package/package.json +33 -0
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
|
package/dist/client.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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';
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|