@technomoron/api-server-base 2.0.0-beta.21 → 2.0.0-beta.23

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 (177) hide show
  1. package/dist/cjs/common/types.cjs +10 -0
  2. package/dist/cjs/common/types.d.ts +137 -0
  3. package/dist/cjs/{api-module.cjs → server/src/api-module.cjs} +8 -0
  4. package/dist/{esm → cjs/server/src}/api-module.d.ts +15 -0
  5. package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +669 -627
  6. package/dist/{esm → cjs/server/src}/api-server-base.d.ts +105 -78
  7. package/dist/cjs/{auth-api/auth-module.js → server/src/auth-api/auth-module.cjs} +96 -76
  8. package/dist/cjs/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
  9. package/dist/cjs/{auth-api/compat-auth-storage.js → server/src/auth-api/compat-auth-storage.cjs} +4 -4
  10. package/dist/cjs/{auth-api/mem-auth-store.js → server/src/auth-api/mem-auth-store.cjs} +7 -7
  11. package/dist/cjs/{auth-api/module.js → server/src/auth-api/module.cjs} +1 -1
  12. package/dist/cjs/server/src/auth-api/schemas.cjs +171 -0
  13. package/dist/cjs/server/src/auth-api/schemas.d.ts +21 -0
  14. package/dist/cjs/{auth-api/sql-auth-store.js → server/src/auth-api/sql-auth-store.cjs} +8 -8
  15. package/dist/cjs/{auth-api/user-id.js → server/src/auth-api/user-id.cjs} +12 -3
  16. package/dist/{esm → cjs/server/src}/auth-cookie-options.d.ts +5 -3
  17. package/dist/cjs/server/src/base/client-info.cjs +285 -0
  18. package/dist/cjs/server/src/base/client-info.d.ts +27 -0
  19. package/dist/cjs/server/src/base/error-utils.cjs +50 -0
  20. package/dist/cjs/server/src/base/error-utils.d.ts +16 -0
  21. package/dist/cjs/server/src/base/request-utils.cjs +27 -0
  22. package/dist/cjs/server/src/base/request-utils.d.ts +8 -0
  23. package/dist/cjs/{index.cjs → server/src/index.cjs} +24 -15
  24. package/dist/{esm → cjs/server/src}/index.d.ts +7 -0
  25. package/dist/cjs/server/src/limiter/auth-rate-limiter.cjs +35 -0
  26. package/dist/cjs/server/src/limiter/auth-rate-limiter.d.ts +12 -0
  27. package/dist/cjs/server/src/limiter/fixed-window.cjs +41 -0
  28. package/dist/cjs/server/src/limiter/fixed-window.d.ts +11 -0
  29. package/dist/cjs/{oauth/base.js → server/src/oauth/base.cjs} +1 -0
  30. package/dist/cjs/{oauth → server/src/oauth}/base.d.ts +8 -1
  31. package/dist/cjs/{oauth/memory.js → server/src/oauth/memory.cjs} +7 -4
  32. package/dist/{esm → cjs/server/src}/oauth/memory.d.ts +1 -1
  33. package/dist/cjs/{oauth/models.js → server/src/oauth/models.cjs} +2 -2
  34. package/dist/cjs/{oauth/sequelize.js → server/src/oauth/sequelize.cjs} +11 -7
  35. package/dist/{esm → cjs/server/src}/oauth/sequelize.d.ts +1 -1
  36. package/dist/cjs/{passkey/base.js → server/src/passkey/base.cjs} +1 -0
  37. package/dist/{esm → cjs/server/src}/passkey/base.d.ts +11 -0
  38. package/dist/cjs/{passkey/memory.js → server/src/passkey/memory.cjs} +2 -2
  39. package/dist/cjs/{passkey/models.js → server/src/passkey/models.cjs} +1 -1
  40. package/dist/cjs/{passkey/sequelize.js → server/src/passkey/sequelize.cjs} +3 -3
  41. package/dist/cjs/{passkey/service.js → server/src/passkey/service.cjs} +17 -3
  42. package/dist/{esm → cjs/server/src}/passkey/service.d.ts +1 -1
  43. package/dist/cjs/{sequelize-utils.js → server/src/sequelize-utils.cjs} +4 -5
  44. package/dist/cjs/{token/base.js → server/src/token/base.cjs} +4 -0
  45. package/dist/{esm → cjs/server/src}/token/base.d.ts +7 -0
  46. package/dist/cjs/{token/memory.js → server/src/token/memory.cjs} +15 -20
  47. package/dist/cjs/{token/sequelize.js → server/src/token/sequelize.cjs} +25 -11
  48. package/dist/cjs/server/src/upload/memory.cjs +92 -0
  49. package/dist/cjs/server/src/upload/memory.d.ts +17 -0
  50. package/dist/cjs/server/src/upload/tus-module.cjs +270 -0
  51. package/dist/cjs/server/src/upload/tus-module.d.ts +38 -0
  52. package/dist/cjs/server/src/upload/types.d.ts +8 -0
  53. package/dist/cjs/{user/base.js → server/src/user/base.cjs} +1 -0
  54. package/dist/cjs/{user → server/src/user}/base.d.ts +9 -0
  55. package/dist/cjs/{user/memory.js → server/src/user/memory.cjs} +29 -7
  56. package/dist/cjs/{user/sequelize.js → server/src/user/sequelize.cjs} +33 -8
  57. package/dist/cjs/server/src/user/types.cjs +2 -0
  58. package/dist/esm/common/types.d.ts +137 -0
  59. package/dist/esm/common/types.js +9 -0
  60. package/dist/{cjs → esm/server/src}/api-module.d.ts +15 -0
  61. package/dist/esm/{api-module.js → server/src/api-module.js} +8 -0
  62. package/dist/{cjs → esm/server/src}/api-server-base.d.ts +105 -78
  63. package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +658 -616
  64. package/dist/esm/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
  65. package/dist/esm/{auth-api → server/src/auth-api}/auth-module.js +92 -72
  66. package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.js +3 -3
  67. package/dist/esm/server/src/auth-api/schemas.d.ts +21 -0
  68. package/dist/esm/server/src/auth-api/schemas.js +168 -0
  69. package/dist/esm/{auth-api → server/src/auth-api}/user-id.js +12 -3
  70. package/dist/{cjs → esm/server/src}/auth-cookie-options.d.ts +5 -3
  71. package/dist/esm/server/src/base/client-info.d.ts +27 -0
  72. package/dist/esm/server/src/base/client-info.js +282 -0
  73. package/dist/esm/server/src/base/error-utils.d.ts +16 -0
  74. package/dist/esm/server/src/base/error-utils.js +44 -0
  75. package/dist/esm/server/src/base/request-utils.d.ts +8 -0
  76. package/dist/esm/server/src/base/request-utils.js +23 -0
  77. package/dist/{cjs → esm/server/src}/index.d.ts +7 -0
  78. package/dist/esm/{index.js → server/src/index.js} +4 -0
  79. package/dist/esm/server/src/limiter/auth-rate-limiter.d.ts +12 -0
  80. package/dist/esm/server/src/limiter/auth-rate-limiter.js +32 -0
  81. package/dist/esm/server/src/limiter/fixed-window.d.ts +11 -0
  82. package/dist/esm/server/src/limiter/fixed-window.js +37 -0
  83. package/dist/esm/{oauth → server/src/oauth}/base.d.ts +8 -1
  84. package/dist/esm/server/src/oauth/base.js +3 -0
  85. package/dist/{cjs → esm/server/src}/oauth/memory.d.ts +1 -1
  86. package/dist/esm/{oauth → server/src/oauth}/memory.js +5 -2
  87. package/dist/{cjs → esm/server/src}/oauth/sequelize.d.ts +1 -1
  88. package/dist/esm/{oauth → server/src/oauth}/sequelize.js +6 -2
  89. package/dist/{cjs → esm/server/src}/passkey/base.d.ts +11 -0
  90. package/dist/esm/server/src/passkey/base.js +3 -0
  91. package/dist/{cjs → esm/server/src}/passkey/service.d.ts +1 -1
  92. package/dist/esm/{passkey → server/src/passkey}/service.js +17 -3
  93. package/dist/esm/{sequelize-utils.js → server/src/sequelize-utils.js} +4 -5
  94. package/dist/{cjs → esm/server/src}/token/base.d.ts +7 -0
  95. package/dist/esm/{token → server/src/token}/base.js +4 -0
  96. package/dist/esm/{token → server/src/token}/memory.js +14 -19
  97. package/dist/esm/{token → server/src/token}/sequelize.js +22 -8
  98. package/dist/esm/server/src/upload/memory.d.ts +17 -0
  99. package/dist/esm/server/src/upload/memory.js +86 -0
  100. package/dist/esm/server/src/upload/tus-module.d.ts +38 -0
  101. package/dist/esm/server/src/upload/tus-module.js +266 -0
  102. package/dist/esm/server/src/upload/types.d.ts +8 -0
  103. package/dist/esm/{user → server/src/user}/base.d.ts +9 -0
  104. package/dist/esm/{user → server/src/user}/base.js +1 -0
  105. package/dist/esm/{user → server/src/user}/memory.js +27 -5
  106. package/dist/esm/{user → server/src/user}/sequelize.js +30 -5
  107. package/dist/esm/server/src/user/types.js +1 -0
  108. package/docs/swagger/openapi.json +411 -125
  109. package/package.json +129 -134
  110. package/README.txt +0 -213
  111. package/dist/esm/oauth/base.js +0 -2
  112. package/dist/esm/passkey/base.js +0 -2
  113. /package/dist/cjs/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
  114. /package/dist/cjs/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
  115. /package/dist/cjs/{auth-api → server/src/auth-api}/module.d.ts +0 -0
  116. /package/dist/cjs/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
  117. /package/dist/cjs/{auth-api/storage.js → server/src/auth-api/storage.cjs} +0 -0
  118. /package/dist/cjs/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
  119. /package/dist/cjs/{auth-api/types.js → server/src/auth-api/types.cjs} +0 -0
  120. /package/dist/cjs/{auth-api → server/src/auth-api}/types.d.ts +0 -0
  121. /package/dist/cjs/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
  122. /package/dist/cjs/{auth-cookie-options.js → server/src/auth-cookie-options.cjs} +0 -0
  123. /package/dist/cjs/{oauth → server/src/oauth}/models.d.ts +0 -0
  124. /package/dist/cjs/{oauth/types.js → server/src/oauth/types.cjs} +0 -0
  125. /package/dist/cjs/{oauth → server/src/oauth}/types.d.ts +0 -0
  126. /package/dist/cjs/{passkey/config.js → server/src/passkey/config.cjs} +0 -0
  127. /package/dist/cjs/{passkey → server/src/passkey}/config.d.ts +0 -0
  128. /package/dist/cjs/{passkey → server/src/passkey}/memory.d.ts +0 -0
  129. /package/dist/cjs/{passkey → server/src/passkey}/models.d.ts +0 -0
  130. /package/dist/cjs/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
  131. /package/dist/cjs/{passkey/types.js → server/src/passkey/types.cjs} +0 -0
  132. /package/dist/cjs/{passkey → server/src/passkey}/types.d.ts +0 -0
  133. /package/dist/cjs/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
  134. /package/dist/cjs/{token → server/src/token}/memory.d.ts +0 -0
  135. /package/dist/cjs/{token → server/src/token}/sequelize.d.ts +0 -0
  136. /package/dist/cjs/{token/types.js → server/src/token/types.cjs} +0 -0
  137. /package/dist/cjs/{token → server/src/token}/types.d.ts +0 -0
  138. /package/dist/cjs/{user/types.js → server/src/upload/types.cjs} +0 -0
  139. /package/dist/cjs/{user → server/src/user}/memory.d.ts +0 -0
  140. /package/dist/cjs/{user → server/src/user}/sequelize.d.ts +0 -0
  141. /package/dist/cjs/{user → server/src/user}/types.d.ts +0 -0
  142. /package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
  143. /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
  144. /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.js +0 -0
  145. /package/dist/esm/{auth-api → server/src/auth-api}/module.d.ts +0 -0
  146. /package/dist/esm/{auth-api → server/src/auth-api}/module.js +0 -0
  147. /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
  148. /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.js +0 -0
  149. /package/dist/esm/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
  150. /package/dist/esm/{auth-api → server/src/auth-api}/storage.js +0 -0
  151. /package/dist/esm/{auth-api → server/src/auth-api}/types.d.ts +0 -0
  152. /package/dist/esm/{auth-api → server/src/auth-api}/types.js +0 -0
  153. /package/dist/esm/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
  154. /package/dist/esm/{auth-cookie-options.js → server/src/auth-cookie-options.js} +0 -0
  155. /package/dist/esm/{oauth → server/src/oauth}/models.d.ts +0 -0
  156. /package/dist/esm/{oauth → server/src/oauth}/models.js +0 -0
  157. /package/dist/esm/{oauth → server/src/oauth}/types.d.ts +0 -0
  158. /package/dist/esm/{oauth → server/src/oauth}/types.js +0 -0
  159. /package/dist/esm/{passkey → server/src/passkey}/config.d.ts +0 -0
  160. /package/dist/esm/{passkey → server/src/passkey}/config.js +0 -0
  161. /package/dist/esm/{passkey → server/src/passkey}/memory.d.ts +0 -0
  162. /package/dist/esm/{passkey → server/src/passkey}/memory.js +0 -0
  163. /package/dist/esm/{passkey → server/src/passkey}/models.d.ts +0 -0
  164. /package/dist/esm/{passkey → server/src/passkey}/models.js +0 -0
  165. /package/dist/esm/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
  166. /package/dist/esm/{passkey → server/src/passkey}/sequelize.js +0 -0
  167. /package/dist/esm/{passkey → server/src/passkey}/types.d.ts +0 -0
  168. /package/dist/esm/{passkey → server/src/passkey}/types.js +0 -0
  169. /package/dist/esm/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
  170. /package/dist/esm/{token → server/src/token}/memory.d.ts +0 -0
  171. /package/dist/esm/{token → server/src/token}/sequelize.d.ts +0 -0
  172. /package/dist/esm/{token → server/src/token}/types.d.ts +0 -0
  173. /package/dist/esm/{token → server/src/token}/types.js +0 -0
  174. /package/dist/esm/{user → server/src/upload}/types.js +0 -0
  175. /package/dist/esm/{user → server/src/user}/memory.d.ts +0 -0
  176. /package/dist/esm/{user → server/src/user}/sequelize.d.ts +0 -0
  177. /package/dist/esm/{user → server/src/user}/types.d.ts +0 -0
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TusUploadModule = void 0;
4
+ const api_server_base_js_1 = require("../api-server-base.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 api_server_base_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 api_server_base_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 api_server_base_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 api_server_base_js_1.ApiError ? error.code : 401;
106
+ const message = error instanceof api_server_base_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 '../api-server-base.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,8 @@
1
+ import type { TusAppendInput, TusCreateUploadInput, TusUploadRecord } from '#common';
2
+ export type { TusAppendInput, TusCreateUploadInput, TusMetadata, TusUploadRecord } from '#common';
3
+ export interface TusUploadStore {
4
+ createUpload(input: TusCreateUploadInput): Promise<TusUploadRecord>;
5
+ getUpload(uploadId: string): Promise<TusUploadRecord | null>;
6
+ appendUpload(input: TusAppendInput): Promise<TusUploadRecord>;
7
+ deleteUpload?(uploadId: string): Promise<boolean>;
8
+ }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.UserStore = void 0;
7
7
  const node_crypto_1 = require("node:crypto");
8
8
  const bcryptjs_1 = __importDefault(require("bcryptjs"));
9
+ /** Base contract for user persistence backends. */
9
10
  class UserStore {
10
11
  constructor(opts = {}) {
11
12
  this.toPublicUser = opts.toPublic ?? ((u) => u);
@@ -1,5 +1,6 @@
1
1
  import type { CreateUserInput, PublicUserMapper, UpdateUserInput } from './types.js';
2
2
  import type { AuthIdentifier } from '../auth-api/types.js';
3
+ /** Base contract for user persistence backends. */
3
4
  export declare abstract class UserStore<User, PublicUser> {
4
5
  protected readonly toPublicUser: PublicUserMapper<User, PublicUser>;
5
6
  private readonly bcryptRounds;
@@ -13,14 +14,22 @@ export declare abstract class UserStore<User, PublicUser> {
13
14
  protected hashPassword(plain: string): Promise<string>;
14
15
  verifyPassword(plain: string, hashed: string): Promise<boolean>;
15
16
  protected normalizeUserInput(input: Partial<CreateUserInput>): CreateUserInput;
17
+ /** Find a user by id/login/email identifier. */
16
18
  abstract findUser(identifier: AuthIdentifier | string): Promise<User | null>;
19
+ /** Find a user by primary id only. */
17
20
  abstract findById(id: AuthIdentifier): Promise<User | null>;
21
+ /** Find a user by login or email value. */
18
22
  abstract findByLoginOrEmail(loginOrEmail: string): Promise<User | null>;
23
+ /** Create a new user record. */
19
24
  abstract createUser(input: CreateUserInput): Promise<User>;
20
25
  abstract upsertUser(input: CreateUserInput): Promise<User>;
26
+ /** Update selected user fields. */
21
27
  abstract updateUser(id: AuthIdentifier, patch: UpdateUserInput): Promise<User>;
28
+ /** Persist a password hash for the given user id. */
22
29
  abstract setPasswordHash(id: AuthIdentifier, hash: string): Promise<void>;
30
+ /** Extract the password hash from a user record. */
23
31
  abstract getPasswordHash(user: User): string | null;
32
+ /** Extract the stable user identifier from a user record. */
24
33
  abstract getUserId(user: User): AuthIdentifier;
25
34
  toPublic(user: User): PublicUser;
26
35
  }
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MemoryUserStore = void 0;
4
- const user_id_js_1 = require("../auth-api/user-id.js");
5
- const base_js_1 = require("./base.js");
4
+ const user_id_js_1 = require("../auth-api/user-id.cjs");
5
+ const base_js_1 = require("./base.cjs");
6
6
  function cloneUser(user) {
7
7
  return { ...user };
8
8
  }
@@ -44,11 +44,15 @@ class MemoryUserStore extends base_js_1.UserStore {
44
44
  }
45
45
  const loginId = this.loginToId.get(identifier);
46
46
  if (loginId !== undefined) {
47
- return cloneUser(this.usersById.get(loginId));
47
+ const loginUser = this.usersById.get(loginId);
48
+ if (loginUser)
49
+ return cloneUser(loginUser);
48
50
  }
49
51
  const emailId = this.emailToId.get(identifier);
50
52
  if (emailId !== undefined) {
51
- return cloneUser(this.usersById.get(emailId));
53
+ const emailUser = this.usersById.get(emailId);
54
+ if (emailUser)
55
+ return cloneUser(emailUser);
52
56
  }
53
57
  }
54
58
  return null;
@@ -64,7 +68,19 @@ class MemoryUserStore extends base_js_1.UserStore {
64
68
  }
65
69
  }
66
70
  async findByLoginOrEmail(loginOrEmail) {
67
- return this.findUser(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;
68
84
  }
69
85
  async createUser(input) {
70
86
  const normalizedInput = this.normalizeUserInput(input);
@@ -118,9 +134,12 @@ class MemoryUserStore extends base_js_1.UserStore {
118
134
  throw new Error(`User with email ${updates.email} already exists`);
119
135
  }
120
136
  }
121
- if (normalizedInput.password) {
137
+ if (normalizedInput.password && normalizedInput.password.length > 0) {
122
138
  updates.password = await this.hashPassword(normalizedInput.password);
123
139
  }
140
+ else {
141
+ updates.password = existing.password;
142
+ }
124
143
  this.persistUser(updates);
125
144
  return cloneUser(updates);
126
145
  }
@@ -132,9 +151,12 @@ class MemoryUserStore extends base_js_1.UserStore {
132
151
  throw new Error(`User ${String(id)} not found`);
133
152
  }
134
153
  const updates = { ...user, ...patch };
135
- if (patch.password) {
154
+ if (patch.password && patch.password.length > 0) {
136
155
  updates.password = await this.hashPassword(patch.password);
137
156
  }
157
+ else {
158
+ updates.password = user.password;
159
+ }
138
160
  this.persistUser(updates);
139
161
  return cloneUser(updates);
140
162
  }
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SequelizeUserStore = exports.AuthUserModel = void 0;
4
4
  exports.initAuthUserModel = initAuthUserModel;
5
5
  const sequelize_1 = require("sequelize");
6
- const user_id_js_1 = require("../auth-api/user-id.js");
7
- const sequelize_utils_js_1 = require("../sequelize-utils.js");
8
- const base_js_1 = require("./base.js");
6
+ const user_id_js_1 = require("../auth-api/user-id.cjs");
7
+ const sequelize_utils_js_1 = require("../sequelize-utils.cjs");
8
+ const base_js_1 = require("./base.cjs");
9
9
  class AuthUserModel extends sequelize_1.Model {
10
10
  }
11
11
  exports.AuthUserModel = AuthUserModel;
@@ -120,11 +120,22 @@ class SequelizeUserStore extends base_js_1.UserStore {
120
120
  throw new Error(`User ${String(providedId)} not found`);
121
121
  }
122
122
  const next = { ...normalized };
123
- if (normalized.password) {
123
+ if (normalized.password && normalized.password.length > 0) {
124
124
  next.password = await this.hashPassword(normalized.password);
125
125
  }
126
+ else {
127
+ delete next.password;
128
+ }
126
129
  await model.set(next);
127
- await model.save();
130
+ try {
131
+ await model.save();
132
+ }
133
+ catch (error) {
134
+ if (error instanceof sequelize_1.UniqueConstraintError) {
135
+ throw new Error(`User with login ${normalized.login} or email ${normalized.email} already exists`);
136
+ }
137
+ throw error;
138
+ }
128
139
  return this.toUserRecord(model);
129
140
  }
130
141
  return this.createUser(input);
@@ -135,15 +146,29 @@ class SequelizeUserStore extends base_js_1.UserStore {
135
146
  throw new Error(`User ${String(id)} not found`);
136
147
  }
137
148
  const updates = { ...patch };
138
- if (patch.password) {
149
+ if (patch.password && patch.password.length > 0) {
139
150
  updates.password = await this.hashPassword(patch.password);
140
151
  }
152
+ else {
153
+ delete updates.password;
154
+ }
141
155
  await model.set(updates);
142
- await model.save();
156
+ try {
157
+ await model.save();
158
+ }
159
+ catch (error) {
160
+ if (error instanceof sequelize_1.UniqueConstraintError) {
161
+ throw new Error(`User with login or email already exists`);
162
+ }
163
+ throw error;
164
+ }
143
165
  return this.toUserRecord(model);
144
166
  }
145
167
  async setPasswordHash(id, hash) {
146
- await this.Users.update({ password: hash }, { where: { user_id: this.normalizeUserId(id) } });
168
+ const [affected] = await this.Users.update({ password: hash }, { where: { user_id: this.normalizeUserId(id) } });
169
+ if (affected === 0) {
170
+ throw new Error(`User ${String(id)} not found`);
171
+ }
147
172
  }
148
173
  getPasswordHash(user) {
149
174
  return user.password;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Shared type definitions used by both api-server-base and api-client-base.
3
+ * This file contains only `type`/`interface` declarations — no runtime code.
4
+ * Both packages reference these types via `import type`. Note that TypeScript
5
+ * does NOT inline `import type` references in emitted `.d.ts` files — the
6
+ * path aliases persist. Source files therefore keep inline type definitions
7
+ * to avoid broken references for consumers of the published packages.
8
+ */
9
+ export type AuthIdentifier = string | number;
10
+ export type SafeUser = Record<string, unknown>;
11
+ export interface ApiResponseData<T = unknown> {
12
+ success: boolean;
13
+ code: number;
14
+ message: string;
15
+ data: T | null;
16
+ errors: Record<string, string>;
17
+ }
18
+ export interface AuthTokenData<User = SafeUser> {
19
+ accessToken?: string;
20
+ refreshToken?: string;
21
+ user?: User;
22
+ }
23
+ export interface WhoAmIResponseData<User = SafeUser, UserId extends AuthIdentifier = AuthIdentifier> {
24
+ user: User;
25
+ isImpersonating: boolean;
26
+ realUser?: User | null;
27
+ realUserId?: UserId | null;
28
+ }
29
+ export interface LogoutResponseData {
30
+ revoked: number;
31
+ }
32
+ export type TokenStatus = 'active' | 'expired' | 'revoked';
33
+ export interface Token {
34
+ accessToken: string;
35
+ refreshToken: string;
36
+ userId: string;
37
+ expires?: Date;
38
+ issuedAt?: Date;
39
+ lastSeenAt?: Date;
40
+ status?: TokenStatus;
41
+ ruid?: string;
42
+ clientId?: string;
43
+ /**
44
+ * Optional session partition key. Token stores may use `domain` and `fingerprint`
45
+ * to replace previous sessions that match the same bucket.
46
+ */
47
+ domain?: string;
48
+ /**
49
+ * Optional device/session fingerprint used together with `domain` for session bucketing.
50
+ */
51
+ fingerprint?: string;
52
+ label?: string;
53
+ browser?: string;
54
+ device?: string;
55
+ ip?: string;
56
+ os?: string;
57
+ scope?: string[];
58
+ loginType?: string;
59
+ refreshTtlSeconds?: number;
60
+ sessionCookie?: boolean;
61
+ }
62
+ export interface TokenPair {
63
+ accessToken: string;
64
+ refreshToken: string;
65
+ }
66
+ export interface OAuthClient {
67
+ clientId: string;
68
+ clientSecret?: string;
69
+ hasSecret?: boolean;
70
+ firstParty?: boolean;
71
+ metadata?: Record<string, unknown>;
72
+ name?: string;
73
+ redirectUris: string[];
74
+ scope?: string[];
75
+ }
76
+ export interface AuthCodeData {
77
+ code: string;
78
+ clientId: string;
79
+ codeChallenge?: string;
80
+ codeChallengeMethod?: 'plain' | 'S256';
81
+ expiresAt: Date;
82
+ metadata?: Record<string, unknown>;
83
+ redirectUri?: string;
84
+ scope?: string[];
85
+ userId: AuthIdentifier;
86
+ }
87
+ export type AuthCode = AuthCodeData;
88
+ export type AuthCodeRequest = Omit<AuthCodeData, 'code' | 'expiresAt'> & {
89
+ code?: string;
90
+ expiresInSeconds?: number;
91
+ };
92
+ export interface OAuthStartParams {
93
+ provider: string;
94
+ redirectUri?: string;
95
+ scope?: string | string[];
96
+ state?: string;
97
+ extras?: Record<string, unknown>;
98
+ }
99
+ export interface OAuthStartResult extends Record<string, unknown> {
100
+ url: string;
101
+ state?: string;
102
+ codeVerifier?: string;
103
+ }
104
+ export interface OAuthCallbackParams {
105
+ provider: string;
106
+ query: Record<string, string | string[]>;
107
+ body: Record<string, unknown>;
108
+ }
109
+ export interface OAuthCallbackResult<PublicUser> extends Record<string, unknown> {
110
+ user: PublicUser;
111
+ tokens?: {
112
+ accessToken: string;
113
+ refreshToken: string;
114
+ };
115
+ }
116
+ export type TusMetadata = Record<string, string>;
117
+ export interface TusUploadRecord {
118
+ id: string;
119
+ length: number;
120
+ offset: number;
121
+ metadata: TusMetadata;
122
+ userId?: string;
123
+ createdAt: Date;
124
+ updatedAt: Date;
125
+ completedAt?: Date;
126
+ }
127
+ export interface TusCreateUploadInput {
128
+ id?: string;
129
+ length: number;
130
+ metadata: TusMetadata;
131
+ userId?: string;
132
+ }
133
+ export interface TusAppendInput {
134
+ uploadId: string;
135
+ offset: number;
136
+ chunk: Buffer;
137
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared type definitions used by both api-server-base and api-client-base.
3
+ * This file contains only `type`/`interface` declarations — no runtime code.
4
+ * Both packages reference these types via `import type`. Note that TypeScript
5
+ * does NOT inline `import type` references in emitted `.d.ts` files — the
6
+ * path aliases persist. Source files therefore keep inline type definitions
7
+ * to avoid broken references for consumers of the published packages.
8
+ */
9
+ export {};