@enderworld/onlyapi 1.5.1

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 (160) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/LICENSE +21 -0
  3. package/README.md +338 -0
  4. package/dist/cli.js +14 -0
  5. package/package.json +69 -0
  6. package/src/application/dtos/admin.dto.ts +25 -0
  7. package/src/application/dtos/auth.dto.ts +97 -0
  8. package/src/application/dtos/index.ts +40 -0
  9. package/src/application/index.ts +2 -0
  10. package/src/application/services/admin.service.ts +150 -0
  11. package/src/application/services/api-key.service.ts +65 -0
  12. package/src/application/services/auth.service.ts +606 -0
  13. package/src/application/services/health.service.ts +97 -0
  14. package/src/application/services/index.ts +10 -0
  15. package/src/application/services/user.service.ts +95 -0
  16. package/src/cli/commands/help.ts +86 -0
  17. package/src/cli/commands/init.ts +301 -0
  18. package/src/cli/commands/upgrade.ts +471 -0
  19. package/src/cli/index.ts +76 -0
  20. package/src/cli/ui.ts +189 -0
  21. package/src/cluster.ts +62 -0
  22. package/src/core/entities/index.ts +1 -0
  23. package/src/core/entities/user.entity.ts +24 -0
  24. package/src/core/errors/app-error.ts +81 -0
  25. package/src/core/errors/index.ts +15 -0
  26. package/src/core/index.ts +7 -0
  27. package/src/core/ports/account-lockout.ts +15 -0
  28. package/src/core/ports/alert-sink.ts +27 -0
  29. package/src/core/ports/api-key.ts +37 -0
  30. package/src/core/ports/audit-log.ts +46 -0
  31. package/src/core/ports/cache.ts +24 -0
  32. package/src/core/ports/circuit-breaker.ts +42 -0
  33. package/src/core/ports/event-bus.ts +78 -0
  34. package/src/core/ports/index.ts +62 -0
  35. package/src/core/ports/job-queue.ts +73 -0
  36. package/src/core/ports/logger.ts +21 -0
  37. package/src/core/ports/metrics.ts +49 -0
  38. package/src/core/ports/oauth.ts +55 -0
  39. package/src/core/ports/password-hasher.ts +10 -0
  40. package/src/core/ports/password-history.ts +23 -0
  41. package/src/core/ports/password-policy.ts +43 -0
  42. package/src/core/ports/refresh-token-store.ts +37 -0
  43. package/src/core/ports/retry.ts +23 -0
  44. package/src/core/ports/token-blacklist.ts +16 -0
  45. package/src/core/ports/token-service.ts +23 -0
  46. package/src/core/ports/totp-service.ts +16 -0
  47. package/src/core/ports/user.repository.ts +40 -0
  48. package/src/core/ports/verification-token.ts +41 -0
  49. package/src/core/ports/webhook.ts +58 -0
  50. package/src/core/types/brand.ts +19 -0
  51. package/src/core/types/index.ts +19 -0
  52. package/src/core/types/pagination.ts +28 -0
  53. package/src/core/types/result.ts +52 -0
  54. package/src/infrastructure/alerting/index.ts +1 -0
  55. package/src/infrastructure/alerting/webhook.ts +100 -0
  56. package/src/infrastructure/cache/in-memory-cache.ts +111 -0
  57. package/src/infrastructure/cache/index.ts +6 -0
  58. package/src/infrastructure/cache/redis-cache.ts +204 -0
  59. package/src/infrastructure/config/config.ts +185 -0
  60. package/src/infrastructure/config/index.ts +1 -0
  61. package/src/infrastructure/database/in-memory-user.repository.ts +134 -0
  62. package/src/infrastructure/database/index.ts +37 -0
  63. package/src/infrastructure/database/migrations/001_create_users.ts +26 -0
  64. package/src/infrastructure/database/migrations/002_create_token_blacklist.ts +21 -0
  65. package/src/infrastructure/database/migrations/003_create_audit_log.ts +31 -0
  66. package/src/infrastructure/database/migrations/004_auth_platform.ts +112 -0
  67. package/src/infrastructure/database/migrations/runner.ts +120 -0
  68. package/src/infrastructure/database/mssql/index.ts +14 -0
  69. package/src/infrastructure/database/mssql/migrations.ts +299 -0
  70. package/src/infrastructure/database/mssql/mssql-account-lockout.ts +95 -0
  71. package/src/infrastructure/database/mssql/mssql-api-keys.ts +146 -0
  72. package/src/infrastructure/database/mssql/mssql-audit-log.ts +86 -0
  73. package/src/infrastructure/database/mssql/mssql-oauth-accounts.ts +118 -0
  74. package/src/infrastructure/database/mssql/mssql-password-history.ts +71 -0
  75. package/src/infrastructure/database/mssql/mssql-refresh-token-store.ts +144 -0
  76. package/src/infrastructure/database/mssql/mssql-token-blacklist.ts +54 -0
  77. package/src/infrastructure/database/mssql/mssql-user.repository.ts +263 -0
  78. package/src/infrastructure/database/mssql/mssql-verification-tokens.ts +120 -0
  79. package/src/infrastructure/database/postgres/index.ts +14 -0
  80. package/src/infrastructure/database/postgres/migrations.ts +235 -0
  81. package/src/infrastructure/database/postgres/pg-account-lockout.ts +75 -0
  82. package/src/infrastructure/database/postgres/pg-api-keys.ts +126 -0
  83. package/src/infrastructure/database/postgres/pg-audit-log.ts +74 -0
  84. package/src/infrastructure/database/postgres/pg-oauth-accounts.ts +101 -0
  85. package/src/infrastructure/database/postgres/pg-password-history.ts +61 -0
  86. package/src/infrastructure/database/postgres/pg-refresh-token-store.ts +117 -0
  87. package/src/infrastructure/database/postgres/pg-token-blacklist.ts +48 -0
  88. package/src/infrastructure/database/postgres/pg-user.repository.ts +237 -0
  89. package/src/infrastructure/database/postgres/pg-verification-tokens.ts +97 -0
  90. package/src/infrastructure/database/sqlite-account-lockout.ts +97 -0
  91. package/src/infrastructure/database/sqlite-api-keys.ts +155 -0
  92. package/src/infrastructure/database/sqlite-audit-log.ts +90 -0
  93. package/src/infrastructure/database/sqlite-oauth-accounts.ts +105 -0
  94. package/src/infrastructure/database/sqlite-password-history.ts +54 -0
  95. package/src/infrastructure/database/sqlite-refresh-token-store.ts +122 -0
  96. package/src/infrastructure/database/sqlite-token-blacklist.ts +47 -0
  97. package/src/infrastructure/database/sqlite-user.repository.ts +260 -0
  98. package/src/infrastructure/database/sqlite-verification-tokens.ts +112 -0
  99. package/src/infrastructure/events/event-bus.ts +105 -0
  100. package/src/infrastructure/events/event-factory.ts +31 -0
  101. package/src/infrastructure/events/in-memory-webhook-registry.ts +67 -0
  102. package/src/infrastructure/events/index.ts +4 -0
  103. package/src/infrastructure/events/webhook-dispatcher.ts +114 -0
  104. package/src/infrastructure/index.ts +58 -0
  105. package/src/infrastructure/jobs/index.ts +1 -0
  106. package/src/infrastructure/jobs/job-queue.ts +185 -0
  107. package/src/infrastructure/logging/index.ts +1 -0
  108. package/src/infrastructure/logging/logger.ts +63 -0
  109. package/src/infrastructure/metrics/index.ts +1 -0
  110. package/src/infrastructure/metrics/prometheus.ts +231 -0
  111. package/src/infrastructure/oauth/github.ts +116 -0
  112. package/src/infrastructure/oauth/google.ts +83 -0
  113. package/src/infrastructure/oauth/index.ts +2 -0
  114. package/src/infrastructure/resilience/circuit-breaker.ts +133 -0
  115. package/src/infrastructure/resilience/index.ts +2 -0
  116. package/src/infrastructure/resilience/retry.ts +50 -0
  117. package/src/infrastructure/security/account-lockout.ts +73 -0
  118. package/src/infrastructure/security/index.ts +6 -0
  119. package/src/infrastructure/security/password-hasher.ts +31 -0
  120. package/src/infrastructure/security/password-policy.ts +77 -0
  121. package/src/infrastructure/security/token-blacklist.ts +45 -0
  122. package/src/infrastructure/security/token-service.ts +144 -0
  123. package/src/infrastructure/security/totp-service.ts +142 -0
  124. package/src/infrastructure/tracing/index.ts +7 -0
  125. package/src/infrastructure/tracing/trace-context.ts +93 -0
  126. package/src/main.ts +479 -0
  127. package/src/presentation/context.ts +26 -0
  128. package/src/presentation/handlers/admin.handler.ts +114 -0
  129. package/src/presentation/handlers/api-key.handler.ts +68 -0
  130. package/src/presentation/handlers/auth.handler.ts +218 -0
  131. package/src/presentation/handlers/health.handler.ts +27 -0
  132. package/src/presentation/handlers/index.ts +15 -0
  133. package/src/presentation/handlers/metrics.handler.ts +21 -0
  134. package/src/presentation/handlers/oauth.handler.ts +61 -0
  135. package/src/presentation/handlers/openapi.handler.ts +543 -0
  136. package/src/presentation/handlers/response.ts +29 -0
  137. package/src/presentation/handlers/sse.handler.ts +165 -0
  138. package/src/presentation/handlers/user.handler.ts +81 -0
  139. package/src/presentation/handlers/webhook.handler.ts +92 -0
  140. package/src/presentation/handlers/websocket.handler.ts +226 -0
  141. package/src/presentation/i18n/index.ts +254 -0
  142. package/src/presentation/index.ts +5 -0
  143. package/src/presentation/middleware/api-key.ts +18 -0
  144. package/src/presentation/middleware/auth.ts +39 -0
  145. package/src/presentation/middleware/cors.ts +41 -0
  146. package/src/presentation/middleware/index.ts +12 -0
  147. package/src/presentation/middleware/rate-limit.ts +65 -0
  148. package/src/presentation/middleware/security-headers.ts +18 -0
  149. package/src/presentation/middleware/validate.ts +16 -0
  150. package/src/presentation/middleware/versioning.ts +69 -0
  151. package/src/presentation/routes/index.ts +1 -0
  152. package/src/presentation/routes/router.ts +272 -0
  153. package/src/presentation/server.ts +381 -0
  154. package/src/shared/cli.ts +294 -0
  155. package/src/shared/container.ts +65 -0
  156. package/src/shared/index.ts +2 -0
  157. package/src/shared/log-format.ts +148 -0
  158. package/src/shared/utils/id.ts +5 -0
  159. package/src/shared/utils/index.ts +2 -0
  160. package/src/shared/utils/timing-safe.ts +20 -0
@@ -0,0 +1,142 @@
1
+ import { type AppError, internal } from "../../core/errors/app-error.js";
2
+ import type { TotpService } from "../../core/ports/totp-service.js";
3
+ import { type Result, err, ok } from "../../core/types/result.js";
4
+
5
+ /**
6
+ * TOTP implementation (RFC 6238) using Web Crypto HMAC-SHA1.
7
+ * Compatible with Google Authenticator, Authy, Microsoft Authenticator, etc.
8
+ * Zero external dependencies.
9
+ */
10
+
11
+ /** Base32 alphabet (RFC 4648) */
12
+ const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
13
+
14
+ /** Encode a Uint8Array to base32 */
15
+ const base32Encode = (data: Uint8Array): string => {
16
+ let result = "";
17
+ let bits = 0;
18
+ let value = 0;
19
+ for (const byte of data) {
20
+ value = (value << 8) | byte;
21
+ bits += 8;
22
+ while (bits >= 5) {
23
+ bits -= 5;
24
+ result += BASE32_CHARS[(value >>> bits) & 0x1f];
25
+ }
26
+ }
27
+ if (bits > 0) {
28
+ result += BASE32_CHARS[(value << (5 - bits)) & 0x1f];
29
+ }
30
+ return result;
31
+ };
32
+
33
+ /** Decode a base32 string to Uint8Array */
34
+ const base32Decode = (encoded: string): Uint8Array => {
35
+ const cleaned = encoded.replace(/=+$/, "").toUpperCase();
36
+ const bytes: number[] = [];
37
+ let bits = 0;
38
+ let value = 0;
39
+ for (const char of cleaned) {
40
+ const idx = BASE32_CHARS.indexOf(char);
41
+ if (idx === -1) continue;
42
+ value = (value << 5) | idx;
43
+ bits += 5;
44
+ if (bits >= 8) {
45
+ bits -= 8;
46
+ bytes.push((value >>> bits) & 0xff);
47
+ }
48
+ }
49
+ return new Uint8Array(bytes);
50
+ };
51
+
52
+ /** Convert a number to an 8-byte big-endian buffer */
53
+ const intToBytes = (num: number): Uint8Array => {
54
+ const buf = new Uint8Array(8);
55
+ let n = num;
56
+ for (let i = 7; i >= 0; i--) {
57
+ buf[i] = n & 0xff;
58
+ n = Math.floor(n / 256);
59
+ }
60
+ return buf;
61
+ };
62
+
63
+ /** Dynamic truncation (RFC 4226 §5.4) */
64
+ const dynamicTruncate = (hmacResult: Uint8Array): number => {
65
+ // biome-ignore lint/style/noNonNullAssertion: HMAC result always has at least 20 bytes (SHA-1)
66
+ const offset = hmacResult[hmacResult.length - 1]! & 0x0f;
67
+ return (
68
+ // biome-ignore lint/style/noNonNullAssertion: HMAC result always has at least 20 bytes (SHA-1)
69
+ ((hmacResult[offset]! & 0x7f) << 24) |
70
+ // biome-ignore lint/style/noNonNullAssertion: HMAC result always has at least 20 bytes (SHA-1)
71
+ ((hmacResult[offset + 1]! & 0xff) << 16) |
72
+ // biome-ignore lint/style/noNonNullAssertion: HMAC result always has at least 20 bytes (SHA-1)
73
+ ((hmacResult[offset + 2]! & 0xff) << 8) |
74
+ // biome-ignore lint/style/noNonNullAssertion: HMAC result always has at least 20 bytes (SHA-1)
75
+ (hmacResult[offset + 3]! & 0xff)
76
+ );
77
+ };
78
+
79
+ /** TOTP period in seconds */
80
+ const PERIOD = 30;
81
+
82
+ /** Number of periods to allow for clock drift (±1 window = ±30s) */
83
+ const WINDOW = 1;
84
+
85
+ export const createTotpService = (): TotpService => ({
86
+ generateSecret(): string {
87
+ const bytes = new Uint8Array(20); // 160 bits — standard for TOTP
88
+ crypto.getRandomValues(bytes);
89
+ return base32Encode(bytes);
90
+ },
91
+
92
+ generateUri(secret: string, email: string, issuer: string): string {
93
+ const encodedIssuer = encodeURIComponent(issuer);
94
+ const encodedEmail = encodeURIComponent(email);
95
+ return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=${PERIOD}`;
96
+ },
97
+
98
+ verify(secret: string, code: string): Result<boolean, AppError> {
99
+ if (!/^\d{6}$/.test(code)) {
100
+ return ok(false);
101
+ }
102
+
103
+ const secretBytes = base32Decode(secret);
104
+ const now = Math.floor(Date.now() / 1000);
105
+ const currentCounter = Math.floor(now / PERIOD);
106
+
107
+ // We need to do async HMAC but the port returns sync Result.
108
+ // Use a synchronous Bun.CryptoHasher approach instead.
109
+ // Actually, let's do a synchronous HMAC using Bun's native crypto.
110
+ try {
111
+ for (let i = -WINDOW; i <= WINDOW; i++) {
112
+ const counter = currentCounter + i;
113
+ const counterBytes = intToBytes(counter);
114
+ // biome-ignore lint/suspicious/noExplicitAny: Bun.CryptoHasher buffer compat
115
+ const hasher = new Bun.CryptoHasher("sha1", secretBytes as any);
116
+ // biome-ignore lint/suspicious/noExplicitAny: Bun.CryptoHasher buffer compat
117
+ hasher.update(counterBytes as any);
118
+ const digest = hasher.digest();
119
+ const hmac = new Uint8Array(digest.buffer, digest.byteOffset, digest.byteLength);
120
+ const truncated = dynamicTruncate(hmac) % 1_000_000;
121
+ const expected = truncated.toString().padStart(6, "0");
122
+ // Timing-safe comparison
123
+ if (timingSafeEqual(code, expected)) {
124
+ return ok(true);
125
+ }
126
+ }
127
+ return ok(false);
128
+ } catch (e: unknown) {
129
+ return err(internal("TOTP verification failed", e));
130
+ }
131
+ },
132
+ });
133
+
134
+ /** Timing-safe string comparison */
135
+ const timingSafeEqual = (a: string, b: string): boolean => {
136
+ if (a.length !== b.length) return false;
137
+ let result = 0;
138
+ for (let i = 0; i < a.length; i++) {
139
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
140
+ }
141
+ return result === 0;
142
+ };
@@ -0,0 +1,7 @@
1
+ export {
2
+ resolveTraceContext,
3
+ parseTraceparent,
4
+ createTraceContext,
5
+ formatTraceparent,
6
+ type TraceContext,
7
+ } from "./trace-context.js";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * W3C Trace Context implementation — zero-dependency OpenTelemetry-compatible tracing.
3
+ *
4
+ * Implements the W3C Trace Context specification:
5
+ * - Generates trace IDs (128-bit / 32 hex chars)
6
+ * - Generates span IDs (64-bit / 16 hex chars)
7
+ * - Parses incoming `traceparent` header
8
+ * - Propagates trace context in responses
9
+ *
10
+ * Format: `{version}-{traceId}-{parentSpanId}-{flags}`
11
+ * Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`
12
+ */
13
+
14
+ const TRACE_ID_LENGTH = 32; // 128-bit → 32 hex chars
15
+ const SPAN_ID_LENGTH = 16; // 64-bit → 16 hex chars
16
+ const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
17
+
18
+ export interface TraceContext {
19
+ /** 128-bit trace ID (32 hex chars) */
20
+ readonly traceId: string;
21
+ /** 64-bit span ID for this request (16 hex chars) */
22
+ readonly spanId: string;
23
+ /** 64-bit parent span ID from incoming header (16 hex chars, or undefined if root) */
24
+ readonly parentSpanId: string | undefined;
25
+ /** Trace flags (bit 0 = sampled) */
26
+ readonly flags: number;
27
+ }
28
+
29
+ /**
30
+ * Generate a random hex string of the given byte length.
31
+ * Uses crypto.getRandomValues for high-quality randomness.
32
+ */
33
+ const randomHex = (byteLength: number): string => {
34
+ const bytes = new Uint8Array(byteLength);
35
+ crypto.getRandomValues(bytes);
36
+ let hex = "";
37
+ for (let i = 0; i < byteLength; i++) {
38
+ // biome-ignore lint/style/noNonNullAssertion: bounds guaranteed by loop
39
+ hex += bytes[i]!.toString(16).padStart(2, "0");
40
+ }
41
+ return hex;
42
+ };
43
+
44
+ /**
45
+ * Parse an incoming `traceparent` header into trace context.
46
+ * Returns undefined if the header is missing, invalid, or all-zero IDs.
47
+ */
48
+ export const parseTraceparent = (header: string | null): TraceContext | undefined => {
49
+ if (!header) return undefined;
50
+
51
+ const match = TRACEPARENT_REGEX.exec(header.trim().toLowerCase());
52
+ if (!match) return undefined;
53
+
54
+ const traceId = match[1] as string;
55
+ const parentSpanId = match[2] as string;
56
+ const flags = Number.parseInt(match[3] as string, 16);
57
+
58
+ // Reject all-zero trace or span IDs
59
+ if (traceId === "0".repeat(TRACE_ID_LENGTH)) return undefined;
60
+ if (parentSpanId === "0".repeat(SPAN_ID_LENGTH)) return undefined;
61
+
62
+ return {
63
+ traceId,
64
+ spanId: randomHex(8), // new span for this service
65
+ parentSpanId,
66
+ flags,
67
+ };
68
+ };
69
+
70
+ /**
71
+ * Create a new root trace context (no incoming traceparent).
72
+ */
73
+ export const createTraceContext = (): TraceContext => ({
74
+ traceId: randomHex(16),
75
+ spanId: randomHex(8),
76
+ parentSpanId: undefined,
77
+ flags: 1, // sampled by default
78
+ });
79
+
80
+ /**
81
+ * Create or propagate trace context from an incoming request.
82
+ */
83
+ export const resolveTraceContext = (traceparentHeader: string | null): TraceContext => {
84
+ return parseTraceparent(traceparentHeader) ?? createTraceContext();
85
+ };
86
+
87
+ /**
88
+ * Format trace context into a `traceparent` response header value.
89
+ */
90
+ export const formatTraceparent = (ctx: TraceContext): string => {
91
+ const flagsHex = ctx.flags.toString(16).padStart(2, "0");
92
+ return `00-${ctx.traceId}-${ctx.spanId}-${flagsHex}`;
93
+ };