feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
import type {
|
||||
PresignedTransferBinding,
|
||||
PresignedTransferCapability,
|
||||
PresignedTransferCapabilityReceipt,
|
||||
PresignedTransferMethod,
|
||||
PresignedTransferReplayGuard,
|
||||
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
|
||||
import type { BrowserDataResult } from "../../../application/ports/browser-file-storage/shared.ts";
|
||||
import {
|
||||
browserDataFailure,
|
||||
browserDataSuccess,
|
||||
} from "../../browser-file-storage/result.ts";
|
||||
|
||||
export type PresignedHeaderBinding = Readonly<{
|
||||
name: string;
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
export type PresignedCapabilityRegistration = Readonly<{
|
||||
capabilityReceipt: PresignedTransferCapabilityReceipt;
|
||||
method: PresignedTransferMethod;
|
||||
binding: PresignedTransferBinding;
|
||||
href: string;
|
||||
origin: string;
|
||||
path: string;
|
||||
allowedQueryParameters: readonly string[];
|
||||
requestHeaders: readonly PresignedHeaderBinding[];
|
||||
requiredResponseHeaders: readonly PresignedHeaderBinding[];
|
||||
digestRequestHeader: string | null;
|
||||
digestResponseHeader: string | null;
|
||||
receiptResponseHeader: string | null;
|
||||
expectedStatus: number;
|
||||
expectedResponseByteLength: number | null;
|
||||
mediaType: string;
|
||||
byteLength: number;
|
||||
maxBytes: number;
|
||||
expectedSha256: string;
|
||||
expiresAtEpochMs: number;
|
||||
}>;
|
||||
|
||||
export type PresignedCapabilityBinding = Readonly<
|
||||
PresignedCapabilityRegistration & {
|
||||
capability: PresignedTransferCapability;
|
||||
}
|
||||
>;
|
||||
|
||||
export interface PresignedCapabilityVault {
|
||||
register(
|
||||
registration: PresignedCapabilityRegistration,
|
||||
): BrowserDataResult<PresignedTransferCapability>;
|
||||
resolve(
|
||||
capability: PresignedTransferCapability,
|
||||
): BrowserDataResult<PresignedCapabilityBinding>;
|
||||
/**
|
||||
* Atomically retires an exact identity after its single-use replay claim.
|
||||
* The caller may keep the already-resolved binding on its stack for the
|
||||
* in-flight request, but the vault must no longer retain or resolve it.
|
||||
*/
|
||||
consume(
|
||||
capability: PresignedTransferCapability,
|
||||
): BrowserDataResult<true>;
|
||||
/**
|
||||
* Best-effort, idempotent retirement for an unused or abandoned identity.
|
||||
*/
|
||||
revoke(capability: PresignedTransferCapability): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function createPresignedCapabilityVault(options: Readonly<{
|
||||
maxActiveCapabilities: number;
|
||||
now?: () => number;
|
||||
}>): PresignedCapabilityVault {
|
||||
if (
|
||||
!Number.isSafeInteger(options.maxActiveCapabilities) ||
|
||||
options.maxActiveCapabilities < 1
|
||||
) {
|
||||
throw new TypeError("Presigned capability vault limit is invalid.");
|
||||
}
|
||||
const maxActiveCapabilities = options.maxActiveCapabilities;
|
||||
const now = options.now ?? Date.now;
|
||||
const byIdentity =
|
||||
new WeakMap<PresignedTransferCapability, PresignedCapabilityBinding>();
|
||||
const byReceipt =
|
||||
new Map<PresignedTransferCapabilityReceipt, PresignedTransferCapability>();
|
||||
let disposed = false;
|
||||
|
||||
function pruneExpired(): void {
|
||||
const current = now();
|
||||
if (!Number.isSafeInteger(current)) return;
|
||||
for (const [receipt, capability] of byReceipt) {
|
||||
if (capability.expiresAtEpochMs <= current) {
|
||||
byReceipt.delete(receipt);
|
||||
byIdentity.delete(capability);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function revoke(capability: PresignedTransferCapability): boolean {
|
||||
try {
|
||||
const binding = byIdentity.get(capability);
|
||||
if (!binding) return false;
|
||||
byIdentity.delete(capability);
|
||||
if (byReceipt.get(binding.capabilityReceipt) === capability) {
|
||||
byReceipt.delete(binding.capabilityReceipt);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
register(
|
||||
registration: PresignedCapabilityRegistration,
|
||||
): BrowserDataResult<PresignedTransferCapability> {
|
||||
if (disposed) {
|
||||
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
pruneExpired();
|
||||
if (
|
||||
byReceipt.has(registration.capabilityReceipt) ||
|
||||
byReceipt.size >= maxActiveCapabilities
|
||||
) {
|
||||
return browserDataFailure(
|
||||
byReceipt.has(registration.capabilityReceipt)
|
||||
? "CONFLICT"
|
||||
: "LIMIT_EXCEEDED",
|
||||
"PRESIGNED_TRANSFER",
|
||||
byReceipt.has(registration.capabilityReceipt)
|
||||
? { recovery: "REISSUE_CAPABILITY" }
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const capability = Object.freeze({
|
||||
capabilityReceipt: registration.capabilityReceipt,
|
||||
method: registration.method,
|
||||
binding: freezeBinding(registration.binding),
|
||||
mediaType: registration.mediaType,
|
||||
byteLength: registration.byteLength,
|
||||
maxBytes: registration.maxBytes,
|
||||
expectedSha256: registration.expectedSha256,
|
||||
expiresAtEpochMs: registration.expiresAtEpochMs,
|
||||
}) as PresignedTransferCapability;
|
||||
const binding: PresignedCapabilityBinding = Object.freeze({
|
||||
capability,
|
||||
capabilityReceipt: capability.capabilityReceipt,
|
||||
method: capability.method,
|
||||
binding: capability.binding,
|
||||
href: registration.href,
|
||||
origin: registration.origin,
|
||||
path: registration.path,
|
||||
allowedQueryParameters: Object.freeze([
|
||||
...registration.allowedQueryParameters,
|
||||
]),
|
||||
requestHeaders: freezeHeaders(registration.requestHeaders),
|
||||
requiredResponseHeaders: freezeHeaders(
|
||||
registration.requiredResponseHeaders,
|
||||
),
|
||||
digestRequestHeader: registration.digestRequestHeader,
|
||||
digestResponseHeader: registration.digestResponseHeader,
|
||||
receiptResponseHeader: registration.receiptResponseHeader,
|
||||
expectedStatus: registration.expectedStatus,
|
||||
expectedResponseByteLength:
|
||||
registration.expectedResponseByteLength,
|
||||
mediaType: capability.mediaType,
|
||||
byteLength: capability.byteLength,
|
||||
maxBytes: capability.maxBytes,
|
||||
expectedSha256: capability.expectedSha256,
|
||||
expiresAtEpochMs: capability.expiresAtEpochMs,
|
||||
});
|
||||
byIdentity.set(capability, binding);
|
||||
byReceipt.set(capability.capabilityReceipt, capability);
|
||||
return browserDataSuccess(capability);
|
||||
},
|
||||
|
||||
resolve(
|
||||
capability: PresignedTransferCapability,
|
||||
): BrowserDataResult<PresignedCapabilityBinding> {
|
||||
if (disposed) {
|
||||
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
try {
|
||||
const binding = byIdentity.get(capability);
|
||||
return binding
|
||||
? browserDataSuccess(binding)
|
||||
: browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
|
||||
} catch {
|
||||
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
},
|
||||
|
||||
consume(
|
||||
capability: PresignedTransferCapability,
|
||||
): BrowserDataResult<true> {
|
||||
if (disposed) {
|
||||
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
return revoke(capability)
|
||||
? browserDataSuccess(true as const)
|
||||
: browserDataFailure(
|
||||
"POLICY_REJECTED",
|
||||
"PRESIGNED_TRANSFER",
|
||||
);
|
||||
},
|
||||
|
||||
revoke(capability: PresignedTransferCapability): void {
|
||||
if (disposed) return;
|
||||
revoke(capability);
|
||||
},
|
||||
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
for (const capability of byReceipt.values()) {
|
||||
byIdentity.delete(capability);
|
||||
}
|
||||
byReceipt.clear();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createSingleUsePresignedReplayGuard():
|
||||
PresignedTransferReplayGuard {
|
||||
const claimed = new WeakSet<PresignedTransferCapability>();
|
||||
return Object.freeze({
|
||||
claim(
|
||||
capability: PresignedTransferCapability,
|
||||
): BrowserDataResult<true> {
|
||||
try {
|
||||
if (claimed.has(capability)) {
|
||||
return browserDataFailure("CONFLICT", "PRESIGNED_TRANSFER", {
|
||||
recovery: "REISSUE_CAPABILITY",
|
||||
});
|
||||
}
|
||||
claimed.add(capability);
|
||||
return browserDataSuccess(true as const);
|
||||
} catch {
|
||||
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function freezeBinding(
|
||||
binding: PresignedTransferBinding,
|
||||
): PresignedTransferBinding {
|
||||
return binding.kind === "DOWNLOAD"
|
||||
? Object.freeze({
|
||||
kind: "DOWNLOAD" as const,
|
||||
resourceId: binding.resourceId,
|
||||
})
|
||||
: Object.freeze({
|
||||
kind: "UPLOAD_PART" as const,
|
||||
protocol: binding.protocol,
|
||||
sessionId: binding.sessionId,
|
||||
requestBindingSha256: binding.requestBindingSha256,
|
||||
uploadBindingSha256: binding.uploadBindingSha256,
|
||||
partNumber: binding.partNumber,
|
||||
offset: binding.offset,
|
||||
idempotencyKey: binding.idempotencyKey,
|
||||
});
|
||||
}
|
||||
|
||||
function freezeHeaders(
|
||||
headers: readonly PresignedHeaderBinding[],
|
||||
): readonly PresignedHeaderBinding[] {
|
||||
return Object.freeze(
|
||||
headers.map((header) =>
|
||||
Object.freeze({ name: header.name, value: header.value }),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user