@startsimpli/api 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.
Files changed (38) hide show
  1. package/README.md +329 -0
  2. package/package.json +42 -0
  3. package/src/__tests__/jwt-refresh.test.ts +195 -0
  4. package/src/__tests__/query-params.test.ts +144 -0
  5. package/src/__tests__/url-builder.test.ts +121 -0
  6. package/src/constants/endpoints.ts +39 -0
  7. package/src/index.ts +109 -0
  8. package/src/lib/api-client.ts +89 -0
  9. package/src/lib/contacts-api.ts +111 -0
  10. package/src/lib/cors.ts +122 -0
  11. package/src/lib/entities-api.ts +123 -0
  12. package/src/lib/env.ts +35 -0
  13. package/src/lib/error-handler.ts +138 -0
  14. package/src/lib/errors.ts +381 -0
  15. package/src/lib/fetch-wrapper.ts +188 -0
  16. package/src/lib/llm-sanitize.ts +145 -0
  17. package/src/lib/messages-api.ts +273 -0
  18. package/src/lib/messages-api.ts.backup +273 -0
  19. package/src/lib/organizations-api.ts +132 -0
  20. package/src/lib/rate-limit.ts +91 -0
  21. package/src/lib/sanitize.ts +39 -0
  22. package/src/lib/workflows-api.ts +159 -0
  23. package/src/middleware/index.ts +12 -0
  24. package/src/middleware/with-auth.ts +90 -0
  25. package/src/middleware/with-error-handling.ts +83 -0
  26. package/src/middleware/with-validation.ts +110 -0
  27. package/src/types/api.ts +38 -0
  28. package/src/types/contact.ts +49 -0
  29. package/src/types/entity.ts +153 -0
  30. package/src/types/error.ts +129 -0
  31. package/src/types/funnel.ts +133 -0
  32. package/src/types/index.ts +95 -0
  33. package/src/types/organization.ts +49 -0
  34. package/src/types/response.ts +44 -0
  35. package/src/types/workflow.ts +69 -0
  36. package/src/utils/index.ts +13 -0
  37. package/src/utils/query-params.ts +79 -0
  38. package/src/utils/url-builder.ts +78 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Auth middleware for Next.js API routes
3
+ */
4
+
5
+ import type { NextRequest } from 'next/server';
6
+ import { NextResponse } from 'next/server';
7
+
8
+ export interface AuthContext {
9
+ userId?: string;
10
+ token?: string;
11
+ isAuthenticated: boolean;
12
+ }
13
+
14
+ export type ApiHandler<T = unknown> = (
15
+ request: NextRequest,
16
+ context: AuthContext
17
+ ) => Promise<NextResponse<T>> | NextResponse<T>;
18
+
19
+ export interface WithAuthOptions {
20
+ required?: boolean;
21
+ getToken?: (request: NextRequest) => Promise<string | null> | string | null;
22
+ }
23
+
24
+ /**
25
+ * Auth middleware for Next.js API routes
26
+ */
27
+ export function withAuth<T = unknown>(
28
+ handler: ApiHandler<T>,
29
+ options: WithAuthOptions = {}
30
+ ): (request: NextRequest) => Promise<NextResponse<T>> {
31
+ return async (request: NextRequest) => {
32
+ const { required = true, getToken } = options;
33
+
34
+ try {
35
+ // Get token from request
36
+ let token: string | null = null;
37
+
38
+ if (getToken) {
39
+ token = await getToken(request);
40
+ } else {
41
+ // Default: extract from Authorization header
42
+ const authHeader = request.headers.get('authorization');
43
+ if (authHeader?.startsWith('Bearer ')) {
44
+ token = authHeader.substring(7);
45
+ }
46
+ }
47
+
48
+ // Build auth context
49
+ const context: AuthContext = {
50
+ token: token || undefined,
51
+ isAuthenticated: !!token,
52
+ };
53
+
54
+ // Check if auth is required
55
+ if (required && !context.isAuthenticated) {
56
+ return NextResponse.json(
57
+ { error: 'Unauthorized', message: 'Authentication required' },
58
+ { status: 401 }
59
+ ) as NextResponse<T>;
60
+ }
61
+
62
+ // Call handler with context
63
+ return await handler(request, context);
64
+ } catch (error) {
65
+ console.error('Auth middleware error:', error);
66
+ return NextResponse.json(
67
+ { error: 'Internal Server Error', message: 'Authentication failed' },
68
+ { status: 500 }
69
+ ) as NextResponse<T>;
70
+ }
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Extract user ID from JWT token (without verification)
76
+ * NOTE: This is for convenience only - always verify tokens on the backend
77
+ */
78
+ export function extractUserIdFromToken(token: string): string | null {
79
+ try {
80
+ const parts = token.split('.');
81
+ if (parts.length !== 3) {
82
+ return null;
83
+ }
84
+
85
+ const payload = JSON.parse(atob(parts[1]));
86
+ return payload.sub || payload.user_id || payload.userId || null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Error handling middleware for Next.js API routes
3
+ */
4
+
5
+ import type { NextRequest } from 'next/server';
6
+ import { NextResponse } from 'next/server';
7
+ import { ApiException } from '../lib/error-handler';
8
+
9
+ export type ErrorApiHandler<T = unknown> = (
10
+ request: NextRequest
11
+ ) => Promise<NextResponse<T>> | NextResponse<T>;
12
+
13
+ export interface ErrorResponseBody {
14
+ error: string;
15
+ message: string;
16
+ errors?: Record<string, string[]>;
17
+ status?: number;
18
+ }
19
+
20
+ /**
21
+ * Error handling middleware for Next.js API routes
22
+ */
23
+ export function withErrorHandling<T = unknown>(
24
+ handler: ErrorApiHandler<T>
25
+ ): (request: NextRequest) => Promise<NextResponse<T | ErrorResponseBody>> {
26
+ return async (request: NextRequest) => {
27
+ try {
28
+ return await handler(request);
29
+ } catch (error) {
30
+ console.error('API error:', error);
31
+
32
+ // Handle ApiException
33
+ if (error instanceof ApiException) {
34
+ return NextResponse.json(
35
+ {
36
+ error: error.name,
37
+ message: error.message,
38
+ errors: error.errors,
39
+ status: error.status,
40
+ },
41
+ { status: error.status || 500 }
42
+ ) as NextResponse<ErrorResponseBody>;
43
+ }
44
+
45
+ // Handle validation errors (Zod, etc.)
46
+ if (error && typeof error === 'object' && 'issues' in error) {
47
+ const issues = (error as { issues: Array<{ path: string[]; message: string }> })
48
+ .issues;
49
+
50
+ const errors: Record<string, string[]> = {};
51
+ issues.forEach((issue) => {
52
+ const field = issue.path.join('.');
53
+ if (!errors[field]) {
54
+ errors[field] = [];
55
+ }
56
+ errors[field].push(issue.message);
57
+ });
58
+
59
+ return NextResponse.json(
60
+ {
61
+ error: 'ValidationError',
62
+ message: 'Validation failed',
63
+ errors,
64
+ status: 400,
65
+ },
66
+ { status: 400 }
67
+ ) as NextResponse<ErrorResponseBody>;
68
+ }
69
+
70
+ // Handle generic errors
71
+ const message = error instanceof Error ? error.message : 'Unknown error';
72
+
73
+ return NextResponse.json(
74
+ {
75
+ error: 'InternalServerError',
76
+ message,
77
+ status: 500,
78
+ },
79
+ { status: 500 }
80
+ ) as NextResponse<ErrorResponseBody>;
81
+ }
82
+ };
83
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Validation middleware for Next.js API routes using Zod
3
+ */
4
+
5
+ import type { NextRequest } from 'next/server';
6
+ import { NextResponse } from 'next/server';
7
+ import { z, type ZodSchema } from 'zod';
8
+
9
+ export type ValidatedApiHandler<TBody = unknown, TQuery = unknown> = (
10
+ request: NextRequest,
11
+ validated: { body?: TBody; query?: TQuery }
12
+ ) => Promise<NextResponse> | NextResponse;
13
+
14
+ export interface ValidationSchemas {
15
+ body?: ZodSchema;
16
+ query?: ZodSchema;
17
+ }
18
+
19
+ /**
20
+ * Validation middleware for Next.js API routes
21
+ */
22
+ export function withValidation<TBody = unknown, TQuery = unknown>(
23
+ handler: ValidatedApiHandler<TBody, TQuery>,
24
+ schemas: ValidationSchemas
25
+ ): (request: NextRequest) => Promise<NextResponse> {
26
+ return async (request: NextRequest) => {
27
+ try {
28
+ const validated: { body?: TBody; query?: TQuery } = {};
29
+
30
+ // Validate body
31
+ if (schemas.body) {
32
+ const contentType = request.headers.get('content-type');
33
+ if (!contentType?.includes('application/json')) {
34
+ return NextResponse.json(
35
+ { error: 'ValidationError', message: 'Content-Type must be application/json' },
36
+ { status: 400 }
37
+ );
38
+ }
39
+
40
+ const body = await request.json();
41
+ validated.body = schemas.body.parse(body) as TBody;
42
+ }
43
+
44
+ // Validate query params
45
+ if (schemas.query) {
46
+ const { searchParams } = new URL(request.url);
47
+ const query: Record<string, unknown> = {};
48
+
49
+ searchParams.forEach((value, key) => {
50
+ // Handle array params (key repeated multiple times)
51
+ if (key in query) {
52
+ if (Array.isArray(query[key])) {
53
+ (query[key] as unknown[]).push(value);
54
+ } else {
55
+ query[key] = [query[key], value];
56
+ }
57
+ } else {
58
+ query[key] = value;
59
+ }
60
+ });
61
+
62
+ validated.query = schemas.query.parse(query) as TQuery;
63
+ }
64
+
65
+ // Call handler with validated data
66
+ return await handler(request, validated);
67
+ } catch (error) {
68
+ if (error instanceof z.ZodError) {
69
+ const errors: Record<string, string[]> = {};
70
+ error.issues.forEach((issue) => {
71
+ const field = issue.path.join('.');
72
+ if (!errors[field]) {
73
+ errors[field] = [];
74
+ }
75
+ errors[field].push(issue.message);
76
+ });
77
+
78
+ return NextResponse.json(
79
+ {
80
+ error: 'ValidationError',
81
+ message: 'Validation failed',
82
+ errors,
83
+ },
84
+ { status: 400 }
85
+ );
86
+ }
87
+
88
+ throw error;
89
+ }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Combine validation with other middleware
95
+ */
96
+ export function composeMiddleware<TBody = unknown, TQuery = unknown>(
97
+ handler: ValidatedApiHandler<TBody, TQuery>,
98
+ ...middlewares: Array<
99
+ (handler: ValidatedApiHandler<TBody, TQuery>) => ValidatedApiHandler<TBody, TQuery>
100
+ >
101
+ ): (request: NextRequest) => Promise<NextResponse> {
102
+ const composed = middlewares.reduce(
103
+ (acc, middleware) => middleware(acc),
104
+ handler
105
+ );
106
+
107
+ return async (request: NextRequest) => {
108
+ return composed(request, {});
109
+ };
110
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Common API types for Django REST Framework
3
+ */
4
+
5
+ export interface PaginatedResponse<T> {
6
+ count: number;
7
+ next: string | null;
8
+ previous: string | null;
9
+ results: T[];
10
+ }
11
+
12
+ export interface ApiError {
13
+ detail?: string;
14
+ message?: string;
15
+ errors?: Record<string, string[]>;
16
+ status?: number;
17
+ statusText?: string;
18
+ }
19
+
20
+ export interface PaginationParams {
21
+ page?: number;
22
+ pageSize?: number;
23
+ }
24
+
25
+ export interface SortParams {
26
+ ordering?: string; // e.g., '-createdAt', 'name'
27
+ }
28
+
29
+ export interface ApiRequestConfig {
30
+ headers?: HeadersInit;
31
+ signal?: AbortSignal;
32
+ }
33
+
34
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
35
+
36
+ export interface FetchOptions extends RequestInit {
37
+ params?: Record<string, unknown>;
38
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Contact types matching Django Contact model
3
+ */
4
+
5
+ import type { Entity, WritableAssertions } from './entity';
6
+
7
+ export interface Contact extends Entity {
8
+ name: string;
9
+ email?: string | null;
10
+ phone?: string | null;
11
+ title?: string | null;
12
+ companyName?: string | null;
13
+ location?: string | null;
14
+ notes?: string | null;
15
+
16
+ // Computed fields from assertions
17
+ tier?: number | null;
18
+ status?: string | null;
19
+ linkedin?: string | null;
20
+ twitter?: string | null;
21
+ website?: string | null;
22
+ firmId?: string | null;
23
+ firmName?: string | null;
24
+ enrichmentScore?: number | null;
25
+ }
26
+
27
+ export interface CreateContactRequest extends WritableAssertions {
28
+ name: string;
29
+ email?: string;
30
+ phone?: string;
31
+ title?: string;
32
+ companyName?: string;
33
+ location?: string;
34
+ notes?: string;
35
+ }
36
+
37
+ export interface UpdateContactRequest extends Partial<CreateContactRequest> {}
38
+
39
+ export interface ContactFilters {
40
+ [key: string]: unknown;
41
+ search?: string;
42
+ tier?: number;
43
+ status?: string;
44
+ firmId?: string;
45
+ tagCategory?: string;
46
+ tagName?: string;
47
+ hasEmail?: boolean;
48
+ hasLinkedin?: boolean;
49
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Entity system types matching Django core models
3
+ */
4
+
5
+ export type EntityType = 'contact' | 'organization';
6
+
7
+ export interface Entity {
8
+ id: string; // external_id (UUID)
9
+ entityType: EntityType;
10
+ createdAt: string;
11
+ updatedAt: string;
12
+ tags?: EntityTag[];
13
+ metrics?: Metric[];
14
+ profiles?: Profile[];
15
+ attributes?: Attribute[];
16
+ relationshipsFrom?: Relationship[];
17
+ relationshipsTo?: Relationship[];
18
+ roleAssignments?: RoleAssignment[];
19
+ events?: Event[];
20
+ }
21
+
22
+ export interface Tag {
23
+ id: number;
24
+ category: string;
25
+ name: string;
26
+ description?: string;
27
+ fullPath: string;
28
+ }
29
+
30
+ export interface EntityTag {
31
+ id: number;
32
+ category: string;
33
+ name: string;
34
+ tagDescription?: string;
35
+ confidence: number;
36
+ appliedAt: string;
37
+ appliedBy?: string;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+
41
+ export interface Metric {
42
+ id: number;
43
+ type: string;
44
+ subtype: string;
45
+ value: number | string | null;
46
+ valueNumeric?: number | null;
47
+ valueText?: string | null;
48
+ unit?: string;
49
+ asOfDate?: string;
50
+ periodStart?: string;
51
+ confidence: number;
52
+ metadata?: Record<string, unknown>;
53
+ }
54
+
55
+ export interface Profile {
56
+ id: number;
57
+ type: string;
58
+ subtype: string;
59
+ identifier: string;
60
+ identifierType: string;
61
+ displayName: string;
62
+ verified: boolean;
63
+ verifiedAt?: string;
64
+ metadata?: Record<string, unknown>;
65
+ }
66
+
67
+ export interface Attribute {
68
+ id: number;
69
+ type: string;
70
+ subtype: string;
71
+ value: string | Record<string, unknown> | null;
72
+ valueText?: string | null;
73
+ valueJson?: Record<string, unknown> | null;
74
+ valueType: 'text' | 'json';
75
+ validFrom?: string;
76
+ validTo?: string;
77
+ isCurrent: boolean;
78
+ confidence: number;
79
+ metadata?: Record<string, unknown>;
80
+ }
81
+
82
+ export interface Relationship {
83
+ id: number;
84
+ fromEntityId: string;
85
+ toEntityId: string;
86
+ type: string;
87
+ subtype?: string;
88
+ validFrom?: string;
89
+ validTo?: string;
90
+ isCurrent: boolean;
91
+ durationDays?: number;
92
+ strength?: number;
93
+ confidence: number;
94
+ metadata?: Record<string, unknown>;
95
+ }
96
+
97
+ export interface RoleAssignment {
98
+ id: number;
99
+ contactId: string;
100
+ orgId: string;
101
+ type: string;
102
+ subtype?: string;
103
+ title?: string;
104
+ seniority?: string;
105
+ validFrom?: string;
106
+ validTo?: string;
107
+ isCurrent: boolean;
108
+ durationDays?: number;
109
+ confidence: number;
110
+ metadata?: Record<string, unknown>;
111
+ }
112
+
113
+ export interface Event {
114
+ id: number;
115
+ type: string;
116
+ subtype?: string;
117
+ occurredAt?: string;
118
+ announcedAt?: string;
119
+ magnitude?: number;
120
+ confidence: number;
121
+ metadata?: Record<string, unknown>;
122
+ participants?: EventParticipant[];
123
+ }
124
+
125
+ export interface EventParticipant {
126
+ id: number;
127
+ entityId: string;
128
+ roleType: string;
129
+ roleSubtype?: string;
130
+ metadata?: Record<string, unknown>;
131
+ }
132
+
133
+ export interface Source {
134
+ id: number;
135
+ type: string;
136
+ subtype?: string;
137
+ identifier: string;
138
+ retrievedAt: string;
139
+ format: string;
140
+ confidence: number;
141
+ cost?: number;
142
+ metadata?: Record<string, unknown>;
143
+ }
144
+
145
+ /**
146
+ * Writable assertion formats for POST/PATCH
147
+ */
148
+ export interface WritableAssertions {
149
+ writeTags?: Array<string | { category: string; name: string; confidence?: number }>;
150
+ writeMetrics?: Record<string, number | string>;
151
+ writeProfiles?: Record<string, string>;
152
+ writeAttributes?: Record<string, string | Record<string, unknown>>;
153
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Standardized API error response types
3
+ *
4
+ * Provides consistent error handling across frontend and backend
5
+ */
6
+
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * Field-level validation error
11
+ */
12
+ export interface FieldError {
13
+ field: string;
14
+ messages: string[];
15
+ code?: string;
16
+ }
17
+
18
+ /**
19
+ * Standardized API error response
20
+ */
21
+ export interface StandardErrorResponse {
22
+ /** Main error message (user-friendly) */
23
+ error: string;
24
+
25
+ /** Error type/code for programmatic handling */
26
+ code: string;
27
+
28
+ /** HTTP status code */
29
+ statusCode: number;
30
+
31
+ /** Field-level validation errors */
32
+ fieldErrors?: FieldError[];
33
+
34
+ /** Additional error details */
35
+ details?: Record<string, unknown>;
36
+
37
+ /** Request ID for debugging */
38
+ requestId?: string;
39
+
40
+ /** Timestamp of error */
41
+ timestamp?: string;
42
+ }
43
+
44
+ /**
45
+ * Zod schema for runtime validation
46
+ */
47
+ export const FieldErrorSchema = z.object({
48
+ field: z.string(),
49
+ messages: z.array(z.string()),
50
+ code: z.string().optional(),
51
+ });
52
+
53
+ export const StandardErrorResponseSchema = z.object({
54
+ error: z.string(),
55
+ code: z.string(),
56
+ statusCode: z.number().int().min(400).max(599),
57
+ fieldErrors: z.array(FieldErrorSchema).optional(),
58
+ details: z.record(z.unknown()).optional(),
59
+ requestId: z.string().optional(),
60
+ timestamp: z.string().datetime().optional(),
61
+ });
62
+
63
+ /**
64
+ * Common error codes
65
+ */
66
+ export enum ErrorCode {
67
+ // Client errors (4xx)
68
+ BAD_REQUEST = 'bad_request',
69
+ UNAUTHORIZED = 'unauthorized',
70
+ FORBIDDEN = 'forbidden',
71
+ NOT_FOUND = 'not_found',
72
+ METHOD_NOT_ALLOWED = 'method_not_allowed',
73
+ VALIDATION_ERROR = 'validation_error',
74
+ CONFLICT = 'conflict',
75
+ RATE_LIMITED = 'rate_limited',
76
+
77
+ // Server errors (5xx)
78
+ INTERNAL_ERROR = 'internal_error',
79
+ SERVICE_UNAVAILABLE = 'service_unavailable',
80
+ GATEWAY_TIMEOUT = 'gateway_timeout',
81
+
82
+ // Network errors
83
+ NETWORK_ERROR = 'network_error',
84
+ TIMEOUT = 'timeout',
85
+
86
+ // Unknown
87
+ UNKNOWN = 'unknown',
88
+ }
89
+
90
+ /**
91
+ * Map HTTP status codes to error codes
92
+ */
93
+ export function getErrorCodeFromStatus(status: number): ErrorCode {
94
+ const codeMap: Record<number, ErrorCode> = {
95
+ 400: ErrorCode.BAD_REQUEST,
96
+ 401: ErrorCode.UNAUTHORIZED,
97
+ 403: ErrorCode.FORBIDDEN,
98
+ 404: ErrorCode.NOT_FOUND,
99
+ 405: ErrorCode.METHOD_NOT_ALLOWED,
100
+ 409: ErrorCode.CONFLICT,
101
+ 422: ErrorCode.VALIDATION_ERROR,
102
+ 429: ErrorCode.RATE_LIMITED,
103
+ 500: ErrorCode.INTERNAL_ERROR,
104
+ 503: ErrorCode.SERVICE_UNAVAILABLE,
105
+ 504: ErrorCode.GATEWAY_TIMEOUT,
106
+ };
107
+
108
+ return codeMap[status] || ErrorCode.UNKNOWN;
109
+ }
110
+
111
+ /**
112
+ * User-friendly error messages
113
+ */
114
+ export const ErrorMessages: Record<ErrorCode, string> = {
115
+ [ErrorCode.BAD_REQUEST]: 'The request was invalid. Please check your input.',
116
+ [ErrorCode.UNAUTHORIZED]: 'You need to log in to access this resource.',
117
+ [ErrorCode.FORBIDDEN]: "You don't have permission to access this resource.",
118
+ [ErrorCode.NOT_FOUND]: 'The requested resource was not found.',
119
+ [ErrorCode.METHOD_NOT_ALLOWED]: 'This operation is not allowed.',
120
+ [ErrorCode.VALIDATION_ERROR]: 'Please fix the errors in your form.',
121
+ [ErrorCode.CONFLICT]: 'This operation conflicts with existing data.',
122
+ [ErrorCode.RATE_LIMITED]: 'Too many requests. Please slow down.',
123
+ [ErrorCode.INTERNAL_ERROR]: 'An unexpected error occurred. Please try again.',
124
+ [ErrorCode.SERVICE_UNAVAILABLE]: 'The service is temporarily unavailable.',
125
+ [ErrorCode.GATEWAY_TIMEOUT]: 'The request took too long. Please try again.',
126
+ [ErrorCode.NETWORK_ERROR]: 'Network error. Please check your connection.',
127
+ [ErrorCode.TIMEOUT]: 'The request timed out. Please try again.',
128
+ [ErrorCode.UNKNOWN]: 'An unknown error occurred.',
129
+ };