sa2kit 1.6.85 → 1.6.89
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/boothVaultService-Cn4WPhjg.d.mts +83 -0
- package/dist/boothVaultService-Cn4WPhjg.d.ts +83 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +366 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +358 -2
- package/dist/index.mjs.map +1 -1
- package/dist/vocaloidBooth/index.d.mts +64 -0
- package/dist/vocaloidBooth/index.d.ts +64 -0
- package/dist/vocaloidBooth/index.js +376 -0
- package/dist/vocaloidBooth/index.js.map +1 -0
- package/dist/vocaloidBooth/index.mjs +362 -0
- package/dist/vocaloidBooth/index.mjs.map +1 -0
- package/dist/vocaloidBooth/server/index.d.mts +110 -0
- package/dist/vocaloidBooth/server/index.d.ts +110 -0
- package/dist/vocaloidBooth/server/index.js +247 -0
- package/dist/vocaloidBooth/server/index.js.map +1 -0
- package/dist/vocaloidBooth/server/index.mjs +237 -0
- package/dist/vocaloidBooth/server/index.mjs.map +1 -0
- package/dist/vocaloidBooth/web/index.d.mts +3 -0
- package/dist/vocaloidBooth/web/index.d.ts +3 -0
- package/dist/vocaloidBooth/web/index.js +376 -0
- package/dist/vocaloidBooth/web/index.js.map +1 -0
- package/dist/vocaloidBooth/web/index.mjs +362 -0
- package/dist/vocaloidBooth/web/index.mjs.map +1 -0
- package/package.json +16 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { B as BoothVaultStore, a as BoothUploadRecord, b as BoothFileItem, c as BoothFileKind, C as CreateBoothUploadInput, d as BoothVaultService, e as CreateBoothUploadResult, f as BoothAuditEvent } from '../../boothVaultService-Cn4WPhjg.js';
|
|
2
|
+
import { A as AccessPermission, c as UploadProgress } from '../../types-CK4We_aI.js';
|
|
3
|
+
import { U as UniversalFileService } from '../../UniversalFileService-Ct4baUMz.js';
|
|
4
|
+
import 'events';
|
|
5
|
+
|
|
6
|
+
declare class InMemoryBoothVaultStore implements BoothVaultStore {
|
|
7
|
+
private readonly recordsById;
|
|
8
|
+
private readonly idByCode;
|
|
9
|
+
saveRecord(record: BoothUploadRecord): Promise<void>;
|
|
10
|
+
findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
|
|
11
|
+
findByRecordId(recordId: string): Promise<BoothUploadRecord | null>;
|
|
12
|
+
incrementDownloadCount(recordId: string): Promise<void>;
|
|
13
|
+
existsByMatchCode(matchCode: string): Promise<boolean>;
|
|
14
|
+
listActiveRecords(): Promise<BoothUploadRecord[]>;
|
|
15
|
+
updateStatus(recordId: string, status: BoothUploadRecord['status']): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BoothVaultRecordRepository {
|
|
19
|
+
create(record: BoothUploadRecord): Promise<void>;
|
|
20
|
+
findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
|
|
21
|
+
findByRecordId?(recordId: string): Promise<BoothUploadRecord | null>;
|
|
22
|
+
updateDownloadCount(recordId: string, downloadCount: number): Promise<void>;
|
|
23
|
+
existsByMatchCode(matchCode: string): Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
interface BoothObjectStorageProvider {
|
|
26
|
+
save(file: File | Blob | Buffer, objectKey: string, contentType?: string): Promise<string>;
|
|
27
|
+
getSignedDownloadUrl(objectKey: string, expiresInSeconds?: number): Promise<string>;
|
|
28
|
+
delete(objectKey: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
declare class RepositoryBoothVaultStore implements BoothVaultStore {
|
|
31
|
+
private readonly repository;
|
|
32
|
+
constructor(repository: BoothVaultRecordRepository);
|
|
33
|
+
saveRecord(record: BoothUploadRecord): Promise<void>;
|
|
34
|
+
findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;
|
|
35
|
+
incrementDownloadCount(recordId: string): Promise<void>;
|
|
36
|
+
existsByMatchCode(matchCode: string): Promise<boolean>;
|
|
37
|
+
findByRecordId(recordId: string): Promise<BoothUploadRecord | null>;
|
|
38
|
+
}
|
|
39
|
+
interface BoothSignedFile extends BoothFileItem {
|
|
40
|
+
signedDownloadUrl: string;
|
|
41
|
+
}
|
|
42
|
+
declare const signRecordFiles: (record: BoothUploadRecord, storage: BoothObjectStorageProvider, expiresInSeconds?: number) => Promise<BoothSignedFile[]>;
|
|
43
|
+
|
|
44
|
+
interface BoothExpiryStore extends BoothVaultStore {
|
|
45
|
+
listActiveRecords?(): Promise<BoothUploadRecord[]>;
|
|
46
|
+
updateStatus?(recordId: string, status: BoothUploadRecord['status']): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
interface ExpireResult {
|
|
49
|
+
scanned: number;
|
|
50
|
+
expired: number;
|
|
51
|
+
}
|
|
52
|
+
declare const expireBoothRecords: (store: BoothExpiryStore, now?: number, onExpired?: (record: BoothUploadRecord) => void) => Promise<ExpireResult>;
|
|
53
|
+
|
|
54
|
+
interface BoothRedeemGuardOptions {
|
|
55
|
+
maxAttempts?: number;
|
|
56
|
+
windowMs?: number;
|
|
57
|
+
blockMs?: number;
|
|
58
|
+
}
|
|
59
|
+
declare class BoothRedeemGuard {
|
|
60
|
+
private readonly maxAttempts;
|
|
61
|
+
private readonly windowMs;
|
|
62
|
+
private readonly blockMs;
|
|
63
|
+
private readonly state;
|
|
64
|
+
constructor(options?: BoothRedeemGuardOptions);
|
|
65
|
+
assertAllowed(subjectKey: string, now?: number): void;
|
|
66
|
+
registerAttempt(subjectKey: string, success: boolean, now?: number): void;
|
|
67
|
+
private getState;
|
|
68
|
+
}
|
|
69
|
+
interface ValidateUploadFilesOptions {
|
|
70
|
+
maxFiles?: number;
|
|
71
|
+
maxSingleFileSizeBytes?: number;
|
|
72
|
+
maxTotalSizeBytes?: number;
|
|
73
|
+
allowedExtensions?: string[];
|
|
74
|
+
}
|
|
75
|
+
interface UploadLikeFile {
|
|
76
|
+
fileName: string;
|
|
77
|
+
size: number;
|
|
78
|
+
}
|
|
79
|
+
declare const validateUploadFiles: (files: UploadLikeFile[], options?: ValidateUploadFilesOptions) => void;
|
|
80
|
+
|
|
81
|
+
interface BoothIncomingFile {
|
|
82
|
+
file: File;
|
|
83
|
+
kind?: BoothFileKind;
|
|
84
|
+
}
|
|
85
|
+
interface CreateBoothUploadWithOSSInput {
|
|
86
|
+
boothId: string;
|
|
87
|
+
files: BoothIncomingFile[];
|
|
88
|
+
metadata?: CreateBoothUploadInput['metadata'];
|
|
89
|
+
ttlHours?: number;
|
|
90
|
+
moduleId?: string;
|
|
91
|
+
businessId?: string;
|
|
92
|
+
permission?: AccessPermission;
|
|
93
|
+
onProgress?: (fileName: string, progress: UploadProgress) => void;
|
|
94
|
+
}
|
|
95
|
+
declare const uploadToOSSAndCreateBoothRecord: (params: CreateBoothUploadWithOSSInput, deps: {
|
|
96
|
+
fileService: UniversalFileService;
|
|
97
|
+
vaultService: BoothVaultService;
|
|
98
|
+
}) => Promise<CreateBoothUploadResult>;
|
|
99
|
+
|
|
100
|
+
interface BoothAuditSink {
|
|
101
|
+
log(event: BoothAuditEvent): Promise<void> | void;
|
|
102
|
+
}
|
|
103
|
+
declare class InMemoryBoothAuditSink implements BoothAuditSink {
|
|
104
|
+
private readonly events;
|
|
105
|
+
log(event: BoothAuditEvent): void;
|
|
106
|
+
list(): BoothAuditEvent[];
|
|
107
|
+
}
|
|
108
|
+
declare const createAuditLogger: (sink: BoothAuditSink) => (event: BoothAuditEvent) => void;
|
|
109
|
+
|
|
110
|
+
export { type BoothAuditSink, type BoothExpiryStore, type BoothIncomingFile, type BoothObjectStorageProvider, BoothRedeemGuard, type BoothRedeemGuardOptions, type BoothSignedFile, type BoothVaultRecordRepository, type CreateBoothUploadWithOSSInput, type ExpireResult, InMemoryBoothAuditSink, InMemoryBoothVaultStore, RepositoryBoothVaultStore, type UploadLikeFile, type ValidateUploadFilesOptions, createAuditLogger, expireBoothRecords, signRecordFiles, uploadToOSSAndCreateBoothRecord, validateUploadFiles };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/vocaloidBooth/server/inMemoryBoothVaultStore.ts
|
|
4
|
+
var InMemoryBoothVaultStore = class {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.recordsById = /* @__PURE__ */ new Map();
|
|
7
|
+
this.idByCode = /* @__PURE__ */ new Map();
|
|
8
|
+
}
|
|
9
|
+
async saveRecord(record) {
|
|
10
|
+
this.recordsById.set(record.id, record);
|
|
11
|
+
this.idByCode.set(record.matchCode, record.id);
|
|
12
|
+
}
|
|
13
|
+
async findByMatchCode(matchCode) {
|
|
14
|
+
const id = this.idByCode.get(matchCode);
|
|
15
|
+
if (!id) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return this.recordsById.get(id) ?? null;
|
|
19
|
+
}
|
|
20
|
+
async findByRecordId(recordId) {
|
|
21
|
+
return this.recordsById.get(recordId) ?? null;
|
|
22
|
+
}
|
|
23
|
+
async incrementDownloadCount(recordId) {
|
|
24
|
+
const record = this.recordsById.get(recordId);
|
|
25
|
+
if (!record) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.recordsById.set(recordId, {
|
|
29
|
+
...record,
|
|
30
|
+
downloadCount: record.downloadCount + 1
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async existsByMatchCode(matchCode) {
|
|
34
|
+
return this.idByCode.has(matchCode);
|
|
35
|
+
}
|
|
36
|
+
async listActiveRecords() {
|
|
37
|
+
return Array.from(this.recordsById.values()).filter((record) => record.status === "active");
|
|
38
|
+
}
|
|
39
|
+
async updateStatus(recordId, status) {
|
|
40
|
+
const record = this.recordsById.get(recordId);
|
|
41
|
+
if (!record) return;
|
|
42
|
+
this.recordsById.set(recordId, {
|
|
43
|
+
...record,
|
|
44
|
+
status
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/vocaloidBooth/server/adapters.ts
|
|
50
|
+
var RepositoryBoothVaultStore = class {
|
|
51
|
+
constructor(repository) {
|
|
52
|
+
this.repository = repository;
|
|
53
|
+
}
|
|
54
|
+
saveRecord(record) {
|
|
55
|
+
return this.repository.create(record);
|
|
56
|
+
}
|
|
57
|
+
findByMatchCode(matchCode) {
|
|
58
|
+
return this.repository.findByMatchCode(matchCode);
|
|
59
|
+
}
|
|
60
|
+
async incrementDownloadCount(recordId) {
|
|
61
|
+
const record = await this.findByRecordId(recordId);
|
|
62
|
+
if (!record) return;
|
|
63
|
+
return this.repository.updateDownloadCount(recordId, record.downloadCount + 1);
|
|
64
|
+
}
|
|
65
|
+
existsByMatchCode(matchCode) {
|
|
66
|
+
return this.repository.existsByMatchCode(matchCode);
|
|
67
|
+
}
|
|
68
|
+
async findByRecordId(recordId) {
|
|
69
|
+
if (this.repository.findByRecordId) {
|
|
70
|
+
return this.repository.findByRecordId(recordId);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var signRecordFiles = async (record, storage, expiresInSeconds = 60 * 30) => {
|
|
76
|
+
const signed = await Promise.all(
|
|
77
|
+
record.files.map(async (file) => ({
|
|
78
|
+
...file,
|
|
79
|
+
signedDownloadUrl: await storage.getSignedDownloadUrl(file.objectKey, expiresInSeconds)
|
|
80
|
+
}))
|
|
81
|
+
);
|
|
82
|
+
return signed;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/vocaloidBooth/server/cleanup.ts
|
|
86
|
+
var expireBoothRecords = async (store, now = Date.now(), onExpired) => {
|
|
87
|
+
const records = store.listActiveRecords ? await store.listActiveRecords() : [];
|
|
88
|
+
let expired = 0;
|
|
89
|
+
for (const record of records) {
|
|
90
|
+
if (record.status !== "active") continue;
|
|
91
|
+
if (new Date(record.expiresAt).getTime() <= now) {
|
|
92
|
+
expired += 1;
|
|
93
|
+
if (store.updateStatus) {
|
|
94
|
+
await store.updateStatus(record.id, "expired");
|
|
95
|
+
}
|
|
96
|
+
onExpired?.(record);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
scanned: records.length,
|
|
101
|
+
expired
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/vocaloidBooth/server/security.ts
|
|
106
|
+
var BoothRedeemGuard = class {
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
this.state = /* @__PURE__ */ new Map();
|
|
109
|
+
this.maxAttempts = options.maxAttempts ?? 8;
|
|
110
|
+
this.windowMs = options.windowMs ?? 6e4;
|
|
111
|
+
this.blockMs = options.blockMs ?? 5 * 6e4;
|
|
112
|
+
}
|
|
113
|
+
assertAllowed(subjectKey, now = Date.now()) {
|
|
114
|
+
const state = this.getState(subjectKey, now);
|
|
115
|
+
if (state.blockedUntil && state.blockedUntil > now) {
|
|
116
|
+
const seconds = Math.ceil((state.blockedUntil - now) / 1e3);
|
|
117
|
+
throw new Error(`Too many attempts, retry in ${seconds}s`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
registerAttempt(subjectKey, success, now = Date.now()) {
|
|
121
|
+
const state = this.getState(subjectKey, now);
|
|
122
|
+
if (success) {
|
|
123
|
+
this.state.delete(subjectKey);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
state.attempts.push(now);
|
|
127
|
+
state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);
|
|
128
|
+
if (state.attempts.length >= this.maxAttempts) {
|
|
129
|
+
state.blockedUntil = now + this.blockMs;
|
|
130
|
+
state.attempts = [];
|
|
131
|
+
}
|
|
132
|
+
this.state.set(subjectKey, state);
|
|
133
|
+
}
|
|
134
|
+
getState(subjectKey, now) {
|
|
135
|
+
const state = this.state.get(subjectKey) ?? { attempts: [] };
|
|
136
|
+
state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);
|
|
137
|
+
if (state.blockedUntil && state.blockedUntil <= now) {
|
|
138
|
+
delete state.blockedUntil;
|
|
139
|
+
}
|
|
140
|
+
return state;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var ext = (name) => {
|
|
144
|
+
const i = name.lastIndexOf(".");
|
|
145
|
+
return i >= 0 ? name.slice(i + 1).toLowerCase() : "";
|
|
146
|
+
};
|
|
147
|
+
var validateUploadFiles = (files, options = {}) => {
|
|
148
|
+
const maxFiles = options.maxFiles ?? 20;
|
|
149
|
+
const maxSingle = options.maxSingleFileSizeBytes ?? 2 * 1024 * 1024 * 1024;
|
|
150
|
+
const maxTotal = options.maxTotalSizeBytes ?? 5 * 1024 * 1024 * 1024;
|
|
151
|
+
const allowed = options.allowedExtensions?.map((e) => e.toLowerCase());
|
|
152
|
+
if (files.length === 0) {
|
|
153
|
+
throw new Error("No files uploaded");
|
|
154
|
+
}
|
|
155
|
+
if (files.length > maxFiles) {
|
|
156
|
+
throw new Error(`Too many files (max ${maxFiles})`);
|
|
157
|
+
}
|
|
158
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
159
|
+
if (total > maxTotal) {
|
|
160
|
+
throw new Error("Total upload size exceeded");
|
|
161
|
+
}
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
if (file.size > maxSingle) {
|
|
164
|
+
throw new Error(`File too large: ${file.fileName}`);
|
|
165
|
+
}
|
|
166
|
+
if (allowed && allowed.length > 0 && !allowed.includes(ext(file.fileName))) {
|
|
167
|
+
throw new Error(`File extension not allowed: ${file.fileName}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/vocaloidBooth/server/ossIntegration.ts
|
|
173
|
+
var uploadToOSSAndCreateBoothRecord = async (params, deps) => {
|
|
174
|
+
validateUploadFiles(
|
|
175
|
+
params.files.map((item) => ({ fileName: item.file.name, size: item.file.size })),
|
|
176
|
+
{
|
|
177
|
+
maxFiles: 20,
|
|
178
|
+
maxSingleFileSizeBytes: 2 * 1024 * 1024 * 1024,
|
|
179
|
+
maxTotalSizeBytes: 5 * 1024 * 1024 * 1024
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
const moduleId = params.moduleId ?? "vocaloid-booth";
|
|
183
|
+
const businessId = params.businessId ?? params.boothId;
|
|
184
|
+
const permission = params.permission ?? "private";
|
|
185
|
+
const uploaded = await Promise.all(
|
|
186
|
+
params.files.map(async (item) => {
|
|
187
|
+
const metadata = await deps.fileService.uploadFile(
|
|
188
|
+
{
|
|
189
|
+
file: item.file,
|
|
190
|
+
moduleId,
|
|
191
|
+
businessId,
|
|
192
|
+
permission,
|
|
193
|
+
metadata: {
|
|
194
|
+
boothId: params.boothId,
|
|
195
|
+
kind: item.kind ?? "other"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
void 0,
|
|
199
|
+
(progress) => params.onProgress?.(item.file.name, progress)
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
fileName: metadata.originalName,
|
|
203
|
+
size: metadata.size,
|
|
204
|
+
mimeType: metadata.mimeType,
|
|
205
|
+
checksum: metadata.hash,
|
|
206
|
+
objectKey: metadata.storagePath,
|
|
207
|
+
kind: item.kind ?? "other"
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
return deps.vaultService.createUpload({
|
|
212
|
+
boothId: params.boothId,
|
|
213
|
+
ttlHours: params.ttlHours,
|
|
214
|
+
metadata: params.metadata,
|
|
215
|
+
files: uploaded
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/vocaloidBooth/server/audit.ts
|
|
220
|
+
var InMemoryBoothAuditSink = class {
|
|
221
|
+
constructor() {
|
|
222
|
+
this.events = [];
|
|
223
|
+
}
|
|
224
|
+
log(event) {
|
|
225
|
+
this.events.push(event);
|
|
226
|
+
}
|
|
227
|
+
list() {
|
|
228
|
+
return [...this.events];
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var createAuditLogger = (sink) => {
|
|
232
|
+
return (event) => {
|
|
233
|
+
sink.log(event);
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
exports.BoothRedeemGuard = BoothRedeemGuard;
|
|
238
|
+
exports.InMemoryBoothAuditSink = InMemoryBoothAuditSink;
|
|
239
|
+
exports.InMemoryBoothVaultStore = InMemoryBoothVaultStore;
|
|
240
|
+
exports.RepositoryBoothVaultStore = RepositoryBoothVaultStore;
|
|
241
|
+
exports.createAuditLogger = createAuditLogger;
|
|
242
|
+
exports.expireBoothRecords = expireBoothRecords;
|
|
243
|
+
exports.signRecordFiles = signRecordFiles;
|
|
244
|
+
exports.uploadToOSSAndCreateBoothRecord = uploadToOSSAndCreateBoothRecord;
|
|
245
|
+
exports.validateUploadFiles = validateUploadFiles;
|
|
246
|
+
//# sourceMappingURL=index.js.map
|
|
247
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/vocaloidBooth/server/inMemoryBoothVaultStore.ts","../../../src/vocaloidBooth/server/adapters.ts","../../../src/vocaloidBooth/server/cleanup.ts","../../../src/vocaloidBooth/server/security.ts","../../../src/vocaloidBooth/server/ossIntegration.ts","../../../src/vocaloidBooth/server/audit.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { BoothUploadRecord, BoothVaultStore } from '../types';\n\nexport class InMemoryBoothVaultStore implements BoothVaultStore {\n private readonly recordsById = new Map<string, BoothUploadRecord>();\n private readonly idByCode = new Map<string, string>();\n\n async saveRecord(record: BoothUploadRecord): Promise<void> {\n this.recordsById.set(record.id, record);\n this.idByCode.set(record.matchCode, record.id);\n }\n\n async findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null> {\n const id = this.idByCode.get(matchCode);\n if (!id) {\n return null;\n }\n return this.recordsById.get(id) ?? null;\n }\n\n async findByRecordId(recordId: string): Promise<BoothUploadRecord | null> {\n return this.recordsById.get(recordId) ?? null;\n }\n\n async incrementDownloadCount(recordId: string): Promise<void> {\n const record = this.recordsById.get(recordId);\n if (!record) {\n return;\n }\n\n this.recordsById.set(recordId, {\n ...record,\n downloadCount: record.downloadCount + 1,\n });\n }\n\n async existsByMatchCode(matchCode: string): Promise<boolean> {\n return this.idByCode.has(matchCode);\n }\n\n async listActiveRecords(): Promise<BoothUploadRecord[]> {\n return Array.from(this.recordsById.values()).filter((record) => record.status === 'active');\n }\n\n async updateStatus(recordId: string, status: BoothUploadRecord['status']): Promise<void> {\n const record = this.recordsById.get(recordId);\n if (!record) return;\n\n this.recordsById.set(recordId, {\n ...record,\n status,\n });\n }\n}\n","import type { BoothFileItem, BoothUploadRecord, BoothVaultStore } from '../types';\n\nexport interface BoothVaultRecordRepository {\n create(record: BoothUploadRecord): Promise<void>;\n findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null>;\n findByRecordId?(recordId: string): Promise<BoothUploadRecord | null>;\n updateDownloadCount(recordId: string, downloadCount: number): Promise<void>;\n existsByMatchCode(matchCode: string): Promise<boolean>;\n}\n\nexport interface BoothObjectStorageProvider {\n save(file: File | Blob | Buffer, objectKey: string, contentType?: string): Promise<string>;\n getSignedDownloadUrl(objectKey: string, expiresInSeconds?: number): Promise<string>;\n delete(objectKey: string): Promise<void>;\n}\n\nexport class RepositoryBoothVaultStore implements BoothVaultStore {\n constructor(private readonly repository: BoothVaultRecordRepository) {}\n\n saveRecord(record: BoothUploadRecord): Promise<void> {\n return this.repository.create(record);\n }\n\n findByMatchCode(matchCode: string): Promise<BoothUploadRecord | null> {\n return this.repository.findByMatchCode(matchCode);\n }\n\n async incrementDownloadCount(recordId: string): Promise<void> {\n const record = await this.findByRecordId(recordId);\n if (!record) return;\n return this.repository.updateDownloadCount(recordId, record.downloadCount + 1);\n }\n\n existsByMatchCode(matchCode: string): Promise<boolean> {\n return this.repository.existsByMatchCode(matchCode);\n }\n\n async findByRecordId(recordId: string): Promise<BoothUploadRecord | null> {\n if (this.repository.findByRecordId) {\n return this.repository.findByRecordId(recordId);\n }\n return null;\n }\n}\n\nexport interface BoothSignedFile extends BoothFileItem {\n signedDownloadUrl: string;\n}\n\nexport const signRecordFiles = async (\n record: BoothUploadRecord,\n storage: BoothObjectStorageProvider,\n expiresInSeconds = 60 * 30\n): Promise<BoothSignedFile[]> => {\n const signed = await Promise.all(\n record.files.map(async (file) => ({\n ...file,\n signedDownloadUrl: await storage.getSignedDownloadUrl(file.objectKey, expiresInSeconds),\n }))\n );\n\n return signed;\n};\n","import type { BoothUploadRecord, BoothVaultStore } from '../types';\n\nexport interface BoothExpiryStore extends BoothVaultStore {\n listActiveRecords?(): Promise<BoothUploadRecord[]>;\n updateStatus?(recordId: string, status: BoothUploadRecord['status']): Promise<void>;\n}\n\nexport interface ExpireResult {\n scanned: number;\n expired: number;\n}\n\nexport const expireBoothRecords = async (\n store: BoothExpiryStore,\n now = Date.now(),\n onExpired?: (record: BoothUploadRecord) => void\n): Promise<ExpireResult> => {\n const records = store.listActiveRecords ? await store.listActiveRecords() : [];\n\n let expired = 0;\n for (const record of records) {\n if (record.status !== 'active') continue;\n if (new Date(record.expiresAt).getTime() <= now) {\n expired += 1;\n if (store.updateStatus) {\n // eslint-disable-next-line no-await-in-loop\n await store.updateStatus(record.id, 'expired');\n }\n onExpired?.(record);\n }\n }\n\n return {\n scanned: records.length,\n expired,\n };\n};\n","export interface BoothRedeemGuardOptions {\n maxAttempts?: number;\n windowMs?: number;\n blockMs?: number;\n}\n\ninterface AttemptState {\n attempts: number[];\n blockedUntil?: number;\n}\n\nexport class BoothRedeemGuard {\n private readonly maxAttempts: number;\n private readonly windowMs: number;\n private readonly blockMs: number;\n private readonly state = new Map<string, AttemptState>();\n\n constructor(options: BoothRedeemGuardOptions = {}) {\n this.maxAttempts = options.maxAttempts ?? 8;\n this.windowMs = options.windowMs ?? 60_000;\n this.blockMs = options.blockMs ?? 5 * 60_000;\n }\n\n assertAllowed(subjectKey: string, now = Date.now()): void {\n const state = this.getState(subjectKey, now);\n if (state.blockedUntil && state.blockedUntil > now) {\n const seconds = Math.ceil((state.blockedUntil - now) / 1000);\n throw new Error(`Too many attempts, retry in ${seconds}s`);\n }\n }\n\n registerAttempt(subjectKey: string, success: boolean, now = Date.now()): void {\n const state = this.getState(subjectKey, now);\n\n if (success) {\n this.state.delete(subjectKey);\n return;\n }\n\n state.attempts.push(now);\n state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);\n\n if (state.attempts.length >= this.maxAttempts) {\n state.blockedUntil = now + this.blockMs;\n state.attempts = [];\n }\n\n this.state.set(subjectKey, state);\n }\n\n private getState(subjectKey: string, now: number): AttemptState {\n const state = this.state.get(subjectKey) ?? { attempts: [] };\n state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);\n\n if (state.blockedUntil && state.blockedUntil <= now) {\n delete state.blockedUntil;\n }\n\n return state;\n }\n}\n\nexport interface ValidateUploadFilesOptions {\n maxFiles?: number;\n maxSingleFileSizeBytes?: number;\n maxTotalSizeBytes?: number;\n allowedExtensions?: string[];\n}\n\nexport interface UploadLikeFile {\n fileName: string;\n size: number;\n}\n\nconst ext = (name: string): string => {\n const i = name.lastIndexOf('.');\n return i >= 0 ? name.slice(i + 1).toLowerCase() : '';\n};\n\nexport const validateUploadFiles = (\n files: UploadLikeFile[],\n options: ValidateUploadFilesOptions = {}\n): void => {\n const maxFiles = options.maxFiles ?? 20;\n const maxSingle = options.maxSingleFileSizeBytes ?? 2 * 1024 * 1024 * 1024;\n const maxTotal = options.maxTotalSizeBytes ?? 5 * 1024 * 1024 * 1024;\n const allowed = options.allowedExtensions?.map((e) => e.toLowerCase());\n\n if (files.length === 0) {\n throw new Error('No files uploaded');\n }\n if (files.length > maxFiles) {\n throw new Error(`Too many files (max ${maxFiles})`);\n }\n\n const total = files.reduce((sum, f) => sum + f.size, 0);\n if (total > maxTotal) {\n throw new Error('Total upload size exceeded');\n }\n\n for (const file of files) {\n if (file.size > maxSingle) {\n throw new Error(`File too large: ${file.fileName}`);\n }\n if (allowed && allowed.length > 0 && !allowed.includes(ext(file.fileName))) {\n throw new Error(`File extension not allowed: ${file.fileName}`);\n }\n }\n};\n","import type { UniversalFileService } from '../../universalFile/server';\nimport type { AccessPermission, UploadProgress } from '../../universalFile/types';\nimport type { BoothFileKind, CreateBoothUploadInput, CreateBoothUploadResult } from '../types';\nimport { BoothVaultService } from '../core';\nimport { validateUploadFiles } from './security';\n\nexport interface BoothIncomingFile {\n file: File;\n kind?: BoothFileKind;\n}\n\nexport interface CreateBoothUploadWithOSSInput {\n boothId: string;\n files: BoothIncomingFile[];\n metadata?: CreateBoothUploadInput['metadata'];\n ttlHours?: number;\n moduleId?: string;\n businessId?: string;\n permission?: AccessPermission;\n onProgress?: (fileName: string, progress: UploadProgress) => void;\n}\n\nexport const uploadToOSSAndCreateBoothRecord = async (\n params: CreateBoothUploadWithOSSInput,\n deps: {\n fileService: UniversalFileService;\n vaultService: BoothVaultService;\n }\n): Promise<CreateBoothUploadResult> => {\n validateUploadFiles(\n params.files.map((item) => ({ fileName: item.file.name, size: item.file.size })),\n {\n maxFiles: 20,\n maxSingleFileSizeBytes: 2 * 1024 * 1024 * 1024,\n maxTotalSizeBytes: 5 * 1024 * 1024 * 1024,\n }\n );\n\n const moduleId = params.moduleId ?? 'vocaloid-booth';\n const businessId = params.businessId ?? params.boothId;\n const permission = params.permission ?? 'private';\n\n const uploaded = await Promise.all(\n params.files.map(async (item) => {\n const metadata = await deps.fileService.uploadFile(\n {\n file: item.file,\n moduleId,\n businessId,\n permission,\n metadata: {\n boothId: params.boothId,\n kind: item.kind ?? 'other',\n },\n },\n undefined,\n (progress) => params.onProgress?.(item.file.name, progress)\n );\n\n return {\n fileName: metadata.originalName,\n size: metadata.size,\n mimeType: metadata.mimeType,\n checksum: metadata.hash,\n objectKey: metadata.storagePath,\n kind: item.kind ?? 'other',\n };\n })\n );\n\n return deps.vaultService.createUpload({\n boothId: params.boothId,\n ttlHours: params.ttlHours,\n metadata: params.metadata,\n files: uploaded,\n });\n};\n","import type { BoothAuditEvent } from '../types';\n\nexport interface BoothAuditSink {\n log(event: BoothAuditEvent): Promise<void> | void;\n}\n\nexport class InMemoryBoothAuditSink implements BoothAuditSink {\n private readonly events: BoothAuditEvent[] = [];\n\n log(event: BoothAuditEvent): void {\n this.events.push(event);\n }\n\n list(): BoothAuditEvent[] {\n return [...this.events];\n }\n}\n\nexport const createAuditLogger = (sink: BoothAuditSink) => {\n return (event: BoothAuditEvent): void => {\n sink.log(event);\n };\n};\n"]}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/vocaloidBooth/server/inMemoryBoothVaultStore.ts
|
|
2
|
+
var InMemoryBoothVaultStore = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.recordsById = /* @__PURE__ */ new Map();
|
|
5
|
+
this.idByCode = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
async saveRecord(record) {
|
|
8
|
+
this.recordsById.set(record.id, record);
|
|
9
|
+
this.idByCode.set(record.matchCode, record.id);
|
|
10
|
+
}
|
|
11
|
+
async findByMatchCode(matchCode) {
|
|
12
|
+
const id = this.idByCode.get(matchCode);
|
|
13
|
+
if (!id) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return this.recordsById.get(id) ?? null;
|
|
17
|
+
}
|
|
18
|
+
async findByRecordId(recordId) {
|
|
19
|
+
return this.recordsById.get(recordId) ?? null;
|
|
20
|
+
}
|
|
21
|
+
async incrementDownloadCount(recordId) {
|
|
22
|
+
const record = this.recordsById.get(recordId);
|
|
23
|
+
if (!record) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.recordsById.set(recordId, {
|
|
27
|
+
...record,
|
|
28
|
+
downloadCount: record.downloadCount + 1
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async existsByMatchCode(matchCode) {
|
|
32
|
+
return this.idByCode.has(matchCode);
|
|
33
|
+
}
|
|
34
|
+
async listActiveRecords() {
|
|
35
|
+
return Array.from(this.recordsById.values()).filter((record) => record.status === "active");
|
|
36
|
+
}
|
|
37
|
+
async updateStatus(recordId, status) {
|
|
38
|
+
const record = this.recordsById.get(recordId);
|
|
39
|
+
if (!record) return;
|
|
40
|
+
this.recordsById.set(recordId, {
|
|
41
|
+
...record,
|
|
42
|
+
status
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/vocaloidBooth/server/adapters.ts
|
|
48
|
+
var RepositoryBoothVaultStore = class {
|
|
49
|
+
constructor(repository) {
|
|
50
|
+
this.repository = repository;
|
|
51
|
+
}
|
|
52
|
+
saveRecord(record) {
|
|
53
|
+
return this.repository.create(record);
|
|
54
|
+
}
|
|
55
|
+
findByMatchCode(matchCode) {
|
|
56
|
+
return this.repository.findByMatchCode(matchCode);
|
|
57
|
+
}
|
|
58
|
+
async incrementDownloadCount(recordId) {
|
|
59
|
+
const record = await this.findByRecordId(recordId);
|
|
60
|
+
if (!record) return;
|
|
61
|
+
return this.repository.updateDownloadCount(recordId, record.downloadCount + 1);
|
|
62
|
+
}
|
|
63
|
+
existsByMatchCode(matchCode) {
|
|
64
|
+
return this.repository.existsByMatchCode(matchCode);
|
|
65
|
+
}
|
|
66
|
+
async findByRecordId(recordId) {
|
|
67
|
+
if (this.repository.findByRecordId) {
|
|
68
|
+
return this.repository.findByRecordId(recordId);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var signRecordFiles = async (record, storage, expiresInSeconds = 60 * 30) => {
|
|
74
|
+
const signed = await Promise.all(
|
|
75
|
+
record.files.map(async (file) => ({
|
|
76
|
+
...file,
|
|
77
|
+
signedDownloadUrl: await storage.getSignedDownloadUrl(file.objectKey, expiresInSeconds)
|
|
78
|
+
}))
|
|
79
|
+
);
|
|
80
|
+
return signed;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/vocaloidBooth/server/cleanup.ts
|
|
84
|
+
var expireBoothRecords = async (store, now = Date.now(), onExpired) => {
|
|
85
|
+
const records = store.listActiveRecords ? await store.listActiveRecords() : [];
|
|
86
|
+
let expired = 0;
|
|
87
|
+
for (const record of records) {
|
|
88
|
+
if (record.status !== "active") continue;
|
|
89
|
+
if (new Date(record.expiresAt).getTime() <= now) {
|
|
90
|
+
expired += 1;
|
|
91
|
+
if (store.updateStatus) {
|
|
92
|
+
await store.updateStatus(record.id, "expired");
|
|
93
|
+
}
|
|
94
|
+
onExpired?.(record);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
scanned: records.length,
|
|
99
|
+
expired
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/vocaloidBooth/server/security.ts
|
|
104
|
+
var BoothRedeemGuard = class {
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
this.state = /* @__PURE__ */ new Map();
|
|
107
|
+
this.maxAttempts = options.maxAttempts ?? 8;
|
|
108
|
+
this.windowMs = options.windowMs ?? 6e4;
|
|
109
|
+
this.blockMs = options.blockMs ?? 5 * 6e4;
|
|
110
|
+
}
|
|
111
|
+
assertAllowed(subjectKey, now = Date.now()) {
|
|
112
|
+
const state = this.getState(subjectKey, now);
|
|
113
|
+
if (state.blockedUntil && state.blockedUntil > now) {
|
|
114
|
+
const seconds = Math.ceil((state.blockedUntil - now) / 1e3);
|
|
115
|
+
throw new Error(`Too many attempts, retry in ${seconds}s`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
registerAttempt(subjectKey, success, now = Date.now()) {
|
|
119
|
+
const state = this.getState(subjectKey, now);
|
|
120
|
+
if (success) {
|
|
121
|
+
this.state.delete(subjectKey);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
state.attempts.push(now);
|
|
125
|
+
state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);
|
|
126
|
+
if (state.attempts.length >= this.maxAttempts) {
|
|
127
|
+
state.blockedUntil = now + this.blockMs;
|
|
128
|
+
state.attempts = [];
|
|
129
|
+
}
|
|
130
|
+
this.state.set(subjectKey, state);
|
|
131
|
+
}
|
|
132
|
+
getState(subjectKey, now) {
|
|
133
|
+
const state = this.state.get(subjectKey) ?? { attempts: [] };
|
|
134
|
+
state.attempts = state.attempts.filter((t) => now - t <= this.windowMs);
|
|
135
|
+
if (state.blockedUntil && state.blockedUntil <= now) {
|
|
136
|
+
delete state.blockedUntil;
|
|
137
|
+
}
|
|
138
|
+
return state;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var ext = (name) => {
|
|
142
|
+
const i = name.lastIndexOf(".");
|
|
143
|
+
return i >= 0 ? name.slice(i + 1).toLowerCase() : "";
|
|
144
|
+
};
|
|
145
|
+
var validateUploadFiles = (files, options = {}) => {
|
|
146
|
+
const maxFiles = options.maxFiles ?? 20;
|
|
147
|
+
const maxSingle = options.maxSingleFileSizeBytes ?? 2 * 1024 * 1024 * 1024;
|
|
148
|
+
const maxTotal = options.maxTotalSizeBytes ?? 5 * 1024 * 1024 * 1024;
|
|
149
|
+
const allowed = options.allowedExtensions?.map((e) => e.toLowerCase());
|
|
150
|
+
if (files.length === 0) {
|
|
151
|
+
throw new Error("No files uploaded");
|
|
152
|
+
}
|
|
153
|
+
if (files.length > maxFiles) {
|
|
154
|
+
throw new Error(`Too many files (max ${maxFiles})`);
|
|
155
|
+
}
|
|
156
|
+
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
157
|
+
if (total > maxTotal) {
|
|
158
|
+
throw new Error("Total upload size exceeded");
|
|
159
|
+
}
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
if (file.size > maxSingle) {
|
|
162
|
+
throw new Error(`File too large: ${file.fileName}`);
|
|
163
|
+
}
|
|
164
|
+
if (allowed && allowed.length > 0 && !allowed.includes(ext(file.fileName))) {
|
|
165
|
+
throw new Error(`File extension not allowed: ${file.fileName}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/vocaloidBooth/server/ossIntegration.ts
|
|
171
|
+
var uploadToOSSAndCreateBoothRecord = async (params, deps) => {
|
|
172
|
+
validateUploadFiles(
|
|
173
|
+
params.files.map((item) => ({ fileName: item.file.name, size: item.file.size })),
|
|
174
|
+
{
|
|
175
|
+
maxFiles: 20,
|
|
176
|
+
maxSingleFileSizeBytes: 2 * 1024 * 1024 * 1024,
|
|
177
|
+
maxTotalSizeBytes: 5 * 1024 * 1024 * 1024
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
const moduleId = params.moduleId ?? "vocaloid-booth";
|
|
181
|
+
const businessId = params.businessId ?? params.boothId;
|
|
182
|
+
const permission = params.permission ?? "private";
|
|
183
|
+
const uploaded = await Promise.all(
|
|
184
|
+
params.files.map(async (item) => {
|
|
185
|
+
const metadata = await deps.fileService.uploadFile(
|
|
186
|
+
{
|
|
187
|
+
file: item.file,
|
|
188
|
+
moduleId,
|
|
189
|
+
businessId,
|
|
190
|
+
permission,
|
|
191
|
+
metadata: {
|
|
192
|
+
boothId: params.boothId,
|
|
193
|
+
kind: item.kind ?? "other"
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
void 0,
|
|
197
|
+
(progress) => params.onProgress?.(item.file.name, progress)
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
fileName: metadata.originalName,
|
|
201
|
+
size: metadata.size,
|
|
202
|
+
mimeType: metadata.mimeType,
|
|
203
|
+
checksum: metadata.hash,
|
|
204
|
+
objectKey: metadata.storagePath,
|
|
205
|
+
kind: item.kind ?? "other"
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
return deps.vaultService.createUpload({
|
|
210
|
+
boothId: params.boothId,
|
|
211
|
+
ttlHours: params.ttlHours,
|
|
212
|
+
metadata: params.metadata,
|
|
213
|
+
files: uploaded
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// src/vocaloidBooth/server/audit.ts
|
|
218
|
+
var InMemoryBoothAuditSink = class {
|
|
219
|
+
constructor() {
|
|
220
|
+
this.events = [];
|
|
221
|
+
}
|
|
222
|
+
log(event) {
|
|
223
|
+
this.events.push(event);
|
|
224
|
+
}
|
|
225
|
+
list() {
|
|
226
|
+
return [...this.events];
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var createAuditLogger = (sink) => {
|
|
230
|
+
return (event) => {
|
|
231
|
+
sink.log(event);
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export { BoothRedeemGuard, InMemoryBoothAuditSink, InMemoryBoothVaultStore, RepositoryBoothVaultStore, createAuditLogger, expireBoothRecords, signRecordFiles, uploadToOSSAndCreateBoothRecord, validateUploadFiles };
|
|
236
|
+
//# sourceMappingURL=index.mjs.map
|
|
237
|
+
//# sourceMappingURL=index.mjs.map
|