@gobing-ai/ts-utils 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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/access.ts", "../src/api-response.ts", "../src/const.ts", "../src/date.ts", "../src/cursor.ts", "../src/errors.ts", "../src/origin.ts", "../src/output.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Zitadel + generic role-based access control helpers.\n *\n * Supports Zitadel IAM role claims (`urn:zitadel:iam:org:project:roles`),\n * generic `roles` arrays, and object-based role maps.\n *\n * If auth provider uses a different claim format, extend `hasRole`\n * rather than forking this module.\n */\nexport function hasRole(profile: Record<string, unknown> | null | undefined, role: string): boolean {\n if (!profile) return false;\n if (!role || typeof role !== 'string') return false;\n\n const zitadelRoles = profile['urn:zitadel:iam:org:project:roles'];\n if (zitadelRoles && typeof zitadelRoles === 'object') {\n try {\n if (zitadelRoles !== null && !Array.isArray(zitadelRoles) && Object.hasOwn(zitadelRoles, role)) {\n return true;\n }\n } catch {\n // Continue to other role formats when host objects reject inspection.\n }\n }\n\n const rolesArray = profile.roles;\n if (Array.isArray(rolesArray)) {\n return rolesArray.includes(role);\n }\n\n if (rolesArray && typeof rolesArray === 'object' && !Array.isArray(rolesArray)) {\n try {\n if (rolesArray !== null) {\n return Object.hasOwn(rolesArray, role);\n }\n } catch {\n // Fall through.\n }\n }\n\n return false;\n}\n\nexport function getRoles(profile: Record<string, unknown> | null | undefined): string[] {\n if (!profile) return [];\n\n const roles = new Set<string>();\n\n const zitadelRoles = profile['urn:zitadel:iam:org:project:roles'];\n if (zitadelRoles && typeof zitadelRoles === 'object' && zitadelRoles !== null && !Array.isArray(zitadelRoles)) {\n try {\n for (const key of Object.keys(zitadelRoles)) {\n roles.add(key);\n }\n } catch {\n // Ignore host objects that reject key enumeration.\n }\n }\n\n const rolesArray = profile.roles;\n if (Array.isArray(rolesArray)) {\n rolesArray.forEach((role) => {\n if (typeof role === 'string') roles.add(role);\n });\n }\n\n if (rolesArray && typeof rolesArray === 'object' && rolesArray !== null && !Array.isArray(rolesArray)) {\n try {\n for (const key of Object.keys(rolesArray)) {\n roles.add(key);\n }\n } catch {\n // Ignore host objects that reject key enumeration.\n }\n }\n\n return Array.from(roles);\n}\n",
6
+ "export const API_ERROR_CODES = {\n SUCCESS: 0,\n NOT_FOUND: 404,\n VALIDATION_ERROR: 422,\n BAD_REQUEST: 400,\n UNAUTHORIZED: 401,\n FORBIDDEN: 403,\n CONFLICT: 409,\n INTERNAL_ERROR: 500,\n} as const;\n\nexport type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];\n\nexport type ApiEnvelopeResult = 'success' | 'info' | 'warn' | 'error';\n\nexport interface ApiSuccessEnvelope<T> {\n code: 0;\n message: string;\n result: 'success' | 'info';\n data: T;\n meta?: { total?: number; limit?: number; offset?: number };\n}\n\nexport interface ApiErrorEnvelope {\n result: 'warn' | 'error';\n code: number;\n message: string;\n data: null;\n details?: unknown;\n}\n\nexport type ApiEnvelope<T> = ApiSuccessEnvelope<T> | ApiErrorEnvelope;\n\nexport function successResponse<T>(data: T, message = 'Success'): ApiSuccessEnvelope<T> {\n return {\n code: API_ERROR_CODES.SUCCESS,\n message,\n result: 'success',\n data,\n };\n}\n\nexport function infoResponse<T>(data: T, message = 'Data retrieved successfully'): ApiSuccessEnvelope<T> {\n return {\n code: API_ERROR_CODES.SUCCESS,\n message,\n result: 'info',\n data,\n };\n}\n\nexport function paginatedResponse<T>(\n data: T[],\n meta: { total?: number; limit?: number; offset?: number },\n message = 'Data retrieved successfully',\n): ApiSuccessEnvelope<T[]> {\n return {\n code: API_ERROR_CODES.SUCCESS,\n message,\n result: 'info',\n data,\n meta,\n };\n}\n\nexport function errorResponse(code: number, message: string, details?: unknown): ApiErrorEnvelope {\n const response: ApiErrorEnvelope = {\n code,\n message,\n result: code >= 500 ? 'error' : 'warn',\n data: null,\n };\n\n if (details !== undefined) {\n response.details = details;\n }\n\n return response;\n}\n\nexport function notFoundResponse(message = 'Resource not found', details?: unknown): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.NOT_FOUND, message, details);\n}\n\nexport function validationErrorResponse(details: unknown, message = 'Validation failed'): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.VALIDATION_ERROR, message, details);\n}\n\nexport function badRequestResponse(message: string, details?: unknown): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.BAD_REQUEST, message, details);\n}\n\nexport function unauthorizedResponse(message = 'Authentication required', details?: unknown): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.UNAUTHORIZED, message, details);\n}\n\nexport function forbiddenResponse(message = 'Access forbidden', details?: unknown): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.FORBIDDEN, message, details);\n}\n\nexport function conflictResponse(message = 'Resource conflict', details?: unknown): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.CONFLICT, message, details);\n}\n\nexport function internalErrorResponse(message = 'Internal server error', details?: unknown): ApiErrorEnvelope {\n return errorResponse(API_ERROR_CODES.INTERNAL_ERROR, message, details);\n}\n",
7
+ "export const LOG_CATEGORY_APP = 'app';\n\nexport const LOG_CATEGORY_CLI = 'cli';\n",
8
+ "export function nowMs(): number {\n return Date.now();\n}\n\nexport function toMs(input: Date | number | string | null | undefined): number | null {\n if (input === null || input === undefined) return null;\n if (input instanceof Date) return input.getTime();\n if (typeof input === 'string') {\n const parsed = new Date(input).getTime();\n return Number.isNaN(parsed) ? null : parsed;\n }\n return Math.floor(input);\n}\n\nexport function fromMs(ms: number | null | undefined): Date | null {\n if (ms === null || ms === undefined || Number.isNaN(ms)) return null;\n return new Date(ms);\n}\n",
9
+ "import { toMs } from './date';\n\nexport interface CursorData {\n id: string;\n createdAt?: number;\n offset?: number;\n}\n\nexport function createCursor(id: string, createdAt?: Date | number, offset?: number): CursorData {\n const cursor: CursorData = { id };\n if (createdAt !== undefined) {\n const ms = toMs(createdAt);\n if (ms !== null) {\n cursor.createdAt = ms;\n }\n }\n if (offset !== undefined) {\n cursor.offset = offset;\n }\n return cursor;\n}\n\nexport function parseCursor(data: string | Record<string, unknown>): CursorData {\n const parsed = typeof data === 'string' ? (JSON.parse(data) as Record<string, unknown>) : data;\n\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('Invalid cursor: must be an object');\n }\n\n if (!parsed.id || typeof parsed.id !== 'string') {\n throw new Error('Invalid cursor: missing or invalid id');\n }\n\n const result: CursorData = { id: parsed.id };\n if (typeof parsed.createdAt === 'number') {\n result.createdAt = parsed.createdAt;\n }\n if (typeof parsed.offset === 'number') {\n result.offset = parsed.offset;\n }\n return result;\n}\n\nexport function encodeCursor(cursor: CursorData): string {\n return Buffer.from(JSON.stringify(cursor)).toString('base64url');\n}\n\nexport function decodeCursor(encoded: string): string {\n try {\n return Buffer.from(encoded, 'base64url').toString('utf-8');\n } catch (error) {\n throw new Error(`Invalid cursor encoding: ${String(error)}`);\n }\n}\n\nexport function encodeCursorFromItem(id: string, createdAt?: Date | number, offset?: number): string {\n return encodeCursor(createCursor(id, createdAt, offset));\n}\n\nexport function decodeAndParseCursor(encoded: string): CursorData {\n return parseCursor(decodeCursor(encoded));\n}\n\nexport function buildCursorMeta<T extends { id: string; createdAt?: number | Date }>(\n items: T[],\n limit: number,\n hasMore: boolean,\n): { nextCursor?: string; hasMore: boolean; limit: number } {\n const meta: { nextCursor?: string; hasMore: boolean; limit: number } = {\n hasMore,\n limit,\n };\n\n if (hasMore) {\n const lastItem = items.at(-1);\n if (lastItem) {\n meta.nextCursor = encodeCursorFromItem(lastItem.id, lastItem.createdAt);\n }\n }\n\n return meta;\n}\n",
10
+ "export const ErrorCode = {\n NotFound: 'NOT_FOUND',\n Validation: 'VALIDATION',\n Conflict: 'CONFLICT',\n Internal: 'INTERNAL',\n} as const;\n\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\nexport class AppError extends Error {\n readonly code: ErrorCode;\n\n constructor(code: ErrorCode, message: string) {\n super(message);\n this.name = 'AppError';\n this.code = code;\n }\n}\n\nexport class NotFoundError extends AppError {\n constructor(message: string) {\n super(ErrorCode.NotFound, message);\n this.name = 'NotFoundError';\n }\n}\n\nexport class ValidationError extends AppError {\n constructor(message: string) {\n super(ErrorCode.Validation, message);\n this.name = 'ValidationError';\n }\n}\n\nexport class ConflictError extends AppError {\n constructor(message: string) {\n super(ErrorCode.Conflict, message);\n this.name = 'ConflictError';\n }\n}\n\nexport class InternalError extends AppError {\n constructor(\n message: string,\n override readonly cause?: unknown,\n ) {\n super(ErrorCode.Internal, message);\n this.name = 'InternalError';\n }\n}\n\nexport function isAppError(error: unknown): error is AppError {\n return error instanceof AppError;\n}\n",
11
+ "export function matchOriginPattern(origin: string, pattern: string): boolean {\n if (pattern === origin) return true;\n if (pattern === '*') return true;\n\n if (pattern.includes('*')) {\n const parts = pattern.split('*');\n if (parts.length !== 2) {\n return pattern === origin;\n }\n const [prefix, suffix] = parts;\n if (prefix === undefined || suffix === undefined) return false;\n return origin.startsWith(prefix) && origin.endsWith(suffix) && origin.length >= prefix.length + suffix.length;\n }\n\n return false;\n}\n\nexport function isAllowedOrigin(origin: string | undefined | null, allowedOrigins: string[]): boolean {\n if (!origin) return false;\n if (!allowedOrigins || allowedOrigins.length === 0) return false;\n\n return allowedOrigins.some((pattern) => matchOriginPattern(origin, pattern));\n}\n\nexport function getValidatedOrigin(\n origin: string | undefined | null,\n allowedOrigins: string[],\n fallback: string,\n): string {\n if (origin && isAllowedOrigin(origin, allowedOrigins)) {\n return origin;\n }\n return fallback;\n}\n",
12
+ "export interface WriteTarget {\n write(chunk: string): unknown;\n}\n\nlet defaultStdoutTarget: WriteTarget = process.stdout;\nlet defaultStderrTarget: WriteTarget = process.stderr;\n\nfunction writeLine(message: string, target: WriteTarget): void {\n target.write(`${message}\\n`);\n}\n\nexport function echo(message: string, target: WriteTarget = defaultStdoutTarget): void {\n writeLine(message, target);\n}\n\nexport function echoError(message: string, target: WriteTarget = defaultStderrTarget): void {\n writeLine(message, target);\n}\n\nexport function setDefaultOutputTargets(opts: { stdout?: WriteTarget; stderr?: WriteTarget }): () => void {\n const prevStdout = defaultStdoutTarget;\n const prevStderr = defaultStderrTarget;\n if (opts.stdout) defaultStdoutTarget = opts.stdout;\n if (opts.stderr) defaultStderrTarget = opts.stderr;\n return () => {\n defaultStdoutTarget = prevStdout;\n defaultStderrTarget = prevStderr;\n };\n}\n\nexport interface BufferTarget extends WriteTarget {\n readonly chunks: string[];\n text(): string;\n clear(): void;\n}\n\nexport function createBufferTarget(): BufferTarget {\n const chunks: string[] = [];\n return {\n chunks,\n write(chunk: string) {\n chunks.push(String(chunk));\n return true;\n },\n text() {\n return chunks.join('');\n },\n clear() {\n chunks.length = 0;\n },\n };\n}\n"
13
+ ],
14
+ "mappings": ";;AASO,SAAS,OAAO,CAAC,SAAqD,MAAuB;AAAA,EAChG,IAAI,CAAC;AAAA,IAAS,OAAO;AAAA,EACrB,IAAI,CAAC,QAAQ,OAAO,SAAS;AAAA,IAAU,OAAO;AAAA,EAE9C,MAAM,eAAe,QAAQ;AAAA,EAC7B,IAAI,gBAAgB,OAAO,iBAAiB,UAAU;AAAA,IAClD,IAAI;AAAA,MACA,IAAI,iBAAiB,QAAQ,CAAC,MAAM,QAAQ,YAAY,KAAK,OAAO,OAAO,cAAc,IAAI,GAAG;AAAA,QAC5F,OAAO;AAAA,MACX;AAAA,MACF,MAAM;AAAA,EAGZ;AAAA,EAEA,MAAM,aAAa,QAAQ;AAAA,EAC3B,IAAI,MAAM,QAAQ,UAAU,GAAG;AAAA,IAC3B,OAAO,WAAW,SAAS,IAAI;AAAA,EACnC;AAAA,EAEA,IAAI,cAAc,OAAO,eAAe,YAAY,CAAC,MAAM,QAAQ,UAAU,GAAG;AAAA,IAC5E,IAAI;AAAA,MACA,IAAI,eAAe,MAAM;AAAA,QACrB,OAAO,OAAO,OAAO,YAAY,IAAI;AAAA,MACzC;AAAA,MACF,MAAM;AAAA,EAGZ;AAAA,EAEA,OAAO;AAAA;AAGJ,SAAS,QAAQ,CAAC,SAA+D;AAAA,EACpF,IAAI,CAAC;AAAA,IAAS,OAAO,CAAC;AAAA,EAEtB,MAAM,QAAQ,IAAI;AAAA,EAElB,MAAM,eAAe,QAAQ;AAAA,EAC7B,IAAI,gBAAgB,OAAO,iBAAiB,YAAY,iBAAiB,QAAQ,CAAC,MAAM,QAAQ,YAAY,GAAG;AAAA,IAC3G,IAAI;AAAA,MACA,WAAW,OAAO,OAAO,KAAK,YAAY,GAAG;AAAA,QACzC,MAAM,IAAI,GAAG;AAAA,MACjB;AAAA,MACF,MAAM;AAAA,EAGZ;AAAA,EAEA,MAAM,aAAa,QAAQ;AAAA,EAC3B,IAAI,MAAM,QAAQ,UAAU,GAAG;AAAA,IAC3B,WAAW,QAAQ,CAAC,SAAS;AAAA,MACzB,IAAI,OAAO,SAAS;AAAA,QAAU,MAAM,IAAI,IAAI;AAAA,KAC/C;AAAA,EACL;AAAA,EAEA,IAAI,cAAc,OAAO,eAAe,YAAY,eAAe,QAAQ,CAAC,MAAM,QAAQ,UAAU,GAAG;AAAA,IACnG,IAAI;AAAA,MACA,WAAW,OAAO,OAAO,KAAK,UAAU,GAAG;AAAA,QACvC,MAAM,IAAI,GAAG;AAAA,MACjB;AAAA,MACF,MAAM;AAAA,EAGZ;AAAA,EAEA,OAAO,MAAM,KAAK,KAAK;AAAA;;AC3EpB,IAAM,kBAAkB;AAAA,EAC3B,SAAS;AAAA,EACT,WAAW;AAAA,EACX,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX,UAAU;AAAA,EACV,gBAAgB;AACpB;AAwBO,SAAS,eAAkB,CAAC,MAAS,UAAU,WAAkC;AAAA,EACpF,OAAO;AAAA,IACH,MAAM,gBAAgB;AAAA,IACtB;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACJ;AAAA;AAGG,SAAS,YAAe,CAAC,MAAS,UAAU,+BAAsD;AAAA,EACrG,OAAO;AAAA,IACH,MAAM,gBAAgB;AAAA,IACtB;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,EACJ;AAAA;AAGG,SAAS,iBAAoB,CAChC,MACA,MACA,UAAU,+BACa;AAAA,EACvB,OAAO;AAAA,IACH,MAAM,gBAAgB;AAAA,IACtB;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACJ;AAAA;AAGG,SAAS,aAAa,CAAC,MAAc,SAAiB,SAAqC;AAAA,EAC9F,MAAM,WAA6B;AAAA,IAC/B;AAAA,IACA;AAAA,IACA,QAAQ,QAAQ,MAAM,UAAU;AAAA,IAChC,MAAM;AAAA,EACV;AAAA,EAEA,IAAI,YAAY,WAAW;AAAA,IACvB,SAAS,UAAU;AAAA,EACvB;AAAA,EAEA,OAAO;AAAA;AAGJ,SAAS,gBAAgB,CAAC,UAAU,sBAAsB,SAAqC;AAAA,EAClG,OAAO,cAAc,gBAAgB,WAAW,SAAS,OAAO;AAAA;AAG7D,SAAS,uBAAuB,CAAC,SAAkB,UAAU,qBAAuC;AAAA,EACvG,OAAO,cAAc,gBAAgB,kBAAkB,SAAS,OAAO;AAAA;AAGpE,SAAS,kBAAkB,CAAC,SAAiB,SAAqC;AAAA,EACrF,OAAO,cAAc,gBAAgB,aAAa,SAAS,OAAO;AAAA;AAG/D,SAAS,oBAAoB,CAAC,UAAU,2BAA2B,SAAqC;AAAA,EAC3G,OAAO,cAAc,gBAAgB,cAAc,SAAS,OAAO;AAAA;AAGhE,SAAS,iBAAiB,CAAC,UAAU,oBAAoB,SAAqC;AAAA,EACjG,OAAO,cAAc,gBAAgB,WAAW,SAAS,OAAO;AAAA;AAG7D,SAAS,gBAAgB,CAAC,UAAU,qBAAqB,SAAqC;AAAA,EACjG,OAAO,cAAc,gBAAgB,UAAU,SAAS,OAAO;AAAA;AAG5D,SAAS,qBAAqB,CAAC,UAAU,yBAAyB,SAAqC;AAAA,EAC1G,OAAO,cAAc,gBAAgB,gBAAgB,SAAS,OAAO;AAAA;;ACzGlE,IAAM,mBAAmB;AAEzB,IAAM,mBAAmB;;ACFzB,SAAS,KAAK,GAAW;AAAA,EAC5B,OAAO,KAAK,IAAI;AAAA;AAGb,SAAS,IAAI,CAAC,OAAiE;AAAA,EAClF,IAAI,UAAU,QAAQ,UAAU;AAAA,IAAW,OAAO;AAAA,EAClD,IAAI,iBAAiB;AAAA,IAAM,OAAO,MAAM,QAAQ;AAAA,EAChD,IAAI,OAAO,UAAU,UAAU;AAAA,IAC3B,MAAM,SAAS,IAAI,KAAK,KAAK,EAAE,QAAQ;AAAA,IACvC,OAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AAAA,EACzC;AAAA,EACA,OAAO,KAAK,MAAM,KAAK;AAAA;AAGpB,SAAS,MAAM,CAAC,IAA4C;AAAA,EAC/D,IAAI,OAAO,QAAQ,OAAO,aAAa,OAAO,MAAM,EAAE;AAAA,IAAG,OAAO;AAAA,EAChE,OAAO,IAAI,KAAK,EAAE;AAAA;;;ACRf,SAAS,YAAY,CAAC,IAAY,WAA2B,QAA6B;AAAA,EAC7F,MAAM,SAAqB,EAAE,GAAG;AAAA,EAChC,IAAI,cAAc,WAAW;AAAA,IACzB,MAAM,KAAK,KAAK,SAAS;AAAA,IACzB,IAAI,OAAO,MAAM;AAAA,MACb,OAAO,YAAY;AAAA,IACvB;AAAA,EACJ;AAAA,EACA,IAAI,WAAW,WAAW;AAAA,IACtB,OAAO,SAAS;AAAA,EACpB;AAAA,EACA,OAAO;AAAA;AAGJ,SAAS,WAAW,CAAC,MAAoD;AAAA,EAC5E,MAAM,SAAS,OAAO,SAAS,WAAY,KAAK,MAAM,IAAI,IAAgC;AAAA,EAE1F,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AAAA,IACvC,MAAM,IAAI,MAAM,mCAAmC;AAAA,EACvD;AAAA,EAEA,IAAI,CAAC,OAAO,MAAM,OAAO,OAAO,OAAO,UAAU;AAAA,IAC7C,MAAM,IAAI,MAAM,uCAAuC;AAAA,EAC3D;AAAA,EAEA,MAAM,SAAqB,EAAE,IAAI,OAAO,GAAG;AAAA,EAC3C,IAAI,OAAO,OAAO,cAAc,UAAU;AAAA,IACtC,OAAO,YAAY,OAAO;AAAA,EAC9B;AAAA,EACA,IAAI,OAAO,OAAO,WAAW,UAAU;AAAA,IACnC,OAAO,SAAS,OAAO;AAAA,EAC3B;AAAA,EACA,OAAO;AAAA;AAGJ,SAAS,YAAY,CAAC,QAA4B;AAAA,EACrD,OAAO,OAAO,KAAK,KAAK,UAAU,MAAM,CAAC,EAAE,SAAS,WAAW;AAAA;AAG5D,SAAS,YAAY,CAAC,SAAyB;AAAA,EAClD,IAAI;AAAA,IACA,OAAO,OAAO,KAAK,SAAS,WAAW,EAAE,SAAS,OAAO;AAAA,IAC3D,OAAO,OAAO;AAAA,IACZ,MAAM,IAAI,MAAM,4BAA4B,OAAO,KAAK,GAAG;AAAA;AAAA;AAI5D,SAAS,oBAAoB,CAAC,IAAY,WAA2B,QAAyB;AAAA,EACjG,OAAO,aAAa,aAAa,IAAI,WAAW,MAAM,CAAC;AAAA;AAGpD,SAAS,oBAAoB,CAAC,SAA6B;AAAA,EAC9D,OAAO,YAAY,aAAa,OAAO,CAAC;AAAA;AAGrC,SAAS,eAAoE,CAChF,OACA,OACA,SACwD;AAAA,EACxD,MAAM,OAAiE;AAAA,IACnE;AAAA,IACA;AAAA,EACJ;AAAA,EAEA,IAAI,SAAS;AAAA,IACT,MAAM,WAAW,MAAM,GAAG,EAAE;AAAA,IAC5B,IAAI,UAAU;AAAA,MACV,KAAK,aAAa,qBAAqB,SAAS,IAAI,SAAS,SAAS;AAAA,IAC1E;AAAA,EACJ;AAAA,EAEA,OAAO;AAAA;;AChFJ,IAAM,YAAY;AAAA,EACrB,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,UAAU;AACd;AAAA;AAIO,MAAM,iBAAiB,MAAM;AAAA,EACvB;AAAA,EAET,WAAW,CAAC,MAAiB,SAAiB;AAAA,IAC1C,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ,KAAK,OAAO;AAAA;AAEpB;AAAA;AAEO,MAAM,sBAAsB,SAAS;AAAA,EACxC,WAAW,CAAC,SAAiB;AAAA,IACzB,MAAM,UAAU,UAAU,OAAO;AAAA,IACjC,KAAK,OAAO;AAAA;AAEpB;AAAA;AAEO,MAAM,wBAAwB,SAAS;AAAA,EAC1C,WAAW,CAAC,SAAiB;AAAA,IACzB,MAAM,UAAU,YAAY,OAAO;AAAA,IACnC,KAAK,OAAO;AAAA;AAEpB;AAAA;AAEO,MAAM,sBAAsB,SAAS;AAAA,EACxC,WAAW,CAAC,SAAiB;AAAA,IACzB,MAAM,UAAU,UAAU,OAAO;AAAA,IACjC,KAAK,OAAO;AAAA;AAEpB;AAAA;AAEO,MAAM,sBAAsB,SAAS;AAAA,EAGlB;AAAA,EAFtB,WAAW,CACP,SACkB,OACpB;AAAA,IACE,MAAM,UAAU,UAAU,OAAO;AAAA,IAFf;AAAA,IAGlB,KAAK,OAAO;AAAA;AAEpB;AAEO,SAAS,UAAU,CAAC,OAAmC;AAAA,EAC1D,OAAO,iBAAiB;AAAA;;ACnDrB,SAAS,kBAAkB,CAAC,QAAgB,SAA0B;AAAA,EACzE,IAAI,YAAY;AAAA,IAAQ,OAAO;AAAA,EAC/B,IAAI,YAAY;AAAA,IAAK,OAAO;AAAA,EAE5B,IAAI,QAAQ,SAAS,GAAG,GAAG;AAAA,IACvB,MAAM,QAAQ,QAAQ,MAAM,GAAG;AAAA,IAC/B,IAAI,MAAM,WAAW,GAAG;AAAA,MACpB,OAAO,YAAY;AAAA,IACvB;AAAA,IACA,OAAO,QAAQ,UAAU;AAAA,IACzB,IAAI,WAAW,aAAa,WAAW;AAAA,MAAW,OAAO;AAAA,IACzD,OAAO,OAAO,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM,KAAK,OAAO,UAAU,OAAO,SAAS,OAAO;AAAA,EAC3G;AAAA,EAEA,OAAO;AAAA;AAGJ,SAAS,eAAe,CAAC,QAAmC,gBAAmC;AAAA,EAClG,IAAI,CAAC;AAAA,IAAQ,OAAO;AAAA,EACpB,IAAI,CAAC,kBAAkB,eAAe,WAAW;AAAA,IAAG,OAAO;AAAA,EAE3D,OAAO,eAAe,KAAK,CAAC,YAAY,mBAAmB,QAAQ,OAAO,CAAC;AAAA;AAGxE,SAAS,kBAAkB,CAC9B,QACA,gBACA,UACM;AAAA,EACN,IAAI,UAAU,gBAAgB,QAAQ,cAAc,GAAG;AAAA,IACnD,OAAO;AAAA,EACX;AAAA,EACA,OAAO;AAAA;;AC5BX,IAAI,sBAAmC,QAAQ;AAC/C,IAAI,sBAAmC,QAAQ;AAE/C,SAAS,SAAS,CAAC,SAAiB,QAA2B;AAAA,EAC3D,OAAO,MAAM,GAAG;AAAA,CAAW;AAAA;AAGxB,SAAS,IAAI,CAAC,SAAiB,SAAsB,qBAA2B;AAAA,EACnF,UAAU,SAAS,MAAM;AAAA;AAGtB,SAAS,SAAS,CAAC,SAAiB,SAAsB,qBAA2B;AAAA,EACxF,UAAU,SAAS,MAAM;AAAA;AAGtB,SAAS,uBAAuB,CAAC,MAAkE;AAAA,EACtG,MAAM,aAAa;AAAA,EACnB,MAAM,aAAa;AAAA,EACnB,IAAI,KAAK;AAAA,IAAQ,sBAAsB,KAAK;AAAA,EAC5C,IAAI,KAAK;AAAA,IAAQ,sBAAsB,KAAK;AAAA,EAC5C,OAAO,MAAM;AAAA,IACT,sBAAsB;AAAA,IACtB,sBAAsB;AAAA;AAAA;AAUvB,SAAS,kBAAkB,GAAiB;AAAA,EAC/C,MAAM,SAAmB,CAAC;AAAA,EAC1B,OAAO;AAAA,IACH;AAAA,IACA,KAAK,CAAC,OAAe;AAAA,MACjB,OAAO,KAAK,OAAO,KAAK,CAAC;AAAA,MACzB,OAAO;AAAA;AAAA,IAEX,IAAI,GAAG;AAAA,MACH,OAAO,OAAO,KAAK,EAAE;AAAA;AAAA,IAEzB,KAAK,GAAG;AAAA,MACJ,OAAO,SAAS;AAAA;AAAA,EAExB;AAAA;",
15
+ "debugId": "B5116DB702CE581564756E2164756E21",
16
+ "names": []
17
+ }
@@ -0,0 +1,4 @@
1
+ export declare function matchOriginPattern(origin: string, pattern: string): boolean;
2
+ export declare function isAllowedOrigin(origin: string | undefined | null, allowedOrigins: string[]): boolean;
3
+ export declare function getValidatedOrigin(origin: string | undefined | null, allowedOrigins: string[], fallback: string): string;
4
+ //# sourceMappingURL=origin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origin.d.ts","sourceRoot":"","sources":["../src/origin.ts"],"names":[],"mappings":"AAAA,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAe3E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAKpG;AAED,wBAAgB,kBAAkB,CAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACjC,cAAc,EAAE,MAAM,EAAE,EACxB,QAAQ,EAAE,MAAM,GACjB,MAAM,CAKR"}
package/dist/origin.js ADDED
@@ -0,0 +1,30 @@
1
+ export function matchOriginPattern(origin, pattern) {
2
+ if (pattern === origin)
3
+ return true;
4
+ if (pattern === '*')
5
+ return true;
6
+ if (pattern.includes('*')) {
7
+ const parts = pattern.split('*');
8
+ if (parts.length !== 2) {
9
+ return pattern === origin;
10
+ }
11
+ const [prefix, suffix] = parts;
12
+ if (prefix === undefined || suffix === undefined)
13
+ return false;
14
+ return origin.startsWith(prefix) && origin.endsWith(suffix) && origin.length >= prefix.length + suffix.length;
15
+ }
16
+ return false;
17
+ }
18
+ export function isAllowedOrigin(origin, allowedOrigins) {
19
+ if (!origin)
20
+ return false;
21
+ if (!allowedOrigins || allowedOrigins.length === 0)
22
+ return false;
23
+ return allowedOrigins.some((pattern) => matchOriginPattern(origin, pattern));
24
+ }
25
+ export function getValidatedOrigin(origin, allowedOrigins, fallback) {
26
+ if (origin && isAllowedOrigin(origin, allowedOrigins)) {
27
+ return origin;
28
+ }
29
+ return fallback;
30
+ }
@@ -0,0 +1,16 @@
1
+ export interface WriteTarget {
2
+ write(chunk: string): unknown;
3
+ }
4
+ export declare function echo(message: string, target?: WriteTarget): void;
5
+ export declare function echoError(message: string, target?: WriteTarget): void;
6
+ export declare function setDefaultOutputTargets(opts: {
7
+ stdout?: WriteTarget;
8
+ stderr?: WriteTarget;
9
+ }): () => void;
10
+ export interface BufferTarget extends WriteTarget {
11
+ readonly chunks: string[];
12
+ text(): string;
13
+ clear(): void;
14
+ }
15
+ export declare function createBufferTarget(): BufferTarget;
16
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AASD,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,WAAiC,GAAG,IAAI,CAErF;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,WAAiC,GAAG,IAAI,CAE1F;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAAG,MAAM,IAAI,CASxG;AAED,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAejD"}
package/dist/output.js ADDED
@@ -0,0 +1,39 @@
1
+ let defaultStdoutTarget = process.stdout;
2
+ let defaultStderrTarget = process.stderr;
3
+ function writeLine(message, target) {
4
+ target.write(`${message}\n`);
5
+ }
6
+ export function echo(message, target = defaultStdoutTarget) {
7
+ writeLine(message, target);
8
+ }
9
+ export function echoError(message, target = defaultStderrTarget) {
10
+ writeLine(message, target);
11
+ }
12
+ export function setDefaultOutputTargets(opts) {
13
+ const prevStdout = defaultStdoutTarget;
14
+ const prevStderr = defaultStderrTarget;
15
+ if (opts.stdout)
16
+ defaultStdoutTarget = opts.stdout;
17
+ if (opts.stderr)
18
+ defaultStderrTarget = opts.stderr;
19
+ return () => {
20
+ defaultStdoutTarget = prevStdout;
21
+ defaultStderrTarget = prevStderr;
22
+ };
23
+ }
24
+ export function createBufferTarget() {
25
+ const chunks = [];
26
+ return {
27
+ chunks,
28
+ write(chunk) {
29
+ chunks.push(String(chunk));
30
+ return true;
31
+ },
32
+ text() {
33
+ return chunks.join('');
34
+ },
35
+ clear() {
36
+ chunks.length = 0;
37
+ },
38
+ };
39
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@gobing-ai/ts-utils",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency TypeScript utilities for dates, cursors, errors, output, origins, roles, and API responses.",
5
+ "type": "module",
6
+ "private": false,
7
+ "sideEffects": false,
8
+ "license": "Apache-2.0",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json && bun ../../scripts/fix-dist-esm-extensions.ts dist",
24
+ "test": "NODE_ENV=test bun test --coverage --coverage-dir=.coverage --reporter=dots",
25
+ "test:full": "NODE_ENV=test bun test --update-snapshots --coverage --coverage-dir=.coverage",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "biome check . && bun run typecheck",
28
+ "format": "biome check . --write",
29
+ "check": "bun run lint && bun run test",
30
+ "prepublishOnly": "bun run build",
31
+ "release": "npm publish --access public"
32
+ },
33
+ "dependencies": {},
34
+ "devDependencies": {
35
+ "@types/bun": "1.3.14"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
package/src/access.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Zitadel + generic role-based access control helpers.
3
+ *
4
+ * Supports Zitadel IAM role claims (`urn:zitadel:iam:org:project:roles`),
5
+ * generic `roles` arrays, and object-based role maps.
6
+ *
7
+ * If auth provider uses a different claim format, extend `hasRole`
8
+ * rather than forking this module.
9
+ */
10
+ export function hasRole(profile: Record<string, unknown> | null | undefined, role: string): boolean {
11
+ if (!profile) return false;
12
+ if (!role || typeof role !== 'string') return false;
13
+
14
+ const zitadelRoles = profile['urn:zitadel:iam:org:project:roles'];
15
+ if (zitadelRoles && typeof zitadelRoles === 'object') {
16
+ try {
17
+ if (zitadelRoles !== null && !Array.isArray(zitadelRoles) && Object.hasOwn(zitadelRoles, role)) {
18
+ return true;
19
+ }
20
+ } catch {
21
+ // Continue to other role formats when host objects reject inspection.
22
+ }
23
+ }
24
+
25
+ const rolesArray = profile.roles;
26
+ if (Array.isArray(rolesArray)) {
27
+ return rolesArray.includes(role);
28
+ }
29
+
30
+ if (rolesArray && typeof rolesArray === 'object' && !Array.isArray(rolesArray)) {
31
+ try {
32
+ if (rolesArray !== null) {
33
+ return Object.hasOwn(rolesArray, role);
34
+ }
35
+ } catch {
36
+ // Fall through.
37
+ }
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ export function getRoles(profile: Record<string, unknown> | null | undefined): string[] {
44
+ if (!profile) return [];
45
+
46
+ const roles = new Set<string>();
47
+
48
+ const zitadelRoles = profile['urn:zitadel:iam:org:project:roles'];
49
+ if (zitadelRoles && typeof zitadelRoles === 'object' && zitadelRoles !== null && !Array.isArray(zitadelRoles)) {
50
+ try {
51
+ for (const key of Object.keys(zitadelRoles)) {
52
+ roles.add(key);
53
+ }
54
+ } catch {
55
+ // Ignore host objects that reject key enumeration.
56
+ }
57
+ }
58
+
59
+ const rolesArray = profile.roles;
60
+ if (Array.isArray(rolesArray)) {
61
+ rolesArray.forEach((role) => {
62
+ if (typeof role === 'string') roles.add(role);
63
+ });
64
+ }
65
+
66
+ if (rolesArray && typeof rolesArray === 'object' && rolesArray !== null && !Array.isArray(rolesArray)) {
67
+ try {
68
+ for (const key of Object.keys(rolesArray)) {
69
+ roles.add(key);
70
+ }
71
+ } catch {
72
+ // Ignore host objects that reject key enumeration.
73
+ }
74
+ }
75
+
76
+ return Array.from(roles);
77
+ }
@@ -0,0 +1,107 @@
1
+ export const API_ERROR_CODES = {
2
+ SUCCESS: 0,
3
+ NOT_FOUND: 404,
4
+ VALIDATION_ERROR: 422,
5
+ BAD_REQUEST: 400,
6
+ UNAUTHORIZED: 401,
7
+ FORBIDDEN: 403,
8
+ CONFLICT: 409,
9
+ INTERNAL_ERROR: 500,
10
+ } as const;
11
+
12
+ export type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
13
+
14
+ export type ApiEnvelopeResult = 'success' | 'info' | 'warn' | 'error';
15
+
16
+ export interface ApiSuccessEnvelope<T> {
17
+ code: 0;
18
+ message: string;
19
+ result: 'success' | 'info';
20
+ data: T;
21
+ meta?: { total?: number; limit?: number; offset?: number };
22
+ }
23
+
24
+ export interface ApiErrorEnvelope {
25
+ result: 'warn' | 'error';
26
+ code: number;
27
+ message: string;
28
+ data: null;
29
+ details?: unknown;
30
+ }
31
+
32
+ export type ApiEnvelope<T> = ApiSuccessEnvelope<T> | ApiErrorEnvelope;
33
+
34
+ export function successResponse<T>(data: T, message = 'Success'): ApiSuccessEnvelope<T> {
35
+ return {
36
+ code: API_ERROR_CODES.SUCCESS,
37
+ message,
38
+ result: 'success',
39
+ data,
40
+ };
41
+ }
42
+
43
+ export function infoResponse<T>(data: T, message = 'Data retrieved successfully'): ApiSuccessEnvelope<T> {
44
+ return {
45
+ code: API_ERROR_CODES.SUCCESS,
46
+ message,
47
+ result: 'info',
48
+ data,
49
+ };
50
+ }
51
+
52
+ export function paginatedResponse<T>(
53
+ data: T[],
54
+ meta: { total?: number; limit?: number; offset?: number },
55
+ message = 'Data retrieved successfully',
56
+ ): ApiSuccessEnvelope<T[]> {
57
+ return {
58
+ code: API_ERROR_CODES.SUCCESS,
59
+ message,
60
+ result: 'info',
61
+ data,
62
+ meta,
63
+ };
64
+ }
65
+
66
+ export function errorResponse(code: number, message: string, details?: unknown): ApiErrorEnvelope {
67
+ const response: ApiErrorEnvelope = {
68
+ code,
69
+ message,
70
+ result: code >= 500 ? 'error' : 'warn',
71
+ data: null,
72
+ };
73
+
74
+ if (details !== undefined) {
75
+ response.details = details;
76
+ }
77
+
78
+ return response;
79
+ }
80
+
81
+ export function notFoundResponse(message = 'Resource not found', details?: unknown): ApiErrorEnvelope {
82
+ return errorResponse(API_ERROR_CODES.NOT_FOUND, message, details);
83
+ }
84
+
85
+ export function validationErrorResponse(details: unknown, message = 'Validation failed'): ApiErrorEnvelope {
86
+ return errorResponse(API_ERROR_CODES.VALIDATION_ERROR, message, details);
87
+ }
88
+
89
+ export function badRequestResponse(message: string, details?: unknown): ApiErrorEnvelope {
90
+ return errorResponse(API_ERROR_CODES.BAD_REQUEST, message, details);
91
+ }
92
+
93
+ export function unauthorizedResponse(message = 'Authentication required', details?: unknown): ApiErrorEnvelope {
94
+ return errorResponse(API_ERROR_CODES.UNAUTHORIZED, message, details);
95
+ }
96
+
97
+ export function forbiddenResponse(message = 'Access forbidden', details?: unknown): ApiErrorEnvelope {
98
+ return errorResponse(API_ERROR_CODES.FORBIDDEN, message, details);
99
+ }
100
+
101
+ export function conflictResponse(message = 'Resource conflict', details?: unknown): ApiErrorEnvelope {
102
+ return errorResponse(API_ERROR_CODES.CONFLICT, message, details);
103
+ }
104
+
105
+ export function internalErrorResponse(message = 'Internal server error', details?: unknown): ApiErrorEnvelope {
106
+ return errorResponse(API_ERROR_CODES.INTERNAL_ERROR, message, details);
107
+ }
package/src/const.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const LOG_CATEGORY_APP = 'app';
2
+
3
+ export const LOG_CATEGORY_CLI = 'cli';
package/src/cursor.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { toMs } from './date';
2
+
3
+ export interface CursorData {
4
+ id: string;
5
+ createdAt?: number;
6
+ offset?: number;
7
+ }
8
+
9
+ export function createCursor(id: string, createdAt?: Date | number, offset?: number): CursorData {
10
+ const cursor: CursorData = { id };
11
+ if (createdAt !== undefined) {
12
+ const ms = toMs(createdAt);
13
+ if (ms !== null) {
14
+ cursor.createdAt = ms;
15
+ }
16
+ }
17
+ if (offset !== undefined) {
18
+ cursor.offset = offset;
19
+ }
20
+ return cursor;
21
+ }
22
+
23
+ export function parseCursor(data: string | Record<string, unknown>): CursorData {
24
+ const parsed = typeof data === 'string' ? (JSON.parse(data) as Record<string, unknown>) : data;
25
+
26
+ if (!parsed || typeof parsed !== 'object') {
27
+ throw new Error('Invalid cursor: must be an object');
28
+ }
29
+
30
+ if (!parsed.id || typeof parsed.id !== 'string') {
31
+ throw new Error('Invalid cursor: missing or invalid id');
32
+ }
33
+
34
+ const result: CursorData = { id: parsed.id };
35
+ if (typeof parsed.createdAt === 'number') {
36
+ result.createdAt = parsed.createdAt;
37
+ }
38
+ if (typeof parsed.offset === 'number') {
39
+ result.offset = parsed.offset;
40
+ }
41
+ return result;
42
+ }
43
+
44
+ export function encodeCursor(cursor: CursorData): string {
45
+ return Buffer.from(JSON.stringify(cursor)).toString('base64url');
46
+ }
47
+
48
+ export function decodeCursor(encoded: string): string {
49
+ try {
50
+ return Buffer.from(encoded, 'base64url').toString('utf-8');
51
+ } catch (error) {
52
+ throw new Error(`Invalid cursor encoding: ${String(error)}`);
53
+ }
54
+ }
55
+
56
+ export function encodeCursorFromItem(id: string, createdAt?: Date | number, offset?: number): string {
57
+ return encodeCursor(createCursor(id, createdAt, offset));
58
+ }
59
+
60
+ export function decodeAndParseCursor(encoded: string): CursorData {
61
+ return parseCursor(decodeCursor(encoded));
62
+ }
63
+
64
+ export function buildCursorMeta<T extends { id: string; createdAt?: number | Date }>(
65
+ items: T[],
66
+ limit: number,
67
+ hasMore: boolean,
68
+ ): { nextCursor?: string; hasMore: boolean; limit: number } {
69
+ const meta: { nextCursor?: string; hasMore: boolean; limit: number } = {
70
+ hasMore,
71
+ limit,
72
+ };
73
+
74
+ if (hasMore) {
75
+ const lastItem = items.at(-1);
76
+ if (lastItem) {
77
+ meta.nextCursor = encodeCursorFromItem(lastItem.id, lastItem.createdAt);
78
+ }
79
+ }
80
+
81
+ return meta;
82
+ }
package/src/date.ts ADDED
@@ -0,0 +1,18 @@
1
+ export function nowMs(): number {
2
+ return Date.now();
3
+ }
4
+
5
+ export function toMs(input: Date | number | string | null | undefined): number | null {
6
+ if (input === null || input === undefined) return null;
7
+ if (input instanceof Date) return input.getTime();
8
+ if (typeof input === 'string') {
9
+ const parsed = new Date(input).getTime();
10
+ return Number.isNaN(parsed) ? null : parsed;
11
+ }
12
+ return Math.floor(input);
13
+ }
14
+
15
+ export function fromMs(ms: number | null | undefined): Date | null {
16
+ if (ms === null || ms === undefined || Number.isNaN(ms)) return null;
17
+ return new Date(ms);
18
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,53 @@
1
+ export const ErrorCode = {
2
+ NotFound: 'NOT_FOUND',
3
+ Validation: 'VALIDATION',
4
+ Conflict: 'CONFLICT',
5
+ Internal: 'INTERNAL',
6
+ } as const;
7
+
8
+ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
9
+
10
+ export class AppError extends Error {
11
+ readonly code: ErrorCode;
12
+
13
+ constructor(code: ErrorCode, message: string) {
14
+ super(message);
15
+ this.name = 'AppError';
16
+ this.code = code;
17
+ }
18
+ }
19
+
20
+ export class NotFoundError extends AppError {
21
+ constructor(message: string) {
22
+ super(ErrorCode.NotFound, message);
23
+ this.name = 'NotFoundError';
24
+ }
25
+ }
26
+
27
+ export class ValidationError extends AppError {
28
+ constructor(message: string) {
29
+ super(ErrorCode.Validation, message);
30
+ this.name = 'ValidationError';
31
+ }
32
+ }
33
+
34
+ export class ConflictError extends AppError {
35
+ constructor(message: string) {
36
+ super(ErrorCode.Conflict, message);
37
+ this.name = 'ConflictError';
38
+ }
39
+ }
40
+
41
+ export class InternalError extends AppError {
42
+ constructor(
43
+ message: string,
44
+ override readonly cause?: unknown,
45
+ ) {
46
+ super(ErrorCode.Internal, message);
47
+ this.name = 'InternalError';
48
+ }
49
+ }
50
+
51
+ export function isAppError(error: unknown): error is AppError {
52
+ return error instanceof AppError;
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './access';
2
+ export * from './api-response';
3
+ export * from './const';
4
+ export * from './cursor';
5
+ export * from './date';
6
+ export * from './errors';
7
+ export * from './origin';
8
+ export * from './output';
package/src/origin.ts ADDED
@@ -0,0 +1,34 @@
1
+ export function matchOriginPattern(origin: string, pattern: string): boolean {
2
+ if (pattern === origin) return true;
3
+ if (pattern === '*') return true;
4
+
5
+ if (pattern.includes('*')) {
6
+ const parts = pattern.split('*');
7
+ if (parts.length !== 2) {
8
+ return pattern === origin;
9
+ }
10
+ const [prefix, suffix] = parts;
11
+ if (prefix === undefined || suffix === undefined) return false;
12
+ return origin.startsWith(prefix) && origin.endsWith(suffix) && origin.length >= prefix.length + suffix.length;
13
+ }
14
+
15
+ return false;
16
+ }
17
+
18
+ export function isAllowedOrigin(origin: string | undefined | null, allowedOrigins: string[]): boolean {
19
+ if (!origin) return false;
20
+ if (!allowedOrigins || allowedOrigins.length === 0) return false;
21
+
22
+ return allowedOrigins.some((pattern) => matchOriginPattern(origin, pattern));
23
+ }
24
+
25
+ export function getValidatedOrigin(
26
+ origin: string | undefined | null,
27
+ allowedOrigins: string[],
28
+ fallback: string,
29
+ ): string {
30
+ if (origin && isAllowedOrigin(origin, allowedOrigins)) {
31
+ return origin;
32
+ }
33
+ return fallback;
34
+ }
package/src/output.ts ADDED
@@ -0,0 +1,52 @@
1
+ export interface WriteTarget {
2
+ write(chunk: string): unknown;
3
+ }
4
+
5
+ let defaultStdoutTarget: WriteTarget = process.stdout;
6
+ let defaultStderrTarget: WriteTarget = process.stderr;
7
+
8
+ function writeLine(message: string, target: WriteTarget): void {
9
+ target.write(`${message}\n`);
10
+ }
11
+
12
+ export function echo(message: string, target: WriteTarget = defaultStdoutTarget): void {
13
+ writeLine(message, target);
14
+ }
15
+
16
+ export function echoError(message: string, target: WriteTarget = defaultStderrTarget): void {
17
+ writeLine(message, target);
18
+ }
19
+
20
+ export function setDefaultOutputTargets(opts: { stdout?: WriteTarget; stderr?: WriteTarget }): () => void {
21
+ const prevStdout = defaultStdoutTarget;
22
+ const prevStderr = defaultStderrTarget;
23
+ if (opts.stdout) defaultStdoutTarget = opts.stdout;
24
+ if (opts.stderr) defaultStderrTarget = opts.stderr;
25
+ return () => {
26
+ defaultStdoutTarget = prevStdout;
27
+ defaultStderrTarget = prevStderr;
28
+ };
29
+ }
30
+
31
+ export interface BufferTarget extends WriteTarget {
32
+ readonly chunks: string[];
33
+ text(): string;
34
+ clear(): void;
35
+ }
36
+
37
+ export function createBufferTarget(): BufferTarget {
38
+ const chunks: string[] = [];
39
+ return {
40
+ chunks,
41
+ write(chunk: string) {
42
+ chunks.push(String(chunk));
43
+ return true;
44
+ },
45
+ text() {
46
+ return chunks.join('');
47
+ },
48
+ clear() {
49
+ chunks.length = 0;
50
+ },
51
+ };
52
+ }