@technomoron/api-server-base 2.0.0-beta.22 → 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.
- package/dist/cjs/common/types.cjs +10 -0
- package/dist/cjs/common/types.d.ts +137 -0
- package/dist/cjs/{api-module.cjs → server/src/api-module.cjs} +8 -0
- package/dist/{esm → cjs/server/src}/api-module.d.ts +12 -0
- package/dist/cjs/{api-server-base.cjs → server/src/api-server-base.cjs} +573 -615
- package/dist/{esm → cjs/server/src}/api-server-base.d.ts +97 -87
- package/dist/cjs/{auth-api/auth-module.js → server/src/auth-api/auth-module.cjs} +96 -76
- package/dist/cjs/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
- package/dist/cjs/{auth-api/compat-auth-storage.js → server/src/auth-api/compat-auth-storage.cjs} +4 -4
- package/dist/cjs/{auth-api/mem-auth-store.js → server/src/auth-api/mem-auth-store.cjs} +7 -7
- package/dist/cjs/{auth-api/module.js → server/src/auth-api/module.cjs} +1 -1
- package/dist/cjs/server/src/auth-api/schemas.cjs +171 -0
- package/dist/cjs/server/src/auth-api/schemas.d.ts +21 -0
- package/dist/cjs/{auth-api/sql-auth-store.js → server/src/auth-api/sql-auth-store.cjs} +8 -8
- package/dist/cjs/{auth-api/user-id.js → server/src/auth-api/user-id.cjs} +12 -3
- package/dist/{esm → cjs/server/src}/auth-cookie-options.d.ts +5 -3
- package/dist/cjs/server/src/base/client-info.cjs +285 -0
- package/dist/cjs/server/src/base/client-info.d.ts +27 -0
- package/dist/cjs/server/src/base/error-utils.cjs +50 -0
- package/dist/cjs/server/src/base/error-utils.d.ts +16 -0
- package/dist/cjs/server/src/base/request-utils.cjs +27 -0
- package/dist/cjs/server/src/base/request-utils.d.ts +8 -0
- package/dist/cjs/{index.cjs → server/src/index.cjs} +24 -15
- package/dist/{esm → cjs/server/src}/index.d.ts +7 -0
- package/dist/cjs/server/src/limiter/auth-rate-limiter.cjs +35 -0
- package/dist/cjs/server/src/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/cjs/server/src/limiter/fixed-window.cjs +41 -0
- package/dist/cjs/server/src/limiter/fixed-window.d.ts +11 -0
- package/dist/cjs/{oauth/base.js → server/src/oauth/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/oauth/base.d.ts +8 -1
- package/dist/cjs/{oauth/memory.js → server/src/oauth/memory.cjs} +7 -4
- package/dist/{esm → cjs/server/src}/oauth/memory.d.ts +1 -1
- package/dist/cjs/{oauth/models.js → server/src/oauth/models.cjs} +2 -2
- package/dist/cjs/{oauth/sequelize.js → server/src/oauth/sequelize.cjs} +11 -7
- package/dist/{esm → cjs/server/src}/oauth/sequelize.d.ts +1 -1
- package/dist/cjs/{passkey/base.js → server/src/passkey/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/passkey/base.d.ts +11 -0
- package/dist/cjs/{passkey/memory.js → server/src/passkey/memory.cjs} +2 -2
- package/dist/cjs/{passkey/models.js → server/src/passkey/models.cjs} +1 -1
- package/dist/cjs/{passkey/sequelize.js → server/src/passkey/sequelize.cjs} +3 -3
- package/dist/cjs/{passkey/service.js → server/src/passkey/service.cjs} +17 -3
- package/dist/{esm → cjs/server/src}/passkey/service.d.ts +1 -1
- package/dist/cjs/{sequelize-utils.js → server/src/sequelize-utils.cjs} +4 -5
- package/dist/cjs/{token/base.js → server/src/token/base.cjs} +4 -0
- package/dist/{esm → cjs/server/src}/token/base.d.ts +7 -0
- package/dist/cjs/{token/memory.js → server/src/token/memory.cjs} +15 -20
- package/dist/cjs/{token/sequelize.js → server/src/token/sequelize.cjs} +25 -11
- package/dist/cjs/server/src/upload/memory.cjs +92 -0
- package/dist/cjs/server/src/upload/memory.d.ts +17 -0
- package/dist/cjs/server/src/upload/tus-module.cjs +270 -0
- package/dist/cjs/server/src/upload/tus-module.d.ts +38 -0
- package/dist/cjs/server/src/upload/types.d.ts +8 -0
- package/dist/cjs/{user/base.js → server/src/user/base.cjs} +1 -0
- package/dist/{esm → cjs/server/src}/user/base.d.ts +9 -0
- package/dist/cjs/{user/memory.js → server/src/user/memory.cjs} +29 -7
- package/dist/cjs/{user/sequelize.js → server/src/user/sequelize.cjs} +33 -8
- package/dist/cjs/server/src/user/types.cjs +2 -0
- package/dist/esm/common/types.d.ts +137 -0
- package/dist/esm/common/types.js +9 -0
- package/dist/{cjs → esm/server/src}/api-module.d.ts +12 -0
- package/dist/esm/{api-module.js → server/src/api-module.js} +8 -0
- package/dist/{cjs → esm/server/src}/api-server-base.d.ts +97 -87
- package/dist/esm/{api-server-base.js → server/src/api-server-base.js} +562 -604
- package/dist/esm/{auth-api → server/src/auth-api}/auth-module.d.ts +1 -1
- package/dist/esm/{auth-api → server/src/auth-api}/auth-module.js +92 -72
- package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.js +3 -3
- package/dist/esm/server/src/auth-api/schemas.d.ts +21 -0
- package/dist/esm/server/src/auth-api/schemas.js +168 -0
- package/dist/esm/{auth-api → server/src/auth-api}/user-id.js +12 -3
- package/dist/{cjs → esm/server/src}/auth-cookie-options.d.ts +5 -3
- package/dist/esm/server/src/base/client-info.d.ts +27 -0
- package/dist/esm/server/src/base/client-info.js +282 -0
- package/dist/esm/server/src/base/error-utils.d.ts +16 -0
- package/dist/esm/server/src/base/error-utils.js +44 -0
- package/dist/esm/server/src/base/request-utils.d.ts +8 -0
- package/dist/esm/server/src/base/request-utils.js +23 -0
- package/dist/{cjs → esm/server/src}/index.d.ts +7 -0
- package/dist/esm/{index.js → server/src/index.js} +4 -0
- package/dist/esm/server/src/limiter/auth-rate-limiter.d.ts +12 -0
- package/dist/esm/server/src/limiter/auth-rate-limiter.js +32 -0
- package/dist/esm/server/src/limiter/fixed-window.d.ts +11 -0
- package/dist/esm/server/src/limiter/fixed-window.js +37 -0
- package/dist/{cjs → esm/server/src}/oauth/base.d.ts +8 -1
- package/dist/esm/server/src/oauth/base.js +3 -0
- package/dist/{cjs → esm/server/src}/oauth/memory.d.ts +1 -1
- package/dist/esm/{oauth → server/src/oauth}/memory.js +5 -2
- package/dist/{cjs → esm/server/src}/oauth/sequelize.d.ts +1 -1
- package/dist/esm/{oauth → server/src/oauth}/sequelize.js +6 -2
- package/dist/{cjs → esm/server/src}/passkey/base.d.ts +11 -0
- package/dist/esm/server/src/passkey/base.js +3 -0
- package/dist/{cjs → esm/server/src}/passkey/service.d.ts +1 -1
- package/dist/esm/{passkey → server/src/passkey}/service.js +17 -3
- package/dist/esm/{sequelize-utils.js → server/src/sequelize-utils.js} +4 -5
- package/dist/{cjs → esm/server/src}/token/base.d.ts +7 -0
- package/dist/esm/{token → server/src/token}/base.js +4 -0
- package/dist/esm/{token → server/src/token}/memory.js +14 -19
- package/dist/esm/{token → server/src/token}/sequelize.js +22 -8
- package/dist/esm/server/src/upload/memory.d.ts +17 -0
- package/dist/esm/server/src/upload/memory.js +86 -0
- package/dist/esm/server/src/upload/tus-module.d.ts +38 -0
- package/dist/esm/server/src/upload/tus-module.js +266 -0
- package/dist/esm/server/src/upload/types.d.ts +8 -0
- package/dist/{cjs → esm/server/src}/user/base.d.ts +9 -0
- package/dist/esm/{user → server/src/user}/base.js +1 -0
- package/dist/esm/{user → server/src/user}/memory.js +27 -5
- package/dist/esm/{user → server/src/user}/sequelize.js +30 -5
- package/dist/esm/server/src/user/types.js +1 -0
- package/docs/swagger/openapi.json +1 -1
- package/package.json +18 -17
- package/README.txt +0 -216
- package/dist/esm/oauth/base.js +0 -2
- package/dist/esm/passkey/base.js +0 -2
- /package/dist/cjs/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/module.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
- /package/dist/cjs/{auth-api/storage.js → server/src/auth-api/storage.cjs} +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
- /package/dist/cjs/{auth-api/types.js → server/src/auth-api/types.cjs} +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/types.d.ts +0 -0
- /package/dist/cjs/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
- /package/dist/cjs/{auth-cookie-options.js → server/src/auth-cookie-options.cjs} +0 -0
- /package/dist/cjs/{oauth → server/src/oauth}/models.d.ts +0 -0
- /package/dist/cjs/{oauth/types.js → server/src/oauth/types.cjs} +0 -0
- /package/dist/cjs/{oauth → server/src/oauth}/types.d.ts +0 -0
- /package/dist/cjs/{passkey/config.js → server/src/passkey/config.cjs} +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/config.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/memory.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/models.d.ts +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
- /package/dist/cjs/{passkey/types.js → server/src/passkey/types.cjs} +0 -0
- /package/dist/cjs/{passkey → server/src/passkey}/types.d.ts +0 -0
- /package/dist/cjs/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
- /package/dist/cjs/{token → server/src/token}/memory.d.ts +0 -0
- /package/dist/cjs/{token → server/src/token}/sequelize.d.ts +0 -0
- /package/dist/cjs/{token/types.js → server/src/token/types.cjs} +0 -0
- /package/dist/cjs/{token → server/src/token}/types.d.ts +0 -0
- /package/dist/cjs/{user/types.js → server/src/upload/types.cjs} +0 -0
- /package/dist/cjs/{user → server/src/user}/memory.d.ts +0 -0
- /package/dist/cjs/{user → server/src/user}/sequelize.d.ts +0 -0
- /package/dist/cjs/{user → server/src/user}/types.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/compat-auth-storage.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/mem-auth-store.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/module.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/module.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/sql-auth-store.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/storage.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/storage.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/types.d.ts +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/types.js +0 -0
- /package/dist/esm/{auth-api → server/src/auth-api}/user-id.d.ts +0 -0
- /package/dist/esm/{auth-cookie-options.js → server/src/auth-cookie-options.js} +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/models.d.ts +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/models.js +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/types.d.ts +0 -0
- /package/dist/esm/{oauth → server/src/oauth}/types.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/config.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/config.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/memory.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/memory.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/models.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/models.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/sequelize.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/sequelize.js +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/types.d.ts +0 -0
- /package/dist/esm/{passkey → server/src/passkey}/types.js +0 -0
- /package/dist/esm/{sequelize-utils.d.ts → server/src/sequelize-utils.d.ts} +0 -0
- /package/dist/esm/{token → server/src/token}/memory.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/sequelize.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/types.d.ts +0 -0
- /package/dist/esm/{token → server/src/token}/types.js +0 -0
- /package/dist/esm/{user → server/src/upload}/types.js +0 -0
- /package/dist/esm/{user → server/src/user}/memory.d.ts +0 -0
- /package/dist/esm/{user → server/src/user}/sequelize.d.ts +0 -0
- /package/dist/esm/{user → server/src/user}/types.d.ts +0 -0
|
@@ -8,7 +8,7 @@ export declare class PasskeyService {
|
|
|
8
8
|
private readonly logger;
|
|
9
9
|
constructor(config: PasskeyServiceConfig, adapter: PasskeyStorageAdapter, logger?: Logger);
|
|
10
10
|
listUserCredentials(userId: AuthIdentifier): Promise<StoredPasskeyCredential[]>;
|
|
11
|
-
deleteCredential(credentialId: Buffer | string): Promise<boolean>;
|
|
11
|
+
deleteCredential(credentialId: Buffer | string, userId?: AuthIdentifier): Promise<boolean>;
|
|
12
12
|
createChallenge(params: PasskeyChallengeParams): Promise<PasskeyChallenge>;
|
|
13
13
|
verifyResponse(params: PasskeyVerificationParams): Promise<PasskeyVerificationResult>;
|
|
14
14
|
private createRegistrationChallenge;
|
|
@@ -125,7 +125,19 @@ export class PasskeyService {
|
|
|
125
125
|
async listUserCredentials(userId) {
|
|
126
126
|
return this.adapter.listUserCredentials(userId);
|
|
127
127
|
}
|
|
128
|
-
async deleteCredential(credentialId) {
|
|
128
|
+
async deleteCredential(credentialId, userId) {
|
|
129
|
+
if (userId !== undefined) {
|
|
130
|
+
const credentials = await this.adapter.listUserCredentials(userId);
|
|
131
|
+
const target = Buffer.isBuffer(credentialId) ? credentialId : Buffer.from(String(credentialId), 'base64');
|
|
132
|
+
const owns = credentials.some((c) => {
|
|
133
|
+
const stored = Buffer.isBuffer(c.credentialId)
|
|
134
|
+
? c.credentialId
|
|
135
|
+
: Buffer.from(String(c.credentialId), 'base64');
|
|
136
|
+
return stored.equals(target);
|
|
137
|
+
});
|
|
138
|
+
if (!owns)
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
129
141
|
return this.adapter.deleteCredential(credentialId);
|
|
130
142
|
}
|
|
131
143
|
async createChallenge(params) {
|
|
@@ -251,7 +263,9 @@ export class PasskeyService {
|
|
|
251
263
|
const attestationResponse = params.response.response;
|
|
252
264
|
const credentialIdPrimary = toBufferOrNull(registrationInfo.credentialID);
|
|
253
265
|
const credentialIdFallback = toBufferOrNull(params.response.id);
|
|
254
|
-
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0
|
|
266
|
+
const credentialId = credentialIdPrimary && credentialIdPrimary.length > 0
|
|
267
|
+
? credentialIdPrimary
|
|
268
|
+
: (credentialIdFallback ?? Buffer.alloc(0));
|
|
255
269
|
const publicKeyPrimary = toBufferOrNull(registrationInfo.credentialPublicKey);
|
|
256
270
|
let publicKeyFallback = toBufferOrNull(attestationResponse?.publicKey);
|
|
257
271
|
if ((!publicKeyPrimary || publicKeyPrimary.length === 0) && attestationResponse?.attestationObject) {
|
|
@@ -294,7 +308,7 @@ export class PasskeyService {
|
|
|
294
308
|
publicKey: storedPublicKey,
|
|
295
309
|
counter: registrationInfo.counter ?? 0,
|
|
296
310
|
transports: sanitizeTransports(params.response.transports),
|
|
297
|
-
backedUp: registrationInfo.credentialDeviceType === 'multiDevice',
|
|
311
|
+
backedUp: registrationInfo.credentialBackedUp ?? registrationInfo.credentialDeviceType === 'multiDevice',
|
|
298
312
|
deviceType: registrationInfo.credentialDeviceType,
|
|
299
313
|
label: toOptionalString(params.label),
|
|
300
314
|
createdDomain: toOptionalString(params.domain),
|
|
@@ -39,10 +39,9 @@ export function decodeStringArray(raw) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
|
-
//
|
|
42
|
+
// Malformed JSON — return empty rather than guessing via whitespace split.
|
|
43
|
+
// A whitespace-split fallback could silently grant unintended permissions
|
|
44
|
+
// if a scope/role column contains a corrupted value like "admin user".
|
|
43
45
|
}
|
|
44
|
-
return
|
|
45
|
-
.split(/\s+/)
|
|
46
|
-
.map((entry) => entry.trim())
|
|
47
|
-
.filter((entry) => entry.length > 0);
|
|
46
|
+
return [];
|
|
48
47
|
}
|
|
@@ -17,20 +17,27 @@ export interface JwtDecodeResult<T> {
|
|
|
17
17
|
data?: T;
|
|
18
18
|
error?: string;
|
|
19
19
|
}
|
|
20
|
+
/** Base contract for token/session persistence backends plus shared JWT helpers. */
|
|
20
21
|
export declare abstract class TokenStore {
|
|
22
|
+
/** Create/persist a token record. */
|
|
21
23
|
abstract save(record: Token): Promise<void>;
|
|
24
|
+
/** Read a token record by partial query. */
|
|
22
25
|
abstract get(query: Partial<Token>, opts?: {
|
|
23
26
|
includeExpired?: boolean;
|
|
24
27
|
}): Promise<Token | null>;
|
|
28
|
+
/** Delete token records matching a partial query. */
|
|
25
29
|
abstract delete(query: Partial<Token>): Promise<number>;
|
|
30
|
+
/** Update a token identified by refresh token. */
|
|
26
31
|
abstract update(update: Partial<Token> & {
|
|
27
32
|
refreshToken: string;
|
|
28
33
|
}): Promise<boolean>;
|
|
34
|
+
/** List tokens for a specific user. */
|
|
29
35
|
abstract list(userId: string | number, opts?: {
|
|
30
36
|
limit?: number;
|
|
31
37
|
offset?: number;
|
|
32
38
|
includeExpired?: boolean;
|
|
33
39
|
}): Promise<Token[]>;
|
|
40
|
+
/** Close underlying resources. */
|
|
34
41
|
abstract close(): Promise<void>;
|
|
35
42
|
normalizeToken(token: Partial<Token>): Token;
|
|
36
43
|
jwtSign(payload: JwtSignPayload, secret: string, expiresInSeconds: number, options?: SignOptions): JwtSignResult;
|
|
@@ -36,6 +36,9 @@ function normalizeTokenInternal(input) {
|
|
|
36
36
|
const userId = String(input.userId);
|
|
37
37
|
const ruid = input.ruid === undefined || input.ruid === null ? undefined : String(input.ruid);
|
|
38
38
|
const expires = input.expires ? new Date(input.expires) : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
39
|
+
if (Number.isNaN(expires.getTime())) {
|
|
40
|
+
throw new Error(`Invalid token expiry value: ${String(input.expires)}`);
|
|
41
|
+
}
|
|
39
42
|
const issuedAt = input.issuedAt ? new Date(input.issuedAt) : new Date();
|
|
40
43
|
const lastSeenAt = input.lastSeenAt ? new Date(input.lastSeenAt) : issuedAt;
|
|
41
44
|
const scope = normalizeScope(input.scope);
|
|
@@ -65,6 +68,7 @@ function normalizeTokenInternal(input) {
|
|
|
65
68
|
sessionCookie
|
|
66
69
|
};
|
|
67
70
|
}
|
|
71
|
+
/** Base contract for token/session persistence backends plus shared JWT helpers. */
|
|
68
72
|
export class TokenStore {
|
|
69
73
|
// Instance helpers
|
|
70
74
|
normalizeToken(token) {
|
|
@@ -15,16 +15,16 @@ function cloneToken(record) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
function matchesQuery(record, query, includeExpired) {
|
|
18
|
-
if (query.refreshToken && record.refreshToken !== query.refreshToken) {
|
|
18
|
+
if (query.refreshToken !== undefined && record.refreshToken !== query.refreshToken) {
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
-
if (query.accessToken && record.accessToken !== query.accessToken) {
|
|
21
|
+
if (query.accessToken !== undefined && record.accessToken !== query.accessToken) {
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
24
|
if (query.userId !== undefined && comparableUserId(record.userId) !== comparableUserId(query.userId)) {
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
|
-
if (query.clientId && record.clientId !== query.clientId) {
|
|
27
|
+
if (query.clientId !== undefined && record.clientId !== query.clientId) {
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
if (query.domain !== undefined && (record.domain ?? '') !== (query.domain ?? '')) {
|
|
@@ -36,11 +36,14 @@ function matchesQuery(record, query, includeExpired) {
|
|
|
36
36
|
if (query.loginType !== undefined && record.loginType !== (query.loginType ?? undefined)) {
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
|
-
if (query.label && record.label !== query.label) {
|
|
39
|
+
if (query.label !== undefined && record.label !== query.label) {
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
|
-
if (!includeExpired
|
|
43
|
-
|
|
42
|
+
if (!includeExpired) {
|
|
43
|
+
const expiresTime = record.expires ? record.expires.getTime() : Infinity;
|
|
44
|
+
if (expiresTime < Date.now()) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
return true;
|
|
46
49
|
}
|
|
@@ -163,16 +166,14 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
163
166
|
const merged = { ...token };
|
|
164
167
|
const maybeAssign = (key) => {
|
|
165
168
|
const value = params[key];
|
|
166
|
-
if (value !== undefined) {
|
|
169
|
+
if (value !== undefined && value !== null) {
|
|
167
170
|
merged[key] = value;
|
|
168
171
|
}
|
|
169
172
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
merged.expires = params.expires;
|
|
175
|
-
}
|
|
173
|
+
maybeAssign('accessToken');
|
|
174
|
+
maybeAssign('expires');
|
|
175
|
+
maybeAssign('issuedAt');
|
|
176
|
+
maybeAssign('lastSeenAt');
|
|
176
177
|
maybeAssign('scope');
|
|
177
178
|
maybeAssign('label');
|
|
178
179
|
maybeAssign('domain');
|
|
@@ -183,12 +184,6 @@ export class MemoryTokenStore extends TokenStore {
|
|
|
183
184
|
maybeAssign('os');
|
|
184
185
|
maybeAssign('refreshTtlSeconds');
|
|
185
186
|
maybeAssign('loginType');
|
|
186
|
-
if (params.issuedAt !== undefined && params.issuedAt !== null) {
|
|
187
|
-
merged.issuedAt = params.issuedAt;
|
|
188
|
-
}
|
|
189
|
-
if (params.lastSeenAt !== undefined && params.lastSeenAt !== null) {
|
|
190
|
-
merged.lastSeenAt = params.lastSeenAt;
|
|
191
|
-
}
|
|
192
187
|
maybeAssign('sessionCookie');
|
|
193
188
|
const normalized = this.normalizeToken(merged);
|
|
194
189
|
const previousUserId = token.userId;
|
|
@@ -164,12 +164,20 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
164
164
|
await this.Tokens.destroy({ where: removalWhere, transaction });
|
|
165
165
|
// Access/refresh columns are unique. Remove stale collisions before insert to avoid
|
|
166
166
|
// transient uniqueness failures during retries/rotation edge-cases.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
181
|
await this.Tokens.create({
|
|
174
182
|
user_id: resolvedUserId,
|
|
175
183
|
real_user_id: resolvedRealUserId,
|
|
@@ -310,8 +318,14 @@ export class SequelizeTokenStore extends TokenStore {
|
|
|
310
318
|
if (Object.keys(updates).length === 0) {
|
|
311
319
|
return false;
|
|
312
320
|
}
|
|
313
|
-
const
|
|
314
|
-
|
|
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
|
+
});
|
|
315
329
|
}
|
|
316
330
|
async list(userId, opts = {}) {
|
|
317
331
|
const where = { user_id: this.normalizeUserId(userId) };
|
|
@@ -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 '../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,266 @@
|
|
|
1
|
+
import { ApiError, ApiModule } from '../api-server-base.js';
|
|
2
|
+
import { MemoryTusUploadStore, TusUploadExceedsLengthError, TusUploadOffsetError } from './memory.js';
|
|
3
|
+
const TUS_VERSION = '1.0.0';
|
|
4
|
+
function parseNonNegativeInt(raw, headerName) {
|
|
5
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
6
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
8
|
+
throw new ApiError({ code: 400, message: `Missing or invalid ${headerName} header` });
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
function parseUploadLength(raw) {
|
|
13
|
+
return parseNonNegativeInt(raw, 'Upload-Length');
|
|
14
|
+
}
|
|
15
|
+
function parseOffset(raw) {
|
|
16
|
+
return parseNonNegativeInt(raw, 'Upload-Offset');
|
|
17
|
+
}
|
|
18
|
+
function decodeMetadata(raw) {
|
|
19
|
+
const source = Array.isArray(raw) ? raw[0] : raw;
|
|
20
|
+
const input = String(source ?? '').trim();
|
|
21
|
+
if (!input) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const pair of input.split(',')) {
|
|
26
|
+
const trimmed = pair.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const [key, encoded = ''] = trimmed.split(' ');
|
|
31
|
+
if (!key) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
out[key] = Buffer.from(encoded, 'base64').toString('utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
out[key] = '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
async function readChunk(request, maxBytes) {
|
|
44
|
+
if (Buffer.isBuffer(request.body)) {
|
|
45
|
+
return request.body;
|
|
46
|
+
}
|
|
47
|
+
if (typeof request.body === 'string') {
|
|
48
|
+
return Buffer.from(request.body);
|
|
49
|
+
}
|
|
50
|
+
// Fastify only sets request.body when the Content-Type parser fires.
|
|
51
|
+
// Fall back to reading the raw stream, but enforce the same size cap to
|
|
52
|
+
// prevent bodyLimit from being bypassed by omitting the Content-Type header.
|
|
53
|
+
const parts = [];
|
|
54
|
+
let total = 0;
|
|
55
|
+
for await (const chunk of request.raw) {
|
|
56
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
57
|
+
total += buf.length;
|
|
58
|
+
if (total > maxBytes) {
|
|
59
|
+
throw new ApiError({ code: 413, message: 'Upload chunk exceeds maximum chunk size' });
|
|
60
|
+
}
|
|
61
|
+
parts.push(buf);
|
|
62
|
+
}
|
|
63
|
+
return Buffer.concat(parts);
|
|
64
|
+
}
|
|
65
|
+
function setTusHeaders(reply, supportsTermination = false) {
|
|
66
|
+
reply.header('Tus-Resumable', TUS_VERSION);
|
|
67
|
+
reply.header('Tus-Version', TUS_VERSION);
|
|
68
|
+
reply.header('Tus-Extension', supportsTermination ? 'creation,termination' : 'creation');
|
|
69
|
+
}
|
|
70
|
+
function toLocation(basePath, uploadId) {
|
|
71
|
+
const normalized = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
72
|
+
return `${normalized}/${uploadId}`;
|
|
73
|
+
}
|
|
74
|
+
export class TusUploadModule extends ApiModule {
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
super({ namespace: '' });
|
|
77
|
+
const rawPath = options.basePath?.trim() || '/api/v1/upload';
|
|
78
|
+
this.basePath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
|
79
|
+
this.chunkMaxBytes = options.chunkMaxBytes ?? 64 * 1024 * 1024;
|
|
80
|
+
this.uploadMaxBytes = options.uploadMaxBytes ?? 10 * 1024 * 1024 * 1024; // 10 GiB
|
|
81
|
+
this.store = options.store ?? new MemoryTusUploadStore();
|
|
82
|
+
this.auth = options.auth;
|
|
83
|
+
this.onUploadComplete = options.onUploadComplete;
|
|
84
|
+
}
|
|
85
|
+
onMount() {
|
|
86
|
+
this.installTusRoutes();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Authenticate the request if an auth config was provided.
|
|
90
|
+
* Returns false and sends a 401/403 response if auth fails, so the caller
|
|
91
|
+
* can bail out early with `if (!await this.checkAuth(request, reply)) return`.
|
|
92
|
+
*/
|
|
93
|
+
async checkAuth(request, reply) {
|
|
94
|
+
if (!this.auth) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const apiReq = await this.server.resolveRequest(request, reply, this.auth);
|
|
99
|
+
return apiReq.tokenData?.uid != null ? String(apiReq.tokenData.uid) : null;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const code = error instanceof ApiError ? error.code : 401;
|
|
103
|
+
const message = error instanceof ApiError ? error.message : 'Unauthorized';
|
|
104
|
+
reply.code(code).send({ success: false, code, message, data: null, errors: {} });
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
authFailed(result) {
|
|
109
|
+
// When auth is configured and checkAuth catches an error, it returns
|
|
110
|
+
// `false as unknown as string` after sending a response. We distinguish
|
|
111
|
+
// that from a legitimate `null` (no auth configured / no uid in token)
|
|
112
|
+
// by checking the exact `false` sentinel.
|
|
113
|
+
return result === false;
|
|
114
|
+
}
|
|
115
|
+
verifyOwnership(upload, authUserId, reply) {
|
|
116
|
+
if (upload.userId && authUserId && upload.userId !== authUserId) {
|
|
117
|
+
reply
|
|
118
|
+
.code(403)
|
|
119
|
+
.send({ success: false, code: 403, message: 'Upload belongs to another user', data: null, errors: {} });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
installTusRoutes() {
|
|
125
|
+
const app = this.server.fastify;
|
|
126
|
+
const hasTermination = typeof this.store.deleteUpload === 'function';
|
|
127
|
+
try {
|
|
128
|
+
app.addContentTypeParser('application/offset+octet-stream', { parseAs: 'buffer' }, (_req, body, done) => {
|
|
129
|
+
done(null, body);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Parser may already be present when mounting multiple upload modules.
|
|
134
|
+
const message = error instanceof Error ? error.message : '';
|
|
135
|
+
if (!message.includes('already registered') && !message.includes('already exists')) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
app.options(this.basePath, async (_request, reply) => {
|
|
140
|
+
setTusHeaders(reply, hasTermination);
|
|
141
|
+
reply.code(204).send();
|
|
142
|
+
});
|
|
143
|
+
app.post(this.basePath, async (request, reply) => {
|
|
144
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
145
|
+
if (this.authFailed(authUserId))
|
|
146
|
+
return;
|
|
147
|
+
setTusHeaders(reply, hasTermination);
|
|
148
|
+
const length = parseUploadLength(request.headers['upload-length']);
|
|
149
|
+
if (length <= 0) {
|
|
150
|
+
reply.code(400).send({
|
|
151
|
+
success: false,
|
|
152
|
+
code: 400,
|
|
153
|
+
message: 'Upload-Length must be greater than zero',
|
|
154
|
+
data: null,
|
|
155
|
+
errors: {}
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (length > this.uploadMaxBytes) {
|
|
160
|
+
reply.code(413).send({
|
|
161
|
+
success: false,
|
|
162
|
+
code: 413,
|
|
163
|
+
message: `Upload-Length exceeds the maximum allowed size of ${this.uploadMaxBytes} bytes`,
|
|
164
|
+
data: null,
|
|
165
|
+
errors: {}
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const metadata = decodeMetadata(request.headers['upload-metadata']);
|
|
170
|
+
const created = await this.store.createUpload({ length, metadata, userId: authUserId ?? undefined });
|
|
171
|
+
reply.header('Location', toLocation(this.basePath, created.id));
|
|
172
|
+
reply.header('Upload-Offset', String(created.offset));
|
|
173
|
+
reply.code(201).send();
|
|
174
|
+
});
|
|
175
|
+
app.head(`${this.basePath}/:uploadId`, async (request, reply) => {
|
|
176
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
177
|
+
if (this.authFailed(authUserId))
|
|
178
|
+
return;
|
|
179
|
+
setTusHeaders(reply, hasTermination);
|
|
180
|
+
const uploadId = String(request.params.uploadId ?? '');
|
|
181
|
+
const upload = await this.store.getUpload(uploadId);
|
|
182
|
+
if (!upload) {
|
|
183
|
+
reply.code(404).send();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!this.verifyOwnership(upload, authUserId, reply))
|
|
187
|
+
return;
|
|
188
|
+
reply.header('Upload-Offset', String(upload.offset));
|
|
189
|
+
reply.header('Upload-Length', String(upload.length));
|
|
190
|
+
reply.code(200).send();
|
|
191
|
+
});
|
|
192
|
+
app.patch(`${this.basePath}/:uploadId`, { bodyLimit: this.chunkMaxBytes }, async (request, reply) => {
|
|
193
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
194
|
+
if (this.authFailed(authUserId))
|
|
195
|
+
return;
|
|
196
|
+
setTusHeaders(reply, hasTermination);
|
|
197
|
+
const uploadId = String(request.params.uploadId ?? '');
|
|
198
|
+
const upload = await this.store.getUpload(uploadId);
|
|
199
|
+
if (!upload) {
|
|
200
|
+
reply.code(404).send();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!this.verifyOwnership(upload, authUserId, reply))
|
|
204
|
+
return;
|
|
205
|
+
const offset = parseOffset(request.headers['upload-offset']);
|
|
206
|
+
if (offset !== upload.offset) {
|
|
207
|
+
reply.header('Upload-Offset', String(upload.offset));
|
|
208
|
+
reply.code(409).send();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const chunk = await readChunk(request, this.chunkMaxBytes);
|
|
212
|
+
try {
|
|
213
|
+
const updated = await this.store.appendUpload({ uploadId, offset, chunk });
|
|
214
|
+
reply.header('Upload-Offset', String(updated.offset));
|
|
215
|
+
if (updated.completedAt && this.onUploadComplete) {
|
|
216
|
+
try {
|
|
217
|
+
await this.onUploadComplete(updated);
|
|
218
|
+
}
|
|
219
|
+
catch (completionError) {
|
|
220
|
+
console.error('[TusUploadModule] onUploadComplete callback failed', completionError);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
reply.code(204).send();
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof TusUploadOffsetError) {
|
|
227
|
+
reply.header('Upload-Offset', String(error.currentOffset));
|
|
228
|
+
reply.code(409).send();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (error instanceof TusUploadExceedsLengthError) {
|
|
232
|
+
reply.code(413).send({
|
|
233
|
+
success: false,
|
|
234
|
+
code: 413,
|
|
235
|
+
message: 'Upload chunk exceeds declared upload length',
|
|
236
|
+
data: null,
|
|
237
|
+
errors: {}
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
app.delete(`${this.basePath}/:uploadId`, async (request, reply) => {
|
|
245
|
+
const authUserId = await this.checkAuth(request, reply);
|
|
246
|
+
if (this.authFailed(authUserId))
|
|
247
|
+
return;
|
|
248
|
+
setTusHeaders(reply, hasTermination);
|
|
249
|
+
const uploadId = String(request.params.uploadId ?? '');
|
|
250
|
+
if (!this.store.deleteUpload) {
|
|
251
|
+
reply.header('Allow', 'OPTIONS, POST, HEAD, PATCH');
|
|
252
|
+
reply.code(405).send();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const upload = await this.store.getUpload(uploadId);
|
|
256
|
+
if (!upload) {
|
|
257
|
+
reply.code(404).send();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!this.verifyOwnership(upload, authUserId, reply))
|
|
261
|
+
return;
|
|
262
|
+
const deleted = await this.store.deleteUpload(uploadId);
|
|
263
|
+
reply.code(deleted ? 204 : 404).send();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -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
|
+
}
|