Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/web/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1256,15 +1256,28 @@ function encodeUploadMetadata(metadata) {
/**
* 生成 Idempotency-Key (epoch_ms-UUID)
*/
function uuidFromRandomValues(cryptoObj) {
if (!cryptoObj || typeof cryptoObj.getRandomValues !== 'function') {
throw new Error('no Web Crypto random source available in this environment');
}
const bytes = cryptoObj.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

function generateUploadIdempotencyKey() {
const epoch = Date.now();
const cryptoObj = typeof crypto !== 'undefined'
? crypto
: (typeof globalThis !== 'undefined' && globalThis.crypto ? globalThis.crypto : null);
if (!cryptoObj || typeof cryptoObj.randomUUID !== 'function') {
throw new Error('crypto.randomUUID is not supported in this environment');
let uuid = null;
if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
uuid = cryptoObj.randomUUID();
} else {
uuid = uuidFromRandomValues(cryptoObj);
}
const uuid = cryptoObj.randomUUID();
return `${epoch}-${uuid}`;
}

Expand Down
17 changes: 17 additions & 0 deletions src/web/tests/local-upload.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,23 @@ describe('local upload frontend logic', () => {
expect(key).toMatch(/^\d+-[0-9a-fA-F-]+$/);
});

it('still generates epoch_ms-UUID keys when crypto.randomUUID is unavailable (plain HTTP origin)', async () => {
await installIndexPage();
const { window } = dom;

const originalCrypto = window.crypto;
Object.defineProperty(window, 'crypto', {
configurable: true,
value: { getRandomValues: (array) => originalCrypto.getRandomValues(array) },
});
try {
const key = window.generateUploadIdempotencyKey();
expect(key).toMatch(/^\d+-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
} finally {
Object.defineProperty(window, 'crypto', { configurable: true, value: originalCrypto });
}
});

it('switches between URL mode and Upload mode without breaking URL transcription', async () => {
await installIndexPage();
const { document } = dom.window;
Expand Down
Loading