@technomoron/apicore-server 1.0.0-beta.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/api-module.cjs +34 -0
  3. package/dist/cjs/api-module.d.ts +45 -0
  4. package/dist/cjs/apicore-server.cjs +1561 -0
  5. package/dist/cjs/apicore-server.d.ts +288 -0
  6. package/dist/cjs/auth-api/auth-module.cjs +1248 -0
  7. package/dist/cjs/auth-api/auth-module.d.ts +116 -0
  8. package/dist/cjs/auth-api/compat-auth-storage.cjs +128 -0
  9. package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
  10. package/dist/cjs/auth-api/mem-auth-store.cjs +121 -0
  11. package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
  12. package/dist/cjs/auth-api/module.cjs +25 -0
  13. package/dist/cjs/auth-api/module.d.ts +20 -0
  14. package/dist/cjs/auth-api/schemas.cjs +171 -0
  15. package/dist/cjs/auth-api/schemas.d.ts +21 -0
  16. package/dist/cjs/auth-api/sql-auth-store.cjs +179 -0
  17. package/dist/cjs/auth-api/sql-auth-store.d.ts +87 -0
  18. package/dist/cjs/auth-api/storage.cjs +102 -0
  19. package/dist/cjs/auth-api/storage.d.ts +38 -0
  20. package/dist/cjs/auth-api/types.cjs +2 -0
  21. package/dist/cjs/auth-api/types.d.ts +34 -0
  22. package/dist/cjs/auth-api/user-id.cjs +47 -0
  23. package/dist/cjs/auth-api/user-id.d.ts +5 -0
  24. package/dist/cjs/auth-cookie-options.cjs +66 -0
  25. package/dist/cjs/auth-cookie-options.d.ts +13 -0
  26. package/dist/cjs/base/client-info.cjs +285 -0
  27. package/dist/cjs/base/client-info.d.ts +27 -0
  28. package/dist/cjs/base/error-utils.cjs +50 -0
  29. package/dist/cjs/base/error-utils.d.ts +16 -0
  30. package/dist/cjs/base/request-utils.cjs +27 -0
  31. package/dist/cjs/base/request-utils.d.ts +8 -0
  32. package/dist/cjs/index.cjs +51 -0
  33. package/dist/cjs/index.d.ts +34 -0
  34. package/dist/cjs/limiter/auth-rate-limiter.cjs +35 -0
  35. package/dist/cjs/limiter/auth-rate-limiter.d.ts +12 -0
  36. package/dist/cjs/limiter/fixed-window.cjs +41 -0
  37. package/dist/cjs/limiter/fixed-window.d.ts +11 -0
  38. package/dist/cjs/oauth/base.cjs +7 -0
  39. package/dist/cjs/oauth/base.d.ts +17 -0
  40. package/dist/cjs/oauth/memory.cjs +135 -0
  41. package/dist/cjs/oauth/memory.d.ts +22 -0
  42. package/dist/cjs/oauth/models.cjs +47 -0
  43. package/dist/cjs/oauth/models.d.ts +50 -0
  44. package/dist/cjs/oauth/sequelize.cjs +159 -0
  45. package/dist/cjs/oauth/sequelize.d.ts +30 -0
  46. package/dist/cjs/oauth/types.cjs +3 -0
  47. package/dist/cjs/oauth/types.d.ts +51 -0
  48. package/dist/cjs/passkey/base.cjs +7 -0
  49. package/dist/cjs/passkey/base.d.ts +28 -0
  50. package/dist/cjs/passkey/config.cjs +26 -0
  51. package/dist/cjs/passkey/config.d.ts +2 -0
  52. package/dist/cjs/passkey/memory.cjs +123 -0
  53. package/dist/cjs/passkey/memory.d.ts +34 -0
  54. package/dist/cjs/passkey/models.cjs +142 -0
  55. package/dist/cjs/passkey/models.d.ts +34 -0
  56. package/dist/cjs/passkey/sequelize.cjs +126 -0
  57. package/dist/cjs/passkey/sequelize.d.ts +42 -0
  58. package/dist/cjs/passkey/service.cjs +413 -0
  59. package/dist/cjs/passkey/service.d.ts +21 -0
  60. package/dist/cjs/passkey/types.cjs +2 -0
  61. package/dist/cjs/passkey/types.d.ts +84 -0
  62. package/dist/cjs/sequelize-utils.cjs +56 -0
  63. package/dist/cjs/sequelize-utils.d.ts +8 -0
  64. package/dist/cjs/token/base.cjs +120 -0
  65. package/dist/cjs/token/base.d.ts +46 -0
  66. package/dist/cjs/token/memory.cjs +234 -0
  67. package/dist/cjs/token/memory.d.ts +29 -0
  68. package/dist/cjs/token/sequelize.cjs +400 -0
  69. package/dist/cjs/token/sequelize.d.ts +58 -0
  70. package/dist/cjs/token/types.cjs +2 -0
  71. package/dist/cjs/token/types.d.ts +34 -0
  72. package/dist/cjs/upload/memory.cjs +92 -0
  73. package/dist/cjs/upload/memory.d.ts +17 -0
  74. package/dist/cjs/upload/tus-module.cjs +270 -0
  75. package/dist/cjs/upload/tus-module.d.ts +38 -0
  76. package/dist/cjs/upload/types.cjs +2 -0
  77. package/dist/cjs/upload/types.d.ts +28 -0
  78. package/dist/cjs/user/base.cjs +53 -0
  79. package/dist/cjs/user/base.d.ts +36 -0
  80. package/dist/cjs/user/memory.cjs +194 -0
  81. package/dist/cjs/user/memory.d.ts +37 -0
  82. package/dist/cjs/user/sequelize.cjs +194 -0
  83. package/dist/cjs/user/sequelize.d.ts +46 -0
  84. package/dist/cjs/user/types.cjs +2 -0
  85. package/dist/cjs/user/types.d.ts +11 -0
  86. package/dist/esm/api-module.d.ts +45 -0
  87. package/dist/esm/api-module.js +30 -0
  88. package/dist/esm/apicore-server.d.ts +288 -0
  89. package/dist/esm/apicore-server.js +1552 -0
  90. package/dist/esm/auth-api/auth-module.d.ts +116 -0
  91. package/dist/esm/auth-api/auth-module.js +1246 -0
  92. package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
  93. package/dist/esm/auth-api/compat-auth-storage.js +124 -0
  94. package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
  95. package/dist/esm/auth-api/mem-auth-store.js +117 -0
  96. package/dist/esm/auth-api/module.d.ts +20 -0
  97. package/dist/esm/auth-api/module.js +21 -0
  98. package/dist/esm/auth-api/schemas.d.ts +21 -0
  99. package/dist/esm/auth-api/schemas.js +168 -0
  100. package/dist/esm/auth-api/sql-auth-store.d.ts +87 -0
  101. package/dist/esm/auth-api/sql-auth-store.js +175 -0
  102. package/dist/esm/auth-api/storage.d.ts +38 -0
  103. package/dist/esm/auth-api/storage.js +98 -0
  104. package/dist/esm/auth-api/types.d.ts +34 -0
  105. package/dist/esm/auth-api/types.js +1 -0
  106. package/dist/esm/auth-api/user-id.d.ts +5 -0
  107. package/dist/esm/auth-api/user-id.js +41 -0
  108. package/dist/esm/auth-cookie-options.d.ts +13 -0
  109. package/dist/esm/auth-cookie-options.js +63 -0
  110. package/dist/esm/base/client-info.d.ts +27 -0
  111. package/dist/esm/base/client-info.js +282 -0
  112. package/dist/esm/base/error-utils.d.ts +16 -0
  113. package/dist/esm/base/error-utils.js +44 -0
  114. package/dist/esm/base/request-utils.d.ts +8 -0
  115. package/dist/esm/base/request-utils.js +23 -0
  116. package/dist/esm/index.d.ts +34 -0
  117. package/dist/esm/index.js +21 -0
  118. package/dist/esm/limiter/auth-rate-limiter.d.ts +12 -0
  119. package/dist/esm/limiter/auth-rate-limiter.js +32 -0
  120. package/dist/esm/limiter/fixed-window.d.ts +11 -0
  121. package/dist/esm/limiter/fixed-window.js +37 -0
  122. package/dist/esm/oauth/base.d.ts +17 -0
  123. package/dist/esm/oauth/base.js +3 -0
  124. package/dist/esm/oauth/memory.d.ts +22 -0
  125. package/dist/esm/oauth/memory.js +128 -0
  126. package/dist/esm/oauth/models.d.ts +50 -0
  127. package/dist/esm/oauth/models.js +38 -0
  128. package/dist/esm/oauth/sequelize.d.ts +30 -0
  129. package/dist/esm/oauth/sequelize.js +148 -0
  130. package/dist/esm/oauth/types.d.ts +51 -0
  131. package/dist/esm/oauth/types.js +2 -0
  132. package/dist/esm/passkey/base.d.ts +28 -0
  133. package/dist/esm/passkey/base.js +3 -0
  134. package/dist/esm/passkey/config.d.ts +2 -0
  135. package/dist/esm/passkey/config.js +23 -0
  136. package/dist/esm/passkey/memory.d.ts +34 -0
  137. package/dist/esm/passkey/memory.js +119 -0
  138. package/dist/esm/passkey/models.d.ts +34 -0
  139. package/dist/esm/passkey/models.js +135 -0
  140. package/dist/esm/passkey/sequelize.d.ts +42 -0
  141. package/dist/esm/passkey/sequelize.js +122 -0
  142. package/dist/esm/passkey/service.d.ts +21 -0
  143. package/dist/esm/passkey/service.js +376 -0
  144. package/dist/esm/passkey/types.d.ts +84 -0
  145. package/dist/esm/passkey/types.js +1 -0
  146. package/dist/esm/sequelize-utils.d.ts +8 -0
  147. package/dist/esm/sequelize-utils.js +47 -0
  148. package/dist/esm/token/base.d.ts +46 -0
  149. package/dist/esm/token/base.js +113 -0
  150. package/dist/esm/token/memory.d.ts +29 -0
  151. package/dist/esm/token/memory.js +230 -0
  152. package/dist/esm/token/sequelize.d.ts +58 -0
  153. package/dist/esm/token/sequelize.js +396 -0
  154. package/dist/esm/token/types.d.ts +34 -0
  155. package/dist/esm/token/types.js +1 -0
  156. package/dist/esm/upload/memory.d.ts +17 -0
  157. package/dist/esm/upload/memory.js +86 -0
  158. package/dist/esm/upload/tus-module.d.ts +38 -0
  159. package/dist/esm/upload/tus-module.js +266 -0
  160. package/dist/esm/upload/types.d.ts +28 -0
  161. package/dist/esm/upload/types.js +1 -0
  162. package/dist/esm/user/base.d.ts +36 -0
  163. package/dist/esm/user/base.js +46 -0
  164. package/dist/esm/user/memory.d.ts +37 -0
  165. package/dist/esm/user/memory.js +190 -0
  166. package/dist/esm/user/sequelize.d.ts +46 -0
  167. package/dist/esm/user/sequelize.js +188 -0
  168. package/dist/esm/user/types.d.ts +11 -0
  169. package/dist/esm/user/types.js +1 -0
  170. package/docs/swagger/openapi.json +2162 -0
  171. package/package.json +131 -0
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TusUploadModule = void 0;
4
+ const apicore_server_js_1 = require("../apicore-server.cjs");
5
+ const memory_js_1 = require("./memory.cjs");
6
+ const TUS_VERSION = '1.0.0';
7
+ function parseNonNegativeInt(raw, headerName) {
8
+ const value = Array.isArray(raw) ? raw[0] : raw;
9
+ const parsed = Number.parseInt(String(value ?? ''), 10);
10
+ if (!Number.isFinite(parsed) || parsed < 0) {
11
+ throw new apicore_server_js_1.ApiError({ code: 400, message: `Missing or invalid ${headerName} header` });
12
+ }
13
+ return parsed;
14
+ }
15
+ function parseUploadLength(raw) {
16
+ return parseNonNegativeInt(raw, 'Upload-Length');
17
+ }
18
+ function parseOffset(raw) {
19
+ return parseNonNegativeInt(raw, 'Upload-Offset');
20
+ }
21
+ function decodeMetadata(raw) {
22
+ const source = Array.isArray(raw) ? raw[0] : raw;
23
+ const input = String(source ?? '').trim();
24
+ if (!input) {
25
+ return {};
26
+ }
27
+ const out = {};
28
+ for (const pair of input.split(',')) {
29
+ const trimmed = pair.trim();
30
+ if (!trimmed) {
31
+ continue;
32
+ }
33
+ const [key, encoded = ''] = trimmed.split(' ');
34
+ if (!key) {
35
+ continue;
36
+ }
37
+ try {
38
+ out[key] = Buffer.from(encoded, 'base64').toString('utf8');
39
+ }
40
+ catch {
41
+ out[key] = '';
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ async function readChunk(request, maxBytes) {
47
+ if (Buffer.isBuffer(request.body)) {
48
+ return request.body;
49
+ }
50
+ if (typeof request.body === 'string') {
51
+ return Buffer.from(request.body);
52
+ }
53
+ // Fastify only sets request.body when the Content-Type parser fires.
54
+ // Fall back to reading the raw stream, but enforce the same size cap to
55
+ // prevent bodyLimit from being bypassed by omitting the Content-Type header.
56
+ const parts = [];
57
+ let total = 0;
58
+ for await (const chunk of request.raw) {
59
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
60
+ total += buf.length;
61
+ if (total > maxBytes) {
62
+ throw new apicore_server_js_1.ApiError({ code: 413, message: 'Upload chunk exceeds maximum chunk size' });
63
+ }
64
+ parts.push(buf);
65
+ }
66
+ return Buffer.concat(parts);
67
+ }
68
+ function setTusHeaders(reply, supportsTermination = false) {
69
+ reply.header('Tus-Resumable', TUS_VERSION);
70
+ reply.header('Tus-Version', TUS_VERSION);
71
+ reply.header('Tus-Extension', supportsTermination ? 'creation,termination' : 'creation');
72
+ }
73
+ function toLocation(basePath, uploadId) {
74
+ const normalized = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
75
+ return `${normalized}/${uploadId}`;
76
+ }
77
+ class TusUploadModule extends apicore_server_js_1.ApiModule {
78
+ constructor(options = {}) {
79
+ super({ namespace: '' });
80
+ const rawPath = options.basePath?.trim() || '/api/v1/upload';
81
+ this.basePath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
82
+ this.chunkMaxBytes = options.chunkMaxBytes ?? 64 * 1024 * 1024;
83
+ this.uploadMaxBytes = options.uploadMaxBytes ?? 10 * 1024 * 1024 * 1024; // 10 GiB
84
+ this.store = options.store ?? new memory_js_1.MemoryTusUploadStore();
85
+ this.auth = options.auth;
86
+ this.onUploadComplete = options.onUploadComplete;
87
+ }
88
+ onMount() {
89
+ this.installTusRoutes();
90
+ }
91
+ /**
92
+ * Authenticate the request if an auth config was provided.
93
+ * Returns false and sends a 401/403 response if auth fails, so the caller
94
+ * can bail out early with `if (!await this.checkAuth(request, reply)) return`.
95
+ */
96
+ async checkAuth(request, reply) {
97
+ if (!this.auth) {
98
+ return null;
99
+ }
100
+ try {
101
+ const apiReq = await this.server.resolveRequest(request, reply, this.auth);
102
+ return apiReq.tokenData?.uid != null ? String(apiReq.tokenData.uid) : null;
103
+ }
104
+ catch (error) {
105
+ const code = error instanceof apicore_server_js_1.ApiError ? error.code : 401;
106
+ const message = error instanceof apicore_server_js_1.ApiError ? error.message : 'Unauthorized';
107
+ reply.code(code).send({ success: false, code, message, data: null, errors: {} });
108
+ return false;
109
+ }
110
+ }
111
+ authFailed(result) {
112
+ // When auth is configured and checkAuth catches an error, it returns
113
+ // `false as unknown as string` after sending a response. We distinguish
114
+ // that from a legitimate `null` (no auth configured / no uid in token)
115
+ // by checking the exact `false` sentinel.
116
+ return result === false;
117
+ }
118
+ verifyOwnership(upload, authUserId, reply) {
119
+ if (upload.userId && authUserId && upload.userId !== authUserId) {
120
+ reply
121
+ .code(403)
122
+ .send({ success: false, code: 403, message: 'Upload belongs to another user', data: null, errors: {} });
123
+ return false;
124
+ }
125
+ return true;
126
+ }
127
+ installTusRoutes() {
128
+ const app = this.server.fastify;
129
+ const hasTermination = typeof this.store.deleteUpload === 'function';
130
+ try {
131
+ app.addContentTypeParser('application/offset+octet-stream', { parseAs: 'buffer' }, (_req, body, done) => {
132
+ done(null, body);
133
+ });
134
+ }
135
+ catch (error) {
136
+ // Parser may already be present when mounting multiple upload modules.
137
+ const message = error instanceof Error ? error.message : '';
138
+ if (!message.includes('already registered') && !message.includes('already exists')) {
139
+ throw error;
140
+ }
141
+ }
142
+ app.options(this.basePath, async (_request, reply) => {
143
+ setTusHeaders(reply, hasTermination);
144
+ reply.code(204).send();
145
+ });
146
+ app.post(this.basePath, async (request, reply) => {
147
+ const authUserId = await this.checkAuth(request, reply);
148
+ if (this.authFailed(authUserId))
149
+ return;
150
+ setTusHeaders(reply, hasTermination);
151
+ const length = parseUploadLength(request.headers['upload-length']);
152
+ if (length <= 0) {
153
+ reply.code(400).send({
154
+ success: false,
155
+ code: 400,
156
+ message: 'Upload-Length must be greater than zero',
157
+ data: null,
158
+ errors: {}
159
+ });
160
+ return;
161
+ }
162
+ if (length > this.uploadMaxBytes) {
163
+ reply.code(413).send({
164
+ success: false,
165
+ code: 413,
166
+ message: `Upload-Length exceeds the maximum allowed size of ${this.uploadMaxBytes} bytes`,
167
+ data: null,
168
+ errors: {}
169
+ });
170
+ return;
171
+ }
172
+ const metadata = decodeMetadata(request.headers['upload-metadata']);
173
+ const created = await this.store.createUpload({ length, metadata, userId: authUserId ?? undefined });
174
+ reply.header('Location', toLocation(this.basePath, created.id));
175
+ reply.header('Upload-Offset', String(created.offset));
176
+ reply.code(201).send();
177
+ });
178
+ app.head(`${this.basePath}/:uploadId`, async (request, reply) => {
179
+ const authUserId = await this.checkAuth(request, reply);
180
+ if (this.authFailed(authUserId))
181
+ return;
182
+ setTusHeaders(reply, hasTermination);
183
+ const uploadId = String(request.params.uploadId ?? '');
184
+ const upload = await this.store.getUpload(uploadId);
185
+ if (!upload) {
186
+ reply.code(404).send();
187
+ return;
188
+ }
189
+ if (!this.verifyOwnership(upload, authUserId, reply))
190
+ return;
191
+ reply.header('Upload-Offset', String(upload.offset));
192
+ reply.header('Upload-Length', String(upload.length));
193
+ reply.code(200).send();
194
+ });
195
+ app.patch(`${this.basePath}/:uploadId`, { bodyLimit: this.chunkMaxBytes }, async (request, reply) => {
196
+ const authUserId = await this.checkAuth(request, reply);
197
+ if (this.authFailed(authUserId))
198
+ return;
199
+ setTusHeaders(reply, hasTermination);
200
+ const uploadId = String(request.params.uploadId ?? '');
201
+ const upload = await this.store.getUpload(uploadId);
202
+ if (!upload) {
203
+ reply.code(404).send();
204
+ return;
205
+ }
206
+ if (!this.verifyOwnership(upload, authUserId, reply))
207
+ return;
208
+ const offset = parseOffset(request.headers['upload-offset']);
209
+ if (offset !== upload.offset) {
210
+ reply.header('Upload-Offset', String(upload.offset));
211
+ reply.code(409).send();
212
+ return;
213
+ }
214
+ const chunk = await readChunk(request, this.chunkMaxBytes);
215
+ try {
216
+ const updated = await this.store.appendUpload({ uploadId, offset, chunk });
217
+ reply.header('Upload-Offset', String(updated.offset));
218
+ if (updated.completedAt && this.onUploadComplete) {
219
+ try {
220
+ await this.onUploadComplete(updated);
221
+ }
222
+ catch (completionError) {
223
+ console.error('[TusUploadModule] onUploadComplete callback failed', completionError);
224
+ }
225
+ }
226
+ reply.code(204).send();
227
+ }
228
+ catch (error) {
229
+ if (error instanceof memory_js_1.TusUploadOffsetError) {
230
+ reply.header('Upload-Offset', String(error.currentOffset));
231
+ reply.code(409).send();
232
+ return;
233
+ }
234
+ if (error instanceof memory_js_1.TusUploadExceedsLengthError) {
235
+ reply.code(413).send({
236
+ success: false,
237
+ code: 413,
238
+ message: 'Upload chunk exceeds declared upload length',
239
+ data: null,
240
+ errors: {}
241
+ });
242
+ return;
243
+ }
244
+ throw error;
245
+ }
246
+ });
247
+ app.delete(`${this.basePath}/:uploadId`, async (request, reply) => {
248
+ const authUserId = await this.checkAuth(request, reply);
249
+ if (this.authFailed(authUserId))
250
+ return;
251
+ setTusHeaders(reply, hasTermination);
252
+ const uploadId = String(request.params.uploadId ?? '');
253
+ if (!this.store.deleteUpload) {
254
+ reply.header('Allow', 'OPTIONS, POST, HEAD, PATCH');
255
+ reply.code(405).send();
256
+ return;
257
+ }
258
+ const upload = await this.store.getUpload(uploadId);
259
+ if (!upload) {
260
+ reply.code(404).send();
261
+ return;
262
+ }
263
+ if (!this.verifyOwnership(upload, authUserId, reply))
264
+ return;
265
+ const deleted = await this.store.deleteUpload(uploadId);
266
+ reply.code(deleted ? 204 : 404).send();
267
+ });
268
+ }
269
+ }
270
+ exports.TusUploadModule = TusUploadModule;
@@ -0,0 +1,38 @@
1
+ import { ApiModule, type ApiAuthClass, type ApiAuthType, type ApiServer } from '../apicore-server.js';
2
+ import type { TusUploadRecord, TusUploadStore } from './types.js';
3
+ export interface TusUploadModuleOptions {
4
+ basePath?: string;
5
+ store?: TusUploadStore;
6
+ chunkMaxBytes?: number;
7
+ /** Maximum allowed value for Upload-Length. Defaults to 10 GiB. */
8
+ uploadMaxBytes?: number;
9
+ /**
10
+ * Authentication requirement for all TUS routes.
11
+ * When omitted, routes are public (no authentication enforced).
12
+ * Use `{ type: 'yes', req: 'any' }` to require a valid session.
13
+ */
14
+ auth?: {
15
+ type: ApiAuthType;
16
+ req?: ApiAuthClass;
17
+ };
18
+ onUploadComplete?: (upload: TusUploadRecord) => Promise<void> | void;
19
+ }
20
+ export declare class TusUploadModule extends ApiModule<ApiServer> {
21
+ private readonly basePath;
22
+ private readonly chunkMaxBytes;
23
+ private readonly uploadMaxBytes;
24
+ private readonly store;
25
+ private readonly auth?;
26
+ private readonly onUploadComplete?;
27
+ constructor(options?: TusUploadModuleOptions);
28
+ onMount(): void;
29
+ /**
30
+ * Authenticate the request if an auth config was provided.
31
+ * Returns false and sends a 401/403 response if auth fails, so the caller
32
+ * can bail out early with `if (!await this.checkAuth(request, reply)) return`.
33
+ */
34
+ private checkAuth;
35
+ private authFailed;
36
+ private verifyOwnership;
37
+ private installTusRoutes;
38
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,28 @@
1
+ export type TusMetadata = Record<string, string>;
2
+ export interface TusUploadRecord {
3
+ id: string;
4
+ length: number;
5
+ offset: number;
6
+ metadata: TusMetadata;
7
+ userId?: string;
8
+ createdAt: Date;
9
+ updatedAt: Date;
10
+ completedAt?: Date;
11
+ }
12
+ export interface TusCreateUploadInput {
13
+ id?: string;
14
+ length: number;
15
+ metadata: TusMetadata;
16
+ userId?: string;
17
+ }
18
+ export interface TusAppendInput {
19
+ uploadId: string;
20
+ offset: number;
21
+ chunk: Buffer;
22
+ }
23
+ export interface TusUploadStore {
24
+ createUpload(input: TusCreateUploadInput): Promise<TusUploadRecord>;
25
+ getUpload(uploadId: string): Promise<TusUploadRecord | null>;
26
+ appendUpload(input: TusAppendInput): Promise<TusUploadRecord>;
27
+ deleteUpload?(uploadId: string): Promise<boolean>;
28
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.UserStore = void 0;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const bcryptjs_1 = __importDefault(require("bcryptjs"));
9
+ /** Base contract for user persistence backends. */
10
+ class UserStore {
11
+ constructor(opts = {}) {
12
+ this.toPublicUser = opts.toPublic ?? ((u) => u);
13
+ const rounds = typeof opts.bcryptRounds === 'number' && Number.isFinite(opts.bcryptRounds)
14
+ ? Math.max(4, Math.floor(opts.bcryptRounds))
15
+ : 12;
16
+ this.bcryptRounds = rounds;
17
+ this.bcryptPepper =
18
+ typeof opts.bcryptPepper === 'string' && opts.bcryptPepper.length > 0 ? opts.bcryptPepper : undefined;
19
+ }
20
+ applyPepper(plain) {
21
+ if (!this.bcryptPepper) {
22
+ return plain;
23
+ }
24
+ return (0, node_crypto_1.createHmac)('sha256', this.bcryptPepper).update(plain).digest('hex');
25
+ }
26
+ async hashPassword(plain) {
27
+ const candidate = this.applyPepper(plain);
28
+ return bcryptjs_1.default.hash(candidate, this.bcryptRounds);
29
+ }
30
+ async verifyPassword(plain, hashed) {
31
+ const candidate = this.applyPepper(plain);
32
+ return bcryptjs_1.default.compare(candidate, hashed);
33
+ }
34
+ normalizeUserInput(input) {
35
+ const login = typeof input.login === 'string' ? input.login.trim() : '';
36
+ const email = typeof input.email === 'string' ? input.email.trim() : '';
37
+ if (!login || !email) {
38
+ throw new Error('login and email are required');
39
+ }
40
+ const password = typeof input.password === 'string' ? input.password : undefined;
41
+ return { ...input, login, email, password };
42
+ }
43
+ toPublic(user) {
44
+ const mapped = this.toPublicUser(user);
45
+ if (mapped && typeof mapped === 'object') {
46
+ const rest = { ...mapped };
47
+ delete rest.password;
48
+ return rest;
49
+ }
50
+ return mapped;
51
+ }
52
+ }
53
+ exports.UserStore = UserStore;
@@ -0,0 +1,36 @@
1
+ import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
2
+ import type { AuthIdentifier } from '../auth-api/types.js';
3
+ /** Base contract for user persistence backends. */
4
+ export declare abstract class UserStore<User, PublicUser> {
5
+ protected readonly toPublicUser: PublicUserMapper<User, PublicUser>;
6
+ private readonly bcryptRounds;
7
+ private readonly bcryptPepper?;
8
+ constructor(opts?: {
9
+ toPublic?: PublicUserMapper<User, PublicUser>;
10
+ bcryptRounds?: number;
11
+ bcryptPepper?: string;
12
+ });
13
+ private applyPepper;
14
+ protected hashPassword(plain: string): Promise<string>;
15
+ verifyPassword(plain: string, hashed: string): Promise<boolean>;
16
+ protected normalizeUserInput(input: Partial<CreateUserInput>): CreateUserInput;
17
+ /** Find a user by id/login/email identifier. */
18
+ abstract findUser(identifier: AuthIdentifier | string): Promise<User | null>;
19
+ /** Find a user by primary id only. */
20
+ abstract findById(id: AuthIdentifier): Promise<User | null>;
21
+ /** Find a user by login or email value. */
22
+ abstract findByLoginOrEmail(loginOrEmail: string): Promise<User | null>;
23
+ /** Create a new user record. */
24
+ abstract createUser(input: CreateUserInput): Promise<User>;
25
+ abstract upsertUser(input: CreateUserInput): Promise<User>;
26
+ /** Update selected user fields. */
27
+ abstract updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<User>;
28
+ /** Persist a password hash for the given user id. */
29
+ abstract setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
30
+ /** Extract the password hash from a user record. */
31
+ abstract getPasswordHash(user: User): string | null;
32
+ /** Extract the stable user identifier from a user record. */
33
+ abstract getUserId(user: User): AuthIdentifier;
34
+ toPublic(user: User): PublicUser;
35
+ }
36
+ export type { CreateUserInput, UpdateUserInput, PublicUserMapper } from './types.js';
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryUserStore = void 0;
4
+ const user_id_js_1 = require("../auth-api/user-id.cjs");
5
+ const base_js_1 = require("./base.cjs");
6
+ function cloneUser(user) {
7
+ return { ...user };
8
+ }
9
+ class MemoryUserStore extends base_js_1.UserStore {
10
+ constructor(options = {}) {
11
+ super({
12
+ toPublic: options.toPublic,
13
+ bcryptRounds: options.bcryptRounds,
14
+ bcryptPepper: options.bcryptPepper
15
+ });
16
+ this.usersById = new Map();
17
+ this.loginToId = new Map();
18
+ this.emailToId = new Map();
19
+ this.nextUserId = Number.isFinite(options.startingUserId) ? Number(options.startingUserId) : 1;
20
+ this.maxUsers =
21
+ typeof options.maxUsers === 'number' && Number.isFinite(options.maxUsers) && options.maxUsers > 0
22
+ ? Math.floor(options.maxUsers)
23
+ : undefined;
24
+ this.userIdFactory =
25
+ options.userIdFactory ??
26
+ (() => {
27
+ const id = this.nextUserId;
28
+ this.nextUserId += 1;
29
+ return id;
30
+ });
31
+ }
32
+ async findUser(identifier) {
33
+ if (typeof identifier === 'number') {
34
+ const user = this.usersById.get(identifier);
35
+ return user ? cloneUser(user) : null;
36
+ }
37
+ if (typeof identifier === 'string') {
38
+ const numeric = /^\d+$/.test(identifier) ? Number(identifier) : null;
39
+ if (numeric !== null) {
40
+ const user = this.usersById.get(numeric);
41
+ if (user) {
42
+ return cloneUser(user);
43
+ }
44
+ }
45
+ const loginId = this.loginToId.get(identifier);
46
+ if (loginId !== undefined) {
47
+ const loginUser = this.usersById.get(loginId);
48
+ if (loginUser)
49
+ return cloneUser(loginUser);
50
+ }
51
+ const emailId = this.emailToId.get(identifier);
52
+ if (emailId !== undefined) {
53
+ const emailUser = this.usersById.get(emailId);
54
+ if (emailUser)
55
+ return cloneUser(emailUser);
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ async findById(id) {
61
+ try {
62
+ const numeric = (0, user_id_js_1.normalizeNumericUserId)(id);
63
+ const user = this.usersById.get(numeric);
64
+ return user ? cloneUser(user) : null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ async findByLoginOrEmail(loginOrEmail) {
71
+ const loginId = this.loginToId.get(loginOrEmail);
72
+ if (loginId !== undefined) {
73
+ const loginUser = this.usersById.get(loginId);
74
+ if (loginUser)
75
+ return cloneUser(loginUser);
76
+ }
77
+ const emailId = this.emailToId.get(loginOrEmail);
78
+ if (emailId !== undefined) {
79
+ const emailUser = this.usersById.get(emailId);
80
+ if (emailUser)
81
+ return cloneUser(emailUser);
82
+ }
83
+ return null;
84
+ }
85
+ async createUser(input) {
86
+ const normalizedInput = this.normalizeUserInput(input);
87
+ const providedId = input.user_id;
88
+ const userId = typeof providedId === 'number' && Number.isFinite(providedId) ? providedId : this.userIdFactory();
89
+ if (this.usersById.has(userId)) {
90
+ throw new Error(`User ${userId} already exists`);
91
+ }
92
+ if (this.maxUsers !== undefined && this.usersById.size >= this.maxUsers) {
93
+ throw new Error('MemoryUserStore maxUsers limit reached');
94
+ }
95
+ if (this.loginToId.has(normalizedInput.login)) {
96
+ throw new Error(`User with login ${normalizedInput.login} already exists`);
97
+ }
98
+ if (this.emailToId.has(normalizedInput.email)) {
99
+ throw new Error(`User with email ${normalizedInput.email} already exists`);
100
+ }
101
+ const passwordHash = normalizedInput.password ? await this.hashPassword(normalizedInput.password) : '';
102
+ const record = {
103
+ ...normalizedInput,
104
+ user_id: userId,
105
+ password: passwordHash
106
+ };
107
+ this.persistUser(record);
108
+ if (typeof providedId === 'number' && Number.isFinite(providedId)) {
109
+ this.nextUserId = Math.max(this.nextUserId, providedId + 1);
110
+ }
111
+ return cloneUser(record);
112
+ }
113
+ async upsertUser(input) {
114
+ const normalizedInput = this.normalizeUserInput(input);
115
+ const providedId = input.user_id;
116
+ if (providedId !== undefined) {
117
+ const existing = this.usersById.get(providedId);
118
+ if (!existing) {
119
+ throw new Error(`User ${providedId} not found`);
120
+ }
121
+ const updates = {
122
+ ...existing,
123
+ ...normalizedInput
124
+ };
125
+ if (updates.login !== existing.login) {
126
+ const loginOwner = this.loginToId.get(updates.login);
127
+ if (loginOwner !== undefined && loginOwner !== existing.user_id) {
128
+ throw new Error(`User with login ${updates.login} already exists`);
129
+ }
130
+ }
131
+ if (updates.email !== existing.email) {
132
+ const emailOwner = this.emailToId.get(updates.email);
133
+ if (emailOwner !== undefined && emailOwner !== existing.user_id) {
134
+ throw new Error(`User with email ${updates.email} already exists`);
135
+ }
136
+ }
137
+ if (normalizedInput.password && normalizedInput.password.length > 0) {
138
+ updates.password = await this.hashPassword(normalizedInput.password);
139
+ }
140
+ else {
141
+ updates.password = existing.password;
142
+ }
143
+ this.persistUser(updates);
144
+ return cloneUser(updates);
145
+ }
146
+ return this.createUser(input);
147
+ }
148
+ async updateUser(id, patch) {
149
+ const user = await this.findById(id);
150
+ if (!user) {
151
+ throw new Error(`User ${String(id)} not found`);
152
+ }
153
+ const updates = { ...user, ...patch };
154
+ if (patch.password && patch.password.length > 0) {
155
+ updates.password = await this.hashPassword(patch.password);
156
+ }
157
+ else {
158
+ updates.password = user.password;
159
+ }
160
+ this.persistUser(updates);
161
+ return cloneUser(updates);
162
+ }
163
+ async setPasswordHash(id, hash) {
164
+ const user = await this.findById(id);
165
+ if (!user) {
166
+ throw new Error(`User ${String(id)} not found`);
167
+ }
168
+ const updates = { ...user, password: hash };
169
+ this.persistUser(updates);
170
+ }
171
+ getPasswordHash(user) {
172
+ return user.password;
173
+ }
174
+ getUserId(user) {
175
+ return user.user_id;
176
+ }
177
+ persistUser(user) {
178
+ const snapshot = { ...user };
179
+ this.usersById.set(user.user_id, snapshot);
180
+ for (const [login, id] of [...this.loginToId.entries()]) {
181
+ if (id === user.user_id && login !== user.login) {
182
+ this.loginToId.delete(login);
183
+ }
184
+ }
185
+ for (const [email, id] of [...this.emailToId.entries()]) {
186
+ if (id === user.user_id && email !== user.email) {
187
+ this.emailToId.delete(email);
188
+ }
189
+ }
190
+ this.loginToId.set(user.login, user.user_id);
191
+ this.emailToId.set(user.email, user.user_id);
192
+ }
193
+ }
194
+ exports.MemoryUserStore = MemoryUserStore;
@@ -0,0 +1,37 @@
1
+ import { UserStore } from './base.js';
2
+ import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
3
+ import type { AuthIdentifier } from '../auth-api/types.js';
4
+ export interface MemoryUserAttributes extends Record<string, unknown> {
5
+ user_id: number;
6
+ login: string;
7
+ email: string;
8
+ password: string;
9
+ }
10
+ export type MemoryPublicUser<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes> = Omit<UserAttributes, 'password'>;
11
+ export interface MemoryUserStoreOptions<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> {
12
+ bcryptRounds?: number;
13
+ bcryptPepper?: string;
14
+ toPublic?: PublicUserMapper<UserAttributes, PublicUserShape>;
15
+ userIdFactory?: () => number;
16
+ startingUserId?: number;
17
+ maxUsers?: number;
18
+ }
19
+ export declare class MemoryUserStore<UserAttributes extends MemoryUserAttributes = MemoryUserAttributes, PublicUserShape extends Omit<UserAttributes, 'password'> = Omit<UserAttributes, 'password'>> extends UserStore<UserAttributes, PublicUserShape> {
20
+ private readonly usersById;
21
+ private readonly loginToId;
22
+ private readonly emailToId;
23
+ private readonly userIdFactory;
24
+ private readonly maxUsers?;
25
+ private nextUserId;
26
+ constructor(options?: MemoryUserStoreOptions<UserAttributes, PublicUserShape>);
27
+ findUser(identifier: AuthIdentifier | string): Promise<UserAttributes | null>;
28
+ findById(id: AuthIdentifier): Promise<UserAttributes | null>;
29
+ findByLoginOrEmail(loginOrEmail: string): Promise<UserAttributes | null>;
30
+ createUser(input: CreateUserInput): Promise<UserAttributes>;
31
+ upsertUser(input: CreateUserInput): Promise<UserAttributes>;
32
+ updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<UserAttributes>;
33
+ setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
34
+ getPasswordHash(user: UserAttributes): string | null;
35
+ getUserId(user: UserAttributes): AuthIdentifier;
36
+ private persistUser;
37
+ }