@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,396 @@
1
+ import { DataTypes, Model, Op } from 'sequelize';
2
+ import { normalizeStringUserId } from '../auth-api/user-id.js';
3
+ import { applyTablePrefix, decodeStringArray, encodeStringArray, integerIdType, tableOptions } from '../sequelize-utils.js';
4
+ import { TokenStore } from './base.js';
5
+ class TokenModel extends Model {
6
+ }
7
+ function initTokenModel(sequelize, options = {}) {
8
+ const tableName = applyTablePrefix(options.tablePrefix, 'jwttokens');
9
+ const usePrefixedIndexNames = tableName !== 'jwttokens';
10
+ const accessIndexName = usePrefixedIndexNames ? `${tableName}_access_unique` : 'jwt_access_unique';
11
+ const refreshIndexName = usePrefixedIndexNames ? `${tableName}_refresh_unique` : 'jwt_refresh_unique';
12
+ TokenModel.init({
13
+ token_id: {
14
+ type: integerIdType(sequelize),
15
+ autoIncrement: true,
16
+ allowNull: false,
17
+ primaryKey: true
18
+ },
19
+ user_id: {
20
+ type: DataTypes.STRING(191),
21
+ allowNull: false
22
+ },
23
+ real_user_id: {
24
+ type: DataTypes.STRING(191),
25
+ allowNull: true,
26
+ defaultValue: null
27
+ },
28
+ expires: {
29
+ type: DataTypes.DATE,
30
+ allowNull: false
31
+ },
32
+ issued_at: {
33
+ type: DataTypes.DATE,
34
+ allowNull: false,
35
+ defaultValue: DataTypes.NOW
36
+ },
37
+ last_seen_at: {
38
+ type: DataTypes.DATE,
39
+ allowNull: false,
40
+ defaultValue: DataTypes.NOW
41
+ },
42
+ access: {
43
+ type: DataTypes.STRING(768),
44
+ allowNull: false
45
+ },
46
+ refresh: {
47
+ type: DataTypes.STRING(768),
48
+ allowNull: false
49
+ },
50
+ domain: {
51
+ type: DataTypes.STRING(64),
52
+ allowNull: false,
53
+ defaultValue: ''
54
+ },
55
+ fingerprint: {
56
+ type: DataTypes.STRING(64),
57
+ allowNull: false,
58
+ defaultValue: ''
59
+ },
60
+ label: {
61
+ type: DataTypes.STRING(128),
62
+ allowNull: false,
63
+ defaultValue: ''
64
+ },
65
+ browser: {
66
+ type: DataTypes.STRING(64),
67
+ allowNull: false,
68
+ defaultValue: ''
69
+ },
70
+ device: {
71
+ type: DataTypes.STRING(64),
72
+ allowNull: false,
73
+ defaultValue: ''
74
+ },
75
+ ip: {
76
+ type: DataTypes.STRING(45),
77
+ allowNull: false,
78
+ defaultValue: ''
79
+ },
80
+ os: {
81
+ type: DataTypes.STRING(64),
82
+ allowNull: false,
83
+ defaultValue: ''
84
+ },
85
+ login_type: {
86
+ type: DataTypes.STRING(64),
87
+ allowNull: false,
88
+ defaultValue: ''
89
+ },
90
+ refresh_ttl_seconds: {
91
+ type: DataTypes.INTEGER,
92
+ allowNull: true,
93
+ defaultValue: null
94
+ },
95
+ session_cookie: {
96
+ type: DataTypes.BOOLEAN,
97
+ allowNull: false,
98
+ defaultValue: false
99
+ },
100
+ client_id: {
101
+ type: DataTypes.STRING(128),
102
+ allowNull: true,
103
+ defaultValue: null
104
+ },
105
+ scope: {
106
+ type: DataTypes.TEXT,
107
+ allowNull: false,
108
+ defaultValue: '[]'
109
+ }
110
+ }, {
111
+ ...tableOptions(sequelize, 'jwttokens', options.tablePrefix, { timestamps: false }),
112
+ indexes: [
113
+ { name: accessIndexName, unique: true, fields: ['access'] },
114
+ { name: refreshIndexName, unique: true, fields: ['refresh'] }
115
+ ]
116
+ });
117
+ return TokenModel;
118
+ }
119
+ export class SequelizeTokenStore extends TokenStore {
120
+ constructor(options) {
121
+ super();
122
+ if (!options?.sequelize) {
123
+ throw new Error('SequelizeTokenStore requires an initialised Sequelize instance');
124
+ }
125
+ this.Tokens =
126
+ options.tokenModel ??
127
+ (options.tokenModelFactory ?? initTokenModel)(options.sequelize, {
128
+ tablePrefix: options.tablePrefix
129
+ });
130
+ }
131
+ async save(record) {
132
+ const normalized = this.normalizeToken(record);
133
+ const resolvedUserId = this.normalizeUserId(normalized.userId);
134
+ const resolvedRealUserId = this.resolveRealUserId(normalized.ruid);
135
+ const domain = normalized.domain;
136
+ const fingerprint = normalized.fingerprint;
137
+ const browser = normalized.browser;
138
+ const device = normalized.device;
139
+ const ip = normalized.ip;
140
+ const os = normalized.os;
141
+ const label = normalized.label;
142
+ const loginType = normalized.loginType ?? '';
143
+ const refreshTtlSeconds = typeof normalized.refreshTtlSeconds === 'number' && normalized.refreshTtlSeconds > 0
144
+ ? Math.floor(normalized.refreshTtlSeconds)
145
+ : null;
146
+ const issuedAt = normalized.issuedAt ?? new Date();
147
+ const lastSeenAt = normalized.lastSeenAt ?? issuedAt;
148
+ const sessionCookie = normalized.sessionCookie ?? false;
149
+ const removalWhere = { user_id: resolvedUserId };
150
+ if (record.domain !== undefined) {
151
+ removalWhere.domain = domain;
152
+ }
153
+ if (record.fingerprint !== undefined) {
154
+ removalWhere.fingerprint = fingerprint;
155
+ }
156
+ if (normalized.clientId) {
157
+ removalWhere.client_id = normalized.clientId;
158
+ }
159
+ const sequelize = this.Tokens.sequelize;
160
+ if (!sequelize) {
161
+ throw new Error('Token model is not bound to a Sequelize instance');
162
+ }
163
+ await sequelize.transaction(async (transaction) => {
164
+ await this.Tokens.destroy({ where: removalWhere, transaction });
165
+ // Access/refresh columns are unique. Remove stale collisions before insert to avoid
166
+ // transient uniqueness failures during retries/rotation edge-cases.
167
+ // Only include non-empty token values to prevent matching unrelated rows.
168
+ const collisionConditions = [];
169
+ if (normalized.accessToken) {
170
+ collisionConditions.push({ access: normalized.accessToken });
171
+ }
172
+ if (normalized.refreshToken) {
173
+ collisionConditions.push({ refresh: normalized.refreshToken });
174
+ }
175
+ if (collisionConditions.length > 0) {
176
+ await this.Tokens.destroy({
177
+ where: { [Op.or]: collisionConditions },
178
+ transaction
179
+ });
180
+ }
181
+ await this.Tokens.create({
182
+ user_id: resolvedUserId,
183
+ real_user_id: resolvedRealUserId,
184
+ access: normalized.accessToken ?? '',
185
+ refresh: normalized.refreshToken,
186
+ expires: normalized.expires,
187
+ issued_at: issuedAt,
188
+ last_seen_at: lastSeenAt,
189
+ domain,
190
+ fingerprint,
191
+ label,
192
+ browser,
193
+ device,
194
+ ip,
195
+ os,
196
+ client_id: normalized.clientId ?? null,
197
+ scope: this.encodeScope(normalized.scope),
198
+ login_type: loginType,
199
+ refresh_ttl_seconds: refreshTtlSeconds,
200
+ session_cookie: sessionCookie
201
+ }, { transaction });
202
+ });
203
+ }
204
+ async get(query, opts) {
205
+ if (!query.refreshToken && !query.accessToken && query.userId === undefined) {
206
+ throw new Error('At least one token lookup field must be provided');
207
+ }
208
+ const where = {};
209
+ if (query.refreshToken) {
210
+ where.refresh = query.refreshToken;
211
+ }
212
+ if (query.accessToken) {
213
+ where.access = query.accessToken;
214
+ }
215
+ if (query.userId !== undefined) {
216
+ where.user_id = this.normalizeUserId(query.userId);
217
+ }
218
+ if (query.clientId) {
219
+ where.client_id = query.clientId;
220
+ }
221
+ if (query.domain !== undefined && query.domain !== null) {
222
+ where.domain = query.domain;
223
+ }
224
+ if (query.fingerprint !== undefined && query.fingerprint !== null) {
225
+ where.fingerprint = query.fingerprint;
226
+ }
227
+ if (query.label) {
228
+ where.label = query.label;
229
+ }
230
+ if (!(opts?.includeExpired ?? false)) {
231
+ where.expires = { [Op.gt]: new Date() };
232
+ }
233
+ const model = await this.Tokens.findOne({ where });
234
+ return model ? this.toTokenRecord(model) : null;
235
+ }
236
+ async delete(query) {
237
+ if (!query.refreshToken && !query.accessToken && query.userId === undefined && !query.clientId) {
238
+ return 0;
239
+ }
240
+ const where = {};
241
+ if (query.refreshToken) {
242
+ where.refresh = query.refreshToken;
243
+ }
244
+ if (query.accessToken) {
245
+ where.access = query.accessToken;
246
+ }
247
+ if (query.userId !== undefined) {
248
+ where.user_id = this.normalizeUserId(query.userId);
249
+ }
250
+ if (query.clientId) {
251
+ where.client_id = query.clientId;
252
+ }
253
+ if (query.domain !== undefined && query.domain !== null) {
254
+ where.domain = query.domain;
255
+ }
256
+ if (query.fingerprint !== undefined && query.fingerprint !== null) {
257
+ where.fingerprint = query.fingerprint;
258
+ }
259
+ if (query.label) {
260
+ where.label = query.label;
261
+ }
262
+ return this.Tokens.destroy({ where });
263
+ }
264
+ async update(params) {
265
+ const where = { refresh: params.refreshToken };
266
+ if (params.clientId) {
267
+ where.client_id = params.clientId;
268
+ }
269
+ const updates = {};
270
+ if (params.accessToken !== undefined && params.accessToken !== null) {
271
+ updates.access = params.accessToken;
272
+ }
273
+ if (params.expires !== undefined && params.expires !== null) {
274
+ updates.expires = params.expires;
275
+ }
276
+ if (params.scope !== undefined) {
277
+ updates.scope = this.encodeScope(params.scope);
278
+ }
279
+ if (params.label !== undefined) {
280
+ updates.label = params.label ?? '';
281
+ }
282
+ if (params.domain !== undefined) {
283
+ updates.domain = params.domain ?? '';
284
+ }
285
+ if (params.fingerprint !== undefined) {
286
+ updates.fingerprint = params.fingerprint ?? '';
287
+ }
288
+ if (params.browser !== undefined) {
289
+ updates.browser = params.browser ?? '';
290
+ }
291
+ if (params.device !== undefined) {
292
+ updates.device = params.device ?? '';
293
+ }
294
+ if (params.ip !== undefined) {
295
+ updates.ip = params.ip ?? '';
296
+ }
297
+ if (params.os !== undefined) {
298
+ updates.os = params.os ?? '';
299
+ }
300
+ if (params.refreshTtlSeconds !== undefined) {
301
+ updates.refresh_ttl_seconds =
302
+ typeof params.refreshTtlSeconds === 'number' && params.refreshTtlSeconds > 0
303
+ ? Math.floor(params.refreshTtlSeconds)
304
+ : null;
305
+ }
306
+ if (params.loginType !== undefined) {
307
+ updates.login_type = params.loginType ?? '';
308
+ }
309
+ if (params.sessionCookie !== undefined) {
310
+ updates.session_cookie = params.sessionCookie;
311
+ }
312
+ if (params.issuedAt !== undefined && params.issuedAt !== null) {
313
+ updates.issued_at = params.issuedAt;
314
+ }
315
+ if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
316
+ updates.last_seen_at = params.lastSeenAt;
317
+ }
318
+ if (Object.keys(updates).length === 0) {
319
+ return false;
320
+ }
321
+ const sequelize = this.Tokens.sequelize;
322
+ if (!sequelize) {
323
+ throw new Error('Token model is not bound to a Sequelize instance');
324
+ }
325
+ return sequelize.transaction(async (transaction) => {
326
+ const [updated] = await this.Tokens.update(updates, { where, transaction });
327
+ return updated > 0;
328
+ });
329
+ }
330
+ async list(userId, opts = {}) {
331
+ const where = { user_id: this.normalizeUserId(userId) };
332
+ if (!(opts.includeExpired ?? false)) {
333
+ where.expires = { [Op.gt]: new Date() };
334
+ }
335
+ const models = await this.Tokens.findAll({
336
+ where,
337
+ order: [['issued_at', 'DESC']],
338
+ limit: opts.limit,
339
+ offset: opts.offset
340
+ });
341
+ return models.map((model) => this.toTokenRecord(model));
342
+ }
343
+ async close() {
344
+ return;
345
+ }
346
+ normalizeUserId(identifier) {
347
+ return normalizeStringUserId(identifier);
348
+ }
349
+ resolveRealUserId(ruid) {
350
+ if (ruid === undefined || ruid === null) {
351
+ return null;
352
+ }
353
+ const value = String(ruid);
354
+ if (value.length === 0 || value === '0') {
355
+ return null;
356
+ }
357
+ return value;
358
+ }
359
+ encodeScope(scope) {
360
+ if (!scope || (Array.isArray(scope) && scope.length === 0)) {
361
+ return '[]';
362
+ }
363
+ if (Array.isArray(scope)) {
364
+ return encodeStringArray(scope);
365
+ }
366
+ return encodeStringArray(scope.split(/\s+/).filter((entry) => entry.length > 0));
367
+ }
368
+ toTokenRecord(model) {
369
+ const scope = decodeStringArray(model.scope);
370
+ const normalized = this.normalizeToken({
371
+ userId: model.user_id,
372
+ refreshToken: model.refresh,
373
+ accessToken: model.access,
374
+ expires: model.expires,
375
+ issuedAt: model.issued_at,
376
+ lastSeenAt: model.last_seen_at,
377
+ domain: model.domain,
378
+ fingerprint: model.fingerprint,
379
+ label: model.label,
380
+ browser: model.browser,
381
+ device: model.device,
382
+ ip: model.ip,
383
+ os: model.os,
384
+ clientId: model.client_id ?? undefined,
385
+ scope,
386
+ loginType: model.login_type || undefined,
387
+ refreshTtlSeconds: model.refresh_ttl_seconds ?? undefined,
388
+ ruid: model.real_user_id ?? undefined,
389
+ sessionCookie: model.session_cookie
390
+ });
391
+ return {
392
+ ...normalized,
393
+ scope: normalized.scope ? [...normalized.scope] : undefined
394
+ };
395
+ }
396
+ }
@@ -0,0 +1,34 @@
1
+ export type TokenStatus = 'active' | 'expired' | 'revoked';
2
+ export interface Token {
3
+ accessToken: string;
4
+ refreshToken: string;
5
+ userId: string;
6
+ expires?: Date;
7
+ issuedAt?: Date;
8
+ lastSeenAt?: Date;
9
+ status?: TokenStatus;
10
+ ruid?: string;
11
+ clientId?: string;
12
+ /**
13
+ * Optional session partition key. Token stores may use `domain` and `fingerprint`
14
+ * to replace previous sessions that match the same bucket.
15
+ */
16
+ domain?: string;
17
+ /**
18
+ * Optional device/session fingerprint used together with `domain` for session bucketing.
19
+ */
20
+ fingerprint?: string;
21
+ label?: string;
22
+ browser?: string;
23
+ device?: string;
24
+ ip?: string;
25
+ os?: string;
26
+ scope?: string[];
27
+ loginType?: string;
28
+ refreshTtlSeconds?: number;
29
+ sessionCookie?: boolean;
30
+ }
31
+ export interface TokenPair {
32
+ accessToken: string;
33
+ refreshToken: string;
34
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { TusAppendInput, TusCreateUploadInput, TusUploadRecord, TusUploadStore } from './types.js';
2
+ export declare class TusUploadOffsetError extends Error {
3
+ readonly currentOffset: number;
4
+ constructor(currentOffset: number);
5
+ }
6
+ export declare class TusUploadExceedsLengthError extends Error {
7
+ constructor();
8
+ }
9
+ export declare class MemoryTusUploadStore implements TusUploadStore {
10
+ private readonly uploads;
11
+ private readonly chunks;
12
+ createUpload(input: TusCreateUploadInput): Promise<TusUploadRecord>;
13
+ getUpload(uploadId: string): Promise<TusUploadRecord | null>;
14
+ appendUpload(input: TusAppendInput): Promise<TusUploadRecord>;
15
+ deleteUpload(uploadId: string): Promise<boolean>;
16
+ readUpload(uploadId: string): Buffer | null;
17
+ }
@@ -0,0 +1,86 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ function cloneRecord(record) {
3
+ return {
4
+ ...record,
5
+ metadata: { ...record.metadata },
6
+ createdAt: new Date(record.createdAt),
7
+ updatedAt: new Date(record.updatedAt),
8
+ ...(record.completedAt ? { completedAt: new Date(record.completedAt) } : {})
9
+ };
10
+ }
11
+ export class TusUploadOffsetError extends Error {
12
+ constructor(currentOffset) {
13
+ super('Upload offset does not match current offset');
14
+ this.currentOffset = currentOffset;
15
+ }
16
+ }
17
+ export class TusUploadExceedsLengthError extends Error {
18
+ constructor() {
19
+ super('Upload exceeds declared length');
20
+ }
21
+ }
22
+ export class MemoryTusUploadStore {
23
+ constructor() {
24
+ this.uploads = new Map();
25
+ this.chunks = new Map();
26
+ }
27
+ async createUpload(input) {
28
+ const id = input.id?.trim() || randomUUID();
29
+ if (this.uploads.has(id)) {
30
+ throw new Error(`Upload ${id} already exists`);
31
+ }
32
+ const now = new Date();
33
+ const record = {
34
+ id,
35
+ length: Math.max(0, Math.floor(input.length)),
36
+ offset: 0,
37
+ metadata: { ...input.metadata },
38
+ ...(input.userId ? { userId: input.userId } : {}),
39
+ createdAt: now,
40
+ updatedAt: now
41
+ };
42
+ this.uploads.set(id, record);
43
+ this.chunks.set(id, []);
44
+ return cloneRecord(record);
45
+ }
46
+ async getUpload(uploadId) {
47
+ const found = this.uploads.get(uploadId);
48
+ return found ? cloneRecord(found) : null;
49
+ }
50
+ async appendUpload(input) {
51
+ const current = this.uploads.get(input.uploadId);
52
+ if (!current) {
53
+ throw new Error('Upload not found');
54
+ }
55
+ if (input.offset !== current.offset) {
56
+ throw new TusUploadOffsetError(current.offset);
57
+ }
58
+ const nextOffset = current.offset + input.chunk.length;
59
+ if (nextOffset > current.length) {
60
+ throw new TusUploadExceedsLengthError();
61
+ }
62
+ const now = new Date();
63
+ const updated = {
64
+ ...current,
65
+ offset: nextOffset,
66
+ updatedAt: now,
67
+ ...(nextOffset === current.length ? { completedAt: now } : {})
68
+ };
69
+ this.uploads.set(input.uploadId, updated);
70
+ if (input.chunk.length > 0) {
71
+ this.chunks.get(input.uploadId)?.push(Buffer.from(input.chunk));
72
+ }
73
+ return cloneRecord(updated);
74
+ }
75
+ async deleteUpload(uploadId) {
76
+ this.chunks.delete(uploadId);
77
+ return this.uploads.delete(uploadId);
78
+ }
79
+ readUpload(uploadId) {
80
+ const chunks = this.chunks.get(uploadId);
81
+ if (!chunks) {
82
+ return null;
83
+ }
84
+ return Buffer.concat(chunks);
85
+ }
86
+ }
@@ -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
+ }