dokkebi-guard-upload 0.1.0

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/index.js ADDED
@@ -0,0 +1,453 @@
1
+ const O = "AdamCodd/vit-base-nsfw-detector", A = "q4", L = [
2
+ "nsfw",
3
+ "porn",
4
+ "pornography",
5
+ "explicit",
6
+ "hentai",
7
+ "sexy",
8
+ "nude",
9
+ "nudity"
10
+ ], B = 0.75;
11
+ class U extends Error {
12
+ buildId;
13
+ expectedSha256;
14
+ actualSha256;
15
+ constructor(e, t, n, a) {
16
+ super(e), this.name = "GuardIntegrityError", this.expectedSha256 = t, this.actualSha256 = n, this.buildId = a;
17
+ }
18
+ }
19
+ class y extends Error {
20
+ code;
21
+ scan;
22
+ constructor(e, t, n) {
23
+ super(e), this.name = "GuardProofError", this.code = t, this.scan = n;
24
+ }
25
+ }
26
+ async function b(r) {
27
+ const e = r instanceof Uint8Array ? r : new Uint8Array(r), t = await crypto.subtle.digest("SHA-256", e);
28
+ return [...new Uint8Array(t)].map((n) => n.toString(16).padStart(2, "0")).join("");
29
+ }
30
+ async function D(r) {
31
+ const t = await r.slice(0, 65536).arrayBuffer(), n = `${r.size}|${r.type || "application/octet-stream"}|`, a = new TextEncoder().encode(n), s = new Uint8Array(a.length + t.byteLength);
32
+ return s.set(a, 0), s.set(new Uint8Array(t), a.length), b(s);
33
+ }
34
+ async function v(r, e, t, n) {
35
+ const a = await D(r), s = {
36
+ buildId: t.buildId,
37
+ workerSha256: n,
38
+ fileFingerprint: a,
39
+ nsfwScore: e.nsfwScore,
40
+ pass: e.pass,
41
+ labels: e.labels,
42
+ durationMs: e.durationMs
43
+ }, o = await fetch(t.proofEndpoint, {
44
+ method: "POST",
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ ...t.proofHeaders
48
+ },
49
+ credentials: t.proofCredentials ?? "same-origin",
50
+ body: JSON.stringify(s)
51
+ }), c = await o.text();
52
+ let i;
53
+ try {
54
+ i = JSON.parse(c);
55
+ } catch {
56
+ throw new y(
57
+ `proof API 응답 파싱 실패 (HTTP ${o.status})`,
58
+ "PROOF_PARSE_ERROR",
59
+ e
60
+ );
61
+ }
62
+ if (!o.ok) {
63
+ const d = typeof i.code == "string" ? i.code : "PROOF_DENIED", l = typeof i.error == "string" ? i.error : `proof 거부 (HTTP ${o.status})`;
64
+ throw new y(l, d, e);
65
+ }
66
+ const u = i.token, h = i.exp;
67
+ if (typeof u != "string" || typeof h != "number")
68
+ throw new y("proof API 응답에 token/exp 없음", "PROOF_INVALID_RESPONSE", e);
69
+ return { token: u, exp: h };
70
+ }
71
+ async function _(r, e) {
72
+ if (r.reportEndpoint)
73
+ try {
74
+ await fetch(r.reportEndpoint, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json", ...r.proofHeaders },
77
+ credentials: r.proofCredentials ?? "same-origin",
78
+ body: JSON.stringify({
79
+ buildId: r.buildId,
80
+ event: "worker_integrity_mismatch",
81
+ ...e,
82
+ ts: (/* @__PURE__ */ new Date()).toISOString()
83
+ })
84
+ });
85
+ } catch {
86
+ }
87
+ }
88
+ async function x(r) {
89
+ const e = typeof r == "string" ? r : r.href, t = await fetch(e, { cache: "no-store" });
90
+ if (!t.ok)
91
+ throw new Error(`Worker 스크립트를 가져올 수 없습니다. HTTP ${t.status} (${e})`);
92
+ const n = await t.arrayBuffer();
93
+ return b(n);
94
+ }
95
+ async function C(r, e) {
96
+ const t = await x(r), n = e.toLowerCase();
97
+ return t.toLowerCase() === n ? { ok: !0, actualSha256: t } : { ok: !1, actualSha256: t, expectedSha256: n };
98
+ }
99
+ async function M(r, e = 512) {
100
+ const t = await createImageBitmap(r);
101
+ try {
102
+ let n = t.width, a = t.height;
103
+ const s = Math.min(1, e / Math.max(n, a));
104
+ s < 1 && (n = Math.max(1, Math.round(n * s)), a = Math.max(1, Math.round(a * s)));
105
+ const o = typeof OffscreenCanvas < "u" ? new OffscreenCanvas(n, a) : document.createElement("canvas");
106
+ o instanceof OffscreenCanvas, o.width = n, o.height = a;
107
+ const c = o.getContext("2d");
108
+ if (!c) throw new Error("2D canvas context unavailable");
109
+ return c.drawImage(t, 0, 0, n, a), { pixels: c.getImageData(0, 0, n, a).data, width: n, height: a };
110
+ } finally {
111
+ t.close?.();
112
+ }
113
+ }
114
+ function W(r) {
115
+ if (r.type) return r.type.startsWith("image/");
116
+ if (r instanceof File) {
117
+ const e = r.name.split(".").pop()?.toLowerCase() ?? "";
118
+ return ["jpg", "jpeg", "png", "webp", "gif", "bmp", "avif"].includes(e);
119
+ }
120
+ return !1;
121
+ }
122
+ function H(r, e = "upload.jpg") {
123
+ return r instanceof File && r.name ? r.name : `upload.${r.type?.split("/")[1]?.replace("jpeg", "jpg") ?? "jpg"}`;
124
+ }
125
+ function S() {
126
+ return null;
127
+ }
128
+ class F {
129
+ worker = null;
130
+ loadPromise = null;
131
+ pendingScan = null;
132
+ _ready = !1;
133
+ integrityChecked = !1;
134
+ /** 검증된 worker SHA-256 (proof 바인딩) */
135
+ verifiedWorkerSha256 = "";
136
+ modelId;
137
+ modelDtype;
138
+ nsfwThreshold;
139
+ blockLabels;
140
+ maxImageSide;
141
+ onnxWasmPaths;
142
+ workerUrl;
143
+ security;
144
+ onLoadProgress;
145
+ onScanStart;
146
+ onScanComplete;
147
+ constructor(e = {}) {
148
+ this.modelId = e.modelId ?? O, this.modelDtype = e.modelDtype ?? A, this.nsfwThreshold = e.nsfwThreshold ?? 0.75, this.blockLabels = e.blockLabels ?? L, this.maxImageSide = e.maxImageSide ?? 512, this.onnxWasmPaths = e.onnxWasmPaths, this.workerUrl = e.workerUrl, this.security = e.security, this.onLoadProgress = e.onLoadProgress, this.onScanStart = e.onScanStart, this.onScanComplete = e.onScanComplete;
149
+ }
150
+ get ready() {
151
+ return this._ready;
152
+ }
153
+ getWorkerUrl() {
154
+ const e = this.workerUrl ?? S();
155
+ if (!e) throw new Error("workerUrl이 설정되지 않았습니다.");
156
+ return e;
157
+ }
158
+ getSecurity() {
159
+ return this.security;
160
+ }
161
+ async load() {
162
+ if (!this._ready)
163
+ return this.loadPromise ? this.loadPromise : (this.loadPromise = (async () => {
164
+ await this.ensureIntegrity(), this.ensureWorker(), await new Promise((e, t) => {
165
+ const n = (s) => {
166
+ const o = s.data;
167
+ switch (o.type) {
168
+ case "progress":
169
+ this.onLoadProgress?.(o.progress, {
170
+ status: "progress",
171
+ loaded: o.loaded,
172
+ total: o.total,
173
+ file: o.file
174
+ });
175
+ break;
176
+ case "ready":
177
+ this._ready = !0, this.worker?.removeEventListener("message", n), e();
178
+ break;
179
+ case "error":
180
+ this.worker?.removeEventListener("message", n), t(new Error(o.message));
181
+ break;
182
+ }
183
+ };
184
+ this.worker.addEventListener("message", n), this.worker.addEventListener("error", (s) => {
185
+ t(new Error(s.message || "Worker error"));
186
+ });
187
+ const a = {
188
+ type: "load",
189
+ data: {
190
+ modelId: this.modelId,
191
+ modelDtype: this.modelDtype,
192
+ onnxWasmPaths: this.onnxWasmPaths,
193
+ nsfwThreshold: this.nsfwThreshold,
194
+ blockLabels: this.blockLabels
195
+ }
196
+ };
197
+ this.worker.postMessage(a);
198
+ });
199
+ })().finally(() => {
200
+ this.loadPromise = null;
201
+ }), this.loadPromise);
202
+ }
203
+ async scan(e) {
204
+ if (!W(e))
205
+ throw new Error("이미지 파일만 검열할 수 있습니다.");
206
+ await this.load(), this.onScanStart?.();
207
+ const { pixels: t, width: n, height: a } = await M(e, this.maxImageSide), s = performance.now();
208
+ return new Promise((o, c) => {
209
+ if (this.pendingScan) {
210
+ c(new Error("이미 스캔이 진행 중입니다."));
211
+ return;
212
+ }
213
+ this.pendingScan = { resolve: o, reject: c, startedAt: s }, this.ensureWorker();
214
+ const i = (h) => {
215
+ const d = h.data;
216
+ if (d.type !== "result" && d.type !== "error") return;
217
+ this.worker?.removeEventListener("message", i);
218
+ const l = this.pendingScan;
219
+ if (this.pendingScan = null, !l) return;
220
+ const g = Math.round(performance.now() - l.startedAt);
221
+ if (d.type === "error") {
222
+ l.reject(new Error(d.message));
223
+ return;
224
+ }
225
+ const f = {
226
+ pass: d.pass,
227
+ nsfwScore: d.nsfwScore,
228
+ labels: d.labels,
229
+ reason: d.reason,
230
+ durationMs: g
231
+ };
232
+ this.onScanComplete?.(f), l.resolve(f);
233
+ };
234
+ this.worker.addEventListener("message", i);
235
+ const u = t.buffer.slice(0);
236
+ this.worker.postMessage(
237
+ {
238
+ type: "scan",
239
+ data: { pixels: u, width: n, height: a }
240
+ },
241
+ [u]
242
+ );
243
+ });
244
+ }
245
+ dispose() {
246
+ this.pendingScan?.reject(new Error("Scanner disposed")), this.pendingScan = null, this.worker?.terminate(), this.worker = null, this._ready = !1, this.loadPromise = null, this.integrityChecked = !1, this.verifiedWorkerSha256 = "";
247
+ }
248
+ async ensureIntegrity() {
249
+ if (this.integrityChecked) return;
250
+ const e = this.security, t = this.getWorkerUrl(), n = await x(t);
251
+ if (this.verifiedWorkerSha256 = n, !(e?.verifyWorker !== !1 && !!e?.workerSha256)) {
252
+ this.integrityChecked = !0;
253
+ return;
254
+ }
255
+ const s = await C(t, e.workerSha256);
256
+ if (!s.ok)
257
+ throw await _(e, {
258
+ expectedSha256: s.expectedSha256,
259
+ actualSha256: s.actualSha256,
260
+ workerUrl: typeof t == "string" ? t : t.href
261
+ }), new U(
262
+ "검열 모듈 무결성 검증 실패 — 변조 가능성이 있습니다.",
263
+ s.expectedSha256,
264
+ s.actualSha256,
265
+ e.buildId
266
+ );
267
+ this.integrityChecked = !0;
268
+ }
269
+ ensureWorker() {
270
+ if (this.worker) return;
271
+ const e = this.workerUrl ?? S();
272
+ if (!e)
273
+ throw new Error(
274
+ 'workerUrl이 필요합니다. Vite: import workerUrl from "dokkebi-guard-upload/worker?worker&url"'
275
+ );
276
+ this.worker = new Worker(e, { type: "module" });
277
+ }
278
+ }
279
+ class N {
280
+ scanner;
281
+ constructor(e = {}) {
282
+ this.scanner = new F(e);
283
+ }
284
+ get ready() {
285
+ return this.scanner.ready;
286
+ }
287
+ load() {
288
+ return this.scanner.load();
289
+ }
290
+ scan(e) {
291
+ return this.scanner.scan(e);
292
+ }
293
+ async upload(e, t) {
294
+ const n = await this.scanner.scan(e);
295
+ if (!n.pass)
296
+ throw new R(n.reason ?? "업로드가 차단되었습니다.", n);
297
+ const a = this.scanner.getSecurity();
298
+ let s;
299
+ if (a && !t.skipProof) {
300
+ const p = this.scanner.verifiedWorkerSha256;
301
+ if (!p)
302
+ throw new y("Worker 해시 미확인", "WORKER_HASH_MISSING", n);
303
+ s = (await v(e, n, a, p)).token;
304
+ }
305
+ const o = t.fieldName ?? "file", c = t.urlKey ?? "url", i = H(e), u = t.guardTokenHeader ?? "X-Guard-Token", h = new FormData();
306
+ if (h.append(o, e, i), t.extraFields)
307
+ for (const [p, k] of Object.entries(t.extraFields))
308
+ h.append(p, k);
309
+ const d = { ...t.headers ?? {} };
310
+ s && (d[u] = s);
311
+ const l = await fetch(t.endpoint, {
312
+ method: "POST",
313
+ headers: d,
314
+ body: h,
315
+ credentials: t.credentials
316
+ }), g = await l.text();
317
+ let f;
318
+ try {
319
+ f = JSON.parse(g);
320
+ } catch {
321
+ throw new E(
322
+ `업로드 응답을 해석할 수 없습니다. HTTP ${l.status}`,
323
+ l.status,
324
+ g,
325
+ n
326
+ );
327
+ }
328
+ const w = f, m = w[c];
329
+ if (!l.ok || typeof m != "string" || !m) {
330
+ const p = typeof w.error == "string" && w.error || typeof w.message == "string" && w.message || `업로드 실패 (HTTP ${l.status})`;
331
+ throw new E(p, l.status, f, n);
332
+ }
333
+ return { url: m, scan: n, raw: f, guardToken: s };
334
+ }
335
+ dispose() {
336
+ this.scanner.dispose();
337
+ }
338
+ }
339
+ class R extends Error {
340
+ scan;
341
+ constructor(e, t) {
342
+ super(e), this.name = "GuardUploadBlockedError", this.scan = t;
343
+ }
344
+ }
345
+ class E extends Error {
346
+ status;
347
+ body;
348
+ scan;
349
+ constructor(e, t, n, a) {
350
+ super(e), this.name = "GuardUploadHttpError", this.status = t, this.body = n, this.scan = a;
351
+ }
352
+ }
353
+ function K(r) {
354
+ return new N(r);
355
+ }
356
+ async function j(r = "/guard-manifest.json") {
357
+ const e = await fetch(r, { cache: "no-store" });
358
+ if (!e.ok) throw new Error(`guard manifest 로드 실패: HTTP ${e.status}`);
359
+ const t = await e.json();
360
+ if (!t.buildId || !t.workerSha256)
361
+ throw new Error("guard manifest 형식 오류 (buildId, workerSha256 필요)");
362
+ return t;
363
+ }
364
+ function G(r, e) {
365
+ return {
366
+ buildId: r.buildId,
367
+ workerSha256: r.workerSha256,
368
+ proofEndpoint: e.proofEndpoint,
369
+ reportEndpoint: e.reportEndpoint,
370
+ verifyWorker: !0
371
+ };
372
+ }
373
+ async function J(r, e, t) {
374
+ const n = await j(r);
375
+ return {
376
+ ...t,
377
+ security: G(n, e)
378
+ };
379
+ }
380
+ function I(r) {
381
+ let e = "";
382
+ for (const t of r) e += String.fromCharCode(t);
383
+ return btoa(e).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
384
+ }
385
+ function $(r) {
386
+ const e = r.length % 4 === 0 ? "" : "=".repeat(4 - r.length % 4), t = r.replace(/-/g, "+").replace(/_/g, "/") + e, n = atob(t), a = new Uint8Array(n.length);
387
+ for (let s = 0; s < n.length; s++) a[s] = n.charCodeAt(s);
388
+ return a;
389
+ }
390
+ async function P(r, e) {
391
+ const t = await crypto.subtle.importKey(
392
+ "raw",
393
+ new TextEncoder().encode(r),
394
+ { name: "HMAC", hash: "SHA-256" },
395
+ !1,
396
+ ["sign"]
397
+ ), n = await crypto.subtle.sign("HMAC", t, new TextEncoder().encode(e));
398
+ return I(new Uint8Array(n));
399
+ }
400
+ function T(r) {
401
+ return [r.v, r.bid, r.wh, r.fp, r.ns.toFixed(6), String(r.exp), r.nonce].join("|");
402
+ }
403
+ async function V(r, e) {
404
+ const t = I(new TextEncoder().encode(JSON.stringify(e))), n = await P(r, T(e));
405
+ return `${t}.${n}`;
406
+ }
407
+ async function q(r, e, t = Date.now()) {
408
+ const n = e.lastIndexOf(".");
409
+ if (n <= 0) return { ok: !1, reason: "MALFORMED_TOKEN" };
410
+ let a;
411
+ try {
412
+ const i = new TextDecoder().decode($(e.slice(0, n)));
413
+ a = JSON.parse(i);
414
+ } catch {
415
+ return { ok: !1, reason: "INVALID_PAYLOAD" };
416
+ }
417
+ if (a.v !== 1) return { ok: !1, reason: "UNSUPPORTED_VERSION", payload: a };
418
+ if (a.exp * 1e3 < t) return { ok: !1, reason: "EXPIRED", payload: a };
419
+ const s = await P(r, T(a)), o = e.slice(n + 1);
420
+ if (s.length !== o.length)
421
+ return { ok: !1, reason: "SIGNATURE_MISMATCH", payload: a };
422
+ let c = 0;
423
+ for (let i = 0; i < s.length; i++)
424
+ c |= s.charCodeAt(i) ^ o.charCodeAt(i);
425
+ return c !== 0 ? { ok: !1, reason: "SIGNATURE_MISMATCH", payload: a } : { ok: !0, payload: a };
426
+ }
427
+ const X = "dokkebi-guard-upload/worker?worker&url";
428
+ export {
429
+ L as DEFAULT_BLOCK_LABELS,
430
+ A as DEFAULT_MODEL_DTYPE,
431
+ O as DEFAULT_MODEL_ID,
432
+ B as DEFAULT_NSFW_THRESHOLD,
433
+ U as GuardIntegrityError,
434
+ y as GuardProofError,
435
+ N as GuardUpload,
436
+ R as GuardUploadBlockedError,
437
+ E as GuardUploadHttpError,
438
+ F as NsfwScanner,
439
+ X as WORKER_MODULE_ID,
440
+ K as createGuardUpload,
441
+ J as createGuardUploadOptions,
442
+ D as fileFingerprint,
443
+ M as fileToPixels,
444
+ H as guessFilename,
445
+ x as hashWorkerScript,
446
+ W as isImageFile,
447
+ j as loadGuardManifest,
448
+ G as securityFromManifest,
449
+ V as signProofPayload,
450
+ q as verifyProofToken,
451
+ C as verifyWorkerIntegrity
452
+ };
453
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/types.ts","../src/security/errors.ts","../src/security/sha256.ts","../src/security/fileFingerprint.ts","../src/security/requestProof.ts","../src/security/verifyWorker.ts","../src/utils/image.ts","../src/NsfwScanner.ts","../src/GuardUpload.ts","../src/security/manifest.ts","../src/security/proofToken.ts","../src/index.ts"],"sourcesContent":["/** 단일 라벨 점수 */\nexport interface LabelScore {\n label: string;\n score: number;\n}\n\n/** NSFW 스캔 결과 */\nexport interface ScanResult {\n /** 업로드 허용 여부 */\n pass: boolean;\n /** NSFW 관련 최고 점수 (0–1) */\n nsfwScore: number;\n /** 모델이 반환한 전체 라벨 점수 */\n labels: LabelScore[];\n /** 차단 시 사용자에게 보여줄 이유 */\n reason?: string;\n /** 처리에 걸린 시간(ms) */\n durationMs: number;\n}\n\nexport interface GuardUploadOptions {\n /** NSFW 점수가 이 값 이상이면 차단 (0–1, 기본 0.75) */\n nsfwThreshold?: number;\n /** 차단 대상 라벨 (소문자 비교). 기본: nsfw, porn, explicit, hentai, sexy */\n blockLabels?: string[];\n /** Hugging Face 모델 ID (기본 AdamCodd/vit-base-nsfw-detector — Transformers.js ONNX) */\n modelId?: string;\n /** ONNX dtype: q4 | fp16 | fp32 (기본 q4, WASM 호환) */\n modelDtype?: 'q4' | 'q4f16' | 'fp16' | 'fp32' | 'q8';\n /** 추론 전 이미지 최대 변 (기본 512) */\n maxImageSide?: number;\n /** ONNX WASM 디렉터리. 미설정 시 Vite 등 번들러가 worker와 함께 wasm을 제공 (권장) */\n onnxWasmPaths?: string;\n /** Web Worker URL. 미지정 시 패키지 worker 또는 blob worker */\n workerUrl?: string | URL;\n /** 모델 로드 진행 콜백 */\n onLoadProgress?: (progress: number, detail?: LoadProgressDetail) => void;\n /** 스캔 시작/완료 콜백 */\n onScanStart?: () => void;\n onScanComplete?: (result: ScanResult) => void;\n /** 배포용 무결성·proof (서버 검증) */\n security?: GuardSecurityConfig;\n}\n\n/** 빌드 manifest (`guard-manifest.json`) */\nexport interface GuardManifest {\n buildId: string;\n workerSha256: string;\n libVersion: string;\n builtAt: string;\n}\n\n/** 서버 proof·무결성 검증 설정 */\nexport interface GuardSecurityConfig {\n /** 빌드 ID (manifest.buildId) */\n buildId: string;\n /** Worker 스크립트 SHA-256 hex (manifest.workerSha256) */\n workerSha256: string;\n /** proof 발급 API (POST) */\n proofEndpoint: string;\n /** 변조 리포트 API (POST, 선택) */\n reportEndpoint?: string;\n /** proof 요청 추가 헤더 */\n proofHeaders?: Record<string, string>;\n proofCredentials?: RequestCredentials;\n /** Worker 해시 검증 (기본 true) */\n verifyWorker?: boolean;\n}\n\nexport interface GuardSecurityOptions extends GuardUploadOptions {\n security: GuardSecurityConfig;\n}\n\nexport interface LoadProgressDetail {\n status?: string;\n loaded?: number;\n total?: number;\n file?: string;\n}\n\nexport interface UploadOptions {\n /** multipart 업로드 URL */\n endpoint: string;\n /** FormData 필드명 (기본 'file') */\n fieldName?: string;\n /** 추가 FormData 필드 */\n extraFields?: Record<string, string | Blob>;\n /** fetch 헤더 (Authorization 등) */\n headers?: Record<string, string>;\n /** proof 토큰 헤더명 (security 사용 시 기본 X-Guard-Token) */\n guardTokenHeader?: string;\n /** 응답 JSON에서 URL을 꺼낼 키 (기본 'url') */\n urlKey?: string;\n /** fetch credentials */\n credentials?: RequestCredentials;\n /** security.proofEndpoint 를 upload 에서도 쓸 경우 false 로 개별 지정 */\n skipProof?: boolean;\n}\n\nexport interface UploadResult {\n url: string;\n scan: ScanResult;\n raw: unknown;\n /** security 사용 시 발급된 proof 토큰 */\n guardToken?: string;\n}\n\nexport interface GuardUploadInstance {\n /** 모델 로드 (최초 scan/upload 전 1회) */\n load(): Promise<void>;\n /** 이미지만 검열 */\n scan(file: File | Blob): Promise<ScanResult>;\n /** 검열 통과 시에만 업로드 */\n upload(file: File | Blob, options: UploadOptions): Promise<UploadResult>;\n /** Worker 종료 */\n dispose(): void;\n /** 모델 로드 완료 여부 */\n readonly ready: boolean;\n}\n\n/** Worker ↔ Main 메시지 타입 */\nexport type WorkerOutMessage =\n | { type: 'loading'; device?: string }\n | { type: 'progress'; progress: number; loaded?: number; total?: number; file?: string }\n | { type: 'ready'; device?: string }\n | { type: 'result'; labels: LabelScore[]; nsfwScore: number; pass: boolean; reason?: string }\n | { type: 'error'; message: string };\n\nexport type WorkerInMessage =\n | { type: 'load'; data: { modelId: string; modelDtype: string; onnxWasmPaths?: string; nsfwThreshold: number; blockLabels: string[] } }\n | { type: 'scan'; data: { pixels: ArrayBuffer; width: number; height: number } };\n\nexport const DEFAULT_MODEL_ID = 'AdamCodd/vit-base-nsfw-detector';\n\nexport const DEFAULT_MODEL_DTYPE = 'q4' as const;\n\nexport const DEFAULT_BLOCK_LABELS = [\n 'nsfw',\n 'porn',\n 'pornography',\n 'explicit',\n 'hentai',\n 'sexy',\n 'nude',\n 'nudity',\n];\n\nexport const DEFAULT_NSFW_THRESHOLD = 0.75;\n","import type { ScanResult } from '../types';\n\nexport class GuardIntegrityError extends Error {\n readonly buildId?: string;\n readonly expectedSha256: string;\n readonly actualSha256: string;\n\n constructor(message: string, expectedSha256: string, actualSha256: string, buildId?: string) {\n super(message);\n this.name = 'GuardIntegrityError';\n this.expectedSha256 = expectedSha256;\n this.actualSha256 = actualSha256;\n this.buildId = buildId;\n }\n}\n\nexport class GuardProofError extends Error {\n readonly code: string;\n readonly scan?: ScanResult;\n\n constructor(message: string, code: string, scan?: ScanResult) {\n super(message);\n this.name = 'GuardProofError';\n this.code = code;\n this.scan = scan;\n }\n}\n","/** ArrayBuffer → SHA-256 hex (브라우저 Web Crypto) */\nexport async function sha256Hex(data: ArrayBuffer | Uint8Array): Promise<string> {\n const buf = data instanceof Uint8Array ? data : new Uint8Array(data);\n const digest = await crypto.subtle.digest('SHA-256', buf as BufferSource);\n return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nexport async function sha256HexFromString(text: string): Promise<string> {\n return sha256Hex(new TextEncoder().encode(text));\n}\n","import { sha256Hex } from './sha256';\n\n/** 업로드 파일 바인딩용 fingerprint (크기·타입·앞 64KB) */\nexport async function fileFingerprint(file: Blob): Promise<string> {\n const head = file.slice(0, 65536);\n const headBuf = await head.arrayBuffer();\n const meta = `${file.size}|${file.type || 'application/octet-stream'}|`;\n const metaBytes = new TextEncoder().encode(meta);\n const combined = new Uint8Array(metaBytes.length + headBuf.byteLength);\n combined.set(metaBytes, 0);\n combined.set(new Uint8Array(headBuf), metaBytes.length);\n return sha256Hex(combined);\n}\n","import type { ScanResult } from '../types';\nimport type { GuardSecurityConfig } from '../types';\nimport { fileFingerprint } from './fileFingerprint';\nimport { GuardProofError } from './errors';\n\nexport interface ProofResponse {\n token: string;\n exp: number;\n}\n\nexport async function requestUploadProof(\n file: Blob,\n scan: ScanResult,\n security: GuardSecurityConfig,\n workerSha256: string,\n): Promise<ProofResponse> {\n const fp = await fileFingerprint(file);\n const body = {\n buildId: security.buildId,\n workerSha256,\n fileFingerprint: fp,\n nsfwScore: scan.nsfwScore,\n pass: scan.pass,\n labels: scan.labels,\n durationMs: scan.durationMs,\n };\n\n const res = await fetch(security.proofEndpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...security.proofHeaders,\n },\n credentials: security.proofCredentials ?? 'same-origin',\n body: JSON.stringify(body),\n });\n\n const rawText = await res.text();\n let json: Record<string, unknown>;\n try {\n json = JSON.parse(rawText) as Record<string, unknown>;\n } catch {\n throw new GuardProofError(\n `proof API 응답 파싱 실패 (HTTP ${res.status})`,\n 'PROOF_PARSE_ERROR',\n scan,\n );\n }\n\n if (!res.ok) {\n const code = typeof json.code === 'string' ? json.code : 'PROOF_DENIED';\n const err = typeof json.error === 'string' ? json.error : `proof 거부 (HTTP ${res.status})`;\n throw new GuardProofError(err, code, scan);\n }\n\n const token = json.token;\n const exp = json.exp;\n if (typeof token !== 'string' || typeof exp !== 'number') {\n throw new GuardProofError('proof API 응답에 token/exp 없음', 'PROOF_INVALID_RESPONSE', scan);\n }\n\n return { token, exp };\n}\n\nexport async function reportIntegrityViolation(\n security: GuardSecurityConfig,\n detail: {\n expectedSha256: string;\n actualSha256: string;\n workerUrl: string;\n },\n): Promise<void> {\n if (!security.reportEndpoint) return;\n try {\n await fetch(security.reportEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...security.proofHeaders },\n credentials: security.proofCredentials ?? 'same-origin',\n body: JSON.stringify({\n buildId: security.buildId,\n event: 'worker_integrity_mismatch',\n ...detail,\n ts: new Date().toISOString(),\n }),\n });\n } catch {\n /* 리포트 실패는 업로드 차단 사유가 아님 */\n }\n}\n","import { sha256Hex } from './sha256';\n\nexport async function hashWorkerScript(workerUrl: string | URL): Promise<string> {\n const url = typeof workerUrl === 'string' ? workerUrl : workerUrl.href;\n const res = await fetch(url, { cache: 'no-store' });\n if (!res.ok) {\n throw new Error(`Worker 스크립트를 가져올 수 없습니다. HTTP ${res.status} (${url})`);\n }\n const buf = await res.arrayBuffer();\n return sha256Hex(buf);\n}\n\nexport async function verifyWorkerIntegrity(\n workerUrl: string | URL,\n expectedSha256: string,\n): Promise<{ ok: true; actualSha256: string } | { ok: false; actualSha256: string; expectedSha256: string }> {\n const actualSha256 = await hashWorkerScript(workerUrl);\n const expected = expectedSha256.toLowerCase();\n const actual = actualSha256.toLowerCase();\n if (actual === expected) return { ok: true, actualSha256 };\n return { ok: false, actualSha256, expectedSha256: expected };\n}\n","/** File/Blob → RGBA 픽셀 (최대 변 리사이즈) */\n\nexport interface ImagePixels {\n pixels: Uint8ClampedArray;\n width: number;\n height: number;\n}\n\nexport async function fileToPixels(\n file: File | Blob,\n maxSide = 512,\n): Promise<ImagePixels> {\n const bitmap = await createImageBitmap(file);\n try {\n let w = bitmap.width;\n let h = bitmap.height;\n const scale = Math.min(1, maxSide / Math.max(w, h));\n if (scale < 1) {\n w = Math.max(1, Math.round(w * scale));\n h = Math.max(1, Math.round(h * scale));\n }\n\n const canvas = typeof OffscreenCanvas !== 'undefined'\n ? new OffscreenCanvas(w, h)\n : document.createElement('canvas');\n if (!(canvas instanceof OffscreenCanvas)) {\n canvas.width = w;\n canvas.height = h;\n } else {\n (canvas as OffscreenCanvas).width = w;\n (canvas as OffscreenCanvas).height = h;\n }\n\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | null;\n if (!ctx) throw new Error('2D canvas context unavailable');\n\n ctx.drawImage(bitmap, 0, 0, w, h);\n const imageData = ctx.getImageData(0, 0, w, h);\n return { pixels: imageData.data, width: w, height: h };\n } finally {\n bitmap.close?.();\n }\n}\n\nexport function isImageFile(file: File | Blob): boolean {\n if (file.type) return file.type.startsWith('image/');\n if (file instanceof File) {\n const ext = file.name.split('.').pop()?.toLowerCase() ?? '';\n return ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'avif'].includes(ext);\n }\n return false;\n}\n\nexport function guessFilename(file: File | Blob, fallback = 'upload.jpg'): string {\n if (file instanceof File && file.name) return file.name;\n const ext = file.type?.split('/')[1]?.replace('jpeg', 'jpg') ?? 'jpg';\n return `upload.${ext}`;\n}\n","import {\n DEFAULT_BLOCK_LABELS,\n DEFAULT_MODEL_DTYPE,\n DEFAULT_MODEL_ID,\n DEFAULT_NSFW_THRESHOLD,\n type GuardSecurityConfig,\n type GuardUploadOptions,\n type ScanResult,\n type WorkerInMessage,\n type WorkerOutMessage,\n} from './types';\nimport { GuardIntegrityError } from './security/errors';\nimport { reportIntegrityViolation } from './security/requestProof';\nimport { hashWorkerScript, verifyWorkerIntegrity } from './security/verifyWorker';\nimport { fileToPixels, isImageFile } from './utils/image';\n\nfunction defaultWorkerUrl(): URL | null {\n return null;\n}\n\ntype Pending = {\n resolve: (value: ScanResult) => void;\n reject: (reason: Error) => void;\n startedAt: number;\n};\n\nexport class NsfwScanner {\n private worker: Worker | null = null;\n private loadPromise: Promise<void> | null = null;\n private pendingScan: Pending | null = null;\n private _ready = false;\n private integrityChecked = false;\n /** 검증된 worker SHA-256 (proof 바인딩) */\n verifiedWorkerSha256 = '';\n\n private readonly modelId: string;\n private readonly modelDtype: string;\n private readonly nsfwThreshold: number;\n private readonly blockLabels: string[];\n private readonly maxImageSide: number;\n private readonly onnxWasmPaths?: string;\n private readonly workerUrl?: string | URL;\n private readonly security?: GuardSecurityConfig;\n private readonly onLoadProgress?: GuardUploadOptions['onLoadProgress'];\n private readonly onScanStart?: GuardUploadOptions['onScanStart'];\n private readonly onScanComplete?: GuardUploadOptions['onScanComplete'];\n\n constructor(options: GuardUploadOptions = {}) {\n this.modelId = options.modelId ?? DEFAULT_MODEL_ID;\n this.modelDtype = options.modelDtype ?? DEFAULT_MODEL_DTYPE;\n this.nsfwThreshold = options.nsfwThreshold ?? DEFAULT_NSFW_THRESHOLD;\n this.blockLabels = options.blockLabels ?? DEFAULT_BLOCK_LABELS;\n this.maxImageSide = options.maxImageSide ?? 512;\n this.onnxWasmPaths = options.onnxWasmPaths;\n this.workerUrl = options.workerUrl;\n this.security = options.security;\n this.onLoadProgress = options.onLoadProgress;\n this.onScanStart = options.onScanStart;\n this.onScanComplete = options.onScanComplete;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n getWorkerUrl(): string | URL {\n const url = this.workerUrl ?? defaultWorkerUrl();\n if (!url) throw new Error('workerUrl이 설정되지 않았습니다.');\n return url;\n }\n\n getSecurity(): GuardSecurityConfig | undefined {\n return this.security;\n }\n\n async load(): Promise<void> {\n if (this._ready) return;\n if (this.loadPromise) return this.loadPromise;\n\n this.loadPromise = (async () => {\n await this.ensureIntegrity();\n this.ensureWorker();\n\n await new Promise<void>((resolve, reject) => {\n const onMessage = (ev: MessageEvent<WorkerOutMessage>) => {\n const msg = ev.data;\n switch (msg.type) {\n case 'progress':\n this.onLoadProgress?.(msg.progress, {\n status: 'progress',\n loaded: msg.loaded,\n total: msg.total,\n file: msg.file,\n });\n break;\n case 'ready':\n this._ready = true;\n this.worker?.removeEventListener('message', onMessage);\n resolve();\n break;\n case 'error':\n this.worker?.removeEventListener('message', onMessage);\n reject(new Error(msg.message));\n break;\n default:\n break;\n }\n };\n\n this.worker!.addEventListener('message', onMessage);\n this.worker!.addEventListener('error', (e) => {\n reject(new Error(e.message || 'Worker error'));\n });\n\n const payload: WorkerInMessage = {\n type: 'load',\n data: {\n modelId: this.modelId,\n modelDtype: this.modelDtype,\n onnxWasmPaths: this.onnxWasmPaths,\n nsfwThreshold: this.nsfwThreshold,\n blockLabels: this.blockLabels,\n },\n };\n this.worker!.postMessage(payload);\n });\n })().finally(() => {\n this.loadPromise = null;\n });\n\n return this.loadPromise;\n }\n\n async scan(file: File | Blob): Promise<ScanResult> {\n if (!isImageFile(file)) {\n throw new Error('이미지 파일만 검열할 수 있습니다.');\n }\n\n await this.load();\n this.onScanStart?.();\n\n const { pixels, width, height } = await fileToPixels(file, this.maxImageSide);\n const startedAt = performance.now();\n\n return new Promise<ScanResult>((resolve, reject) => {\n if (this.pendingScan) {\n reject(new Error('이미 스캔이 진행 중입니다.'));\n return;\n }\n\n this.pendingScan = { resolve, reject, startedAt };\n this.ensureWorker();\n\n const onMessage = (ev: MessageEvent<WorkerOutMessage>) => {\n const msg = ev.data;\n if (msg.type !== 'result' && msg.type !== 'error') return;\n\n this.worker?.removeEventListener('message', onMessage);\n const pending = this.pendingScan;\n this.pendingScan = null;\n if (!pending) return;\n\n const durationMs = Math.round(performance.now() - pending.startedAt);\n\n if (msg.type === 'error') {\n pending.reject(new Error(msg.message));\n return;\n }\n\n const result: ScanResult = {\n pass: msg.pass,\n nsfwScore: msg.nsfwScore,\n labels: msg.labels,\n reason: msg.reason,\n durationMs,\n };\n this.onScanComplete?.(result);\n pending.resolve(result);\n };\n\n this.worker!.addEventListener('message', onMessage);\n\n const buf = pixels.buffer.slice(0) as ArrayBuffer;\n this.worker!.postMessage(\n {\n type: 'scan',\n data: { pixels: buf, width, height },\n } satisfies WorkerInMessage,\n [buf],\n );\n });\n }\n\n dispose(): void {\n this.pendingScan?.reject(new Error('Scanner disposed'));\n this.pendingScan = null;\n this.worker?.terminate();\n this.worker = null;\n this._ready = false;\n this.loadPromise = null;\n this.integrityChecked = false;\n this.verifiedWorkerSha256 = '';\n }\n\n private async ensureIntegrity(): Promise<void> {\n if (this.integrityChecked) return;\n\n const sec = this.security;\n const workerUrl = this.getWorkerUrl();\n const actualSha256 = await hashWorkerScript(workerUrl);\n this.verifiedWorkerSha256 = actualSha256;\n\n const verifyEnabled = sec?.verifyWorker !== false && !!sec?.workerSha256;\n if (!verifyEnabled) {\n this.integrityChecked = true;\n return;\n }\n\n const check = await verifyWorkerIntegrity(workerUrl, sec!.workerSha256);\n if (!check.ok) {\n await reportIntegrityViolation(sec!, {\n expectedSha256: check.expectedSha256,\n actualSha256: check.actualSha256,\n workerUrl: typeof workerUrl === 'string' ? workerUrl : workerUrl.href,\n });\n throw new GuardIntegrityError(\n '검열 모듈 무결성 검증 실패 — 변조 가능성이 있습니다.',\n check.expectedSha256,\n check.actualSha256,\n sec!.buildId,\n );\n }\n\n this.integrityChecked = true;\n }\n\n private ensureWorker(): void {\n if (this.worker) return;\n const url = this.workerUrl ?? defaultWorkerUrl();\n if (!url) {\n throw new Error(\n 'workerUrl이 필요합니다. Vite: import workerUrl from \"dokkebi-guard-upload/worker?worker&url\"',\n );\n }\n this.worker = new Worker(url, { type: 'module' });\n }\n}\n","import { NsfwScanner } from './NsfwScanner';\nimport { GuardIntegrityError, GuardProofError } from './security/errors';\nimport { requestUploadProof } from './security/requestProof';\nimport {\n type GuardUploadInstance,\n type GuardUploadOptions,\n type ScanResult,\n type UploadOptions,\n type UploadResult,\n} from './types';\nimport { guessFilename } from './utils/image';\n\nexport class GuardUpload implements GuardUploadInstance {\n private scanner: NsfwScanner;\n\n constructor(options: GuardUploadOptions = {}) {\n this.scanner = new NsfwScanner(options);\n }\n\n get ready(): boolean {\n return this.scanner.ready;\n }\n\n load(): Promise<void> {\n return this.scanner.load();\n }\n\n scan(file: File | Blob): Promise<ScanResult> {\n return this.scanner.scan(file);\n }\n\n async upload(file: File | Blob, options: UploadOptions): Promise<UploadResult> {\n const scan = await this.scanner.scan(file);\n if (!scan.pass) {\n throw new GuardUploadBlockedError(scan.reason ?? '업로드가 차단되었습니다.', scan);\n }\n\n const sec = this.scanner.getSecurity();\n let guardToken: string | undefined;\n\n if (sec && !options.skipProof) {\n const workerSha256 = this.scanner.verifiedWorkerSha256;\n if (!workerSha256) {\n throw new GuardProofError('Worker 해시 미확인', 'WORKER_HASH_MISSING', scan);\n }\n const proof = await requestUploadProof(file, scan, sec, workerSha256);\n guardToken = proof.token;\n }\n\n const fieldName = options.fieldName ?? 'file';\n const urlKey = options.urlKey ?? 'url';\n const filename = guessFilename(file);\n const tokenHeader = options.guardTokenHeader ?? 'X-Guard-Token';\n\n const fd = new FormData();\n fd.append(fieldName, file, filename);\n if (options.extraFields) {\n for (const [k, v] of Object.entries(options.extraFields)) {\n fd.append(k, v);\n }\n }\n\n const headers: Record<string, string> = { ...(options.headers ?? {}) };\n if (guardToken) headers[tokenHeader] = guardToken;\n\n const res = await fetch(options.endpoint, {\n method: 'POST',\n headers,\n body: fd,\n credentials: options.credentials,\n });\n\n const rawText = await res.text();\n let raw: unknown;\n try {\n raw = JSON.parse(rawText);\n } catch {\n throw new GuardUploadHttpError(\n `업로드 응답을 해석할 수 없습니다. HTTP ${res.status}`,\n res.status,\n rawText,\n scan,\n );\n }\n\n const body = raw as Record<string, unknown>;\n const url = body[urlKey];\n if (!res.ok || typeof url !== 'string' || !url) {\n const errMsg =\n (typeof body.error === 'string' && body.error) ||\n (typeof body.message === 'string' && body.message) ||\n `업로드 실패 (HTTP ${res.status})`;\n throw new GuardUploadHttpError(errMsg, res.status, raw, scan);\n }\n\n return { url, scan, raw, guardToken };\n }\n\n dispose(): void {\n this.scanner.dispose();\n }\n}\n\n/** 검열 차단 — scan 결과 포함 */\nexport class GuardUploadBlockedError extends Error {\n readonly scan: ScanResult;\n\n constructor(message: string, scan: ScanResult) {\n super(message);\n this.name = 'GuardUploadBlockedError';\n this.scan = scan;\n }\n}\n\n/** HTTP 업로드 실패 */\nexport class GuardUploadHttpError extends Error {\n readonly status: number;\n readonly body: unknown;\n readonly scan: ScanResult;\n\n constructor(message: string, status: number, body: unknown, scan: ScanResult) {\n super(message);\n this.name = 'GuardUploadHttpError';\n this.status = status;\n this.body = body;\n this.scan = scan;\n }\n}\n\n/** 팩토리 */\nexport function createGuardUpload(options?: GuardUploadOptions): GuardUploadInstance {\n return new GuardUpload(options);\n}\n\nexport { NsfwScanner } from './NsfwScanner';\nexport { GuardIntegrityError, GuardProofError } from './security/errors';\nexport * from './types';\n","import type { GuardManifest, GuardSecurityConfig, GuardUploadOptions } from '../types';\n\n/** `/guard-manifest.json` 로드 */\nexport async function loadGuardManifest(url = '/guard-manifest.json'): Promise<GuardManifest> {\n const res = await fetch(url, { cache: 'no-store' });\n if (!res.ok) throw new Error(`guard manifest 로드 실패: HTTP ${res.status}`);\n const json = (await res.json()) as GuardManifest;\n if (!json.buildId || !json.workerSha256) {\n throw new Error('guard manifest 형식 오류 (buildId, workerSha256 필요)');\n }\n return json;\n}\n\nexport function securityFromManifest(\n manifest: GuardManifest,\n endpoints: { proofEndpoint: string; reportEndpoint?: string },\n): GuardSecurityConfig {\n return {\n buildId: manifest.buildId,\n workerSha256: manifest.workerSha256,\n proofEndpoint: endpoints.proofEndpoint,\n reportEndpoint: endpoints.reportEndpoint,\n verifyWorker: true,\n };\n}\n\nexport async function createGuardUploadOptions(\n manifestUrl: string,\n endpoints: { proofEndpoint: string; reportEndpoint?: string },\n base?: GuardUploadOptions,\n): Promise<GuardUploadOptions> {\n const manifest = await loadGuardManifest(manifestUrl);\n return {\n ...base,\n security: securityFromManifest(manifest, endpoints),\n };\n}\n","/**\n * 업로드 proof 토큰 — 서버·클라이언트 공용 (HMAC-SHA256)\n * 형식: base64url(JSON payload).base64url(signature)\n */\n\nexport interface GuardProofPayload {\n v: 1;\n bid: string;\n wh: string;\n fp: string;\n ns: number;\n exp: number;\n nonce: string;\n}\n\nfunction b64urlEncode(bytes: Uint8Array): string {\n let bin = '';\n for (const b of bytes) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction b64urlDecode(s: string): Uint8Array {\n const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));\n const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad;\n const bin = atob(b64);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n}\n\nasync function hmacSign(secret: string, message: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message));\n return b64urlEncode(new Uint8Array(sig));\n}\n\nexport function canonicalProofMessage(p: GuardProofPayload): string {\n return [p.v, p.bid, p.wh, p.fp, p.ns.toFixed(6), String(p.exp), p.nonce].join('|');\n}\n\nexport async function signProofPayload(\n secret: string,\n payload: GuardProofPayload,\n): Promise<string> {\n const body = b64urlEncode(new TextEncoder().encode(JSON.stringify(payload)));\n const sig = await hmacSign(secret, canonicalProofMessage(payload));\n return `${body}.${sig}`;\n}\n\nexport async function verifyProofToken(\n secret: string,\n token: string,\n nowMs = Date.now(),\n): Promise<{ ok: true; payload: GuardProofPayload } | { ok: false; reason: string; payload?: GuardProofPayload }> {\n const dot = token.lastIndexOf('.');\n if (dot <= 0) return { ok: false, reason: 'MALFORMED_TOKEN' };\n\n let payload: GuardProofPayload;\n try {\n const json = new TextDecoder().decode(b64urlDecode(token.slice(0, dot)));\n payload = JSON.parse(json) as GuardProofPayload;\n } catch {\n return { ok: false, reason: 'INVALID_PAYLOAD' };\n }\n\n if (payload.v !== 1) return { ok: false, reason: 'UNSUPPORTED_VERSION', payload };\n if (payload.exp * 1000 < nowMs) return { ok: false, reason: 'EXPIRED', payload };\n\n const expectedSig = await hmacSign(secret, canonicalProofMessage(payload));\n const actualSig = token.slice(dot + 1);\n if (expectedSig.length !== actualSig.length) {\n return { ok: false, reason: 'SIGNATURE_MISMATCH', payload };\n }\n let diff = 0;\n for (let i = 0; i < expectedSig.length; i++) {\n diff |= expectedSig.charCodeAt(i) ^ actualSig.charCodeAt(i);\n }\n if (diff !== 0) return { ok: false, reason: 'SIGNATURE_MISMATCH', payload };\n\n return { ok: true, payload };\n}\n","export {\n createGuardUpload,\n GuardUpload,\n GuardUploadBlockedError,\n GuardUploadHttpError,\n GuardIntegrityError,\n GuardProofError,\n NsfwScanner,\n} from './GuardUpload';\n\nexport type {\n GuardUploadInstance,\n GuardUploadOptions,\n GuardManifest,\n GuardSecurityConfig,\n LabelScore,\n LoadProgressDetail,\n ScanResult,\n UploadOptions,\n UploadResult,\n} from './types';\n\nexport {\n DEFAULT_BLOCK_LABELS,\n DEFAULT_MODEL_DTYPE,\n DEFAULT_MODEL_ID,\n DEFAULT_NSFW_THRESHOLD,\n} from './types';\n\nexport { fileToPixels, guessFilename, isImageFile } from './utils/image';\n\nexport { loadGuardManifest, securityFromManifest, createGuardUploadOptions } from './security/manifest';\nexport { verifyProofToken, signProofPayload } from './security/proofToken';\nexport type { GuardProofPayload } from './security/proofToken';\nexport { hashWorkerScript, verifyWorkerIntegrity } from './security/verifyWorker';\nexport { fileFingerprint } from './security/fileFingerprint';\n\n/** Vite `?worker&url` import 시 모듈 경로 */\nexport const WORKER_MODULE_ID = 'dokkebi-guard-upload/worker?worker&url';\n"],"names":["DEFAULT_MODEL_ID","DEFAULT_MODEL_DTYPE","DEFAULT_BLOCK_LABELS","DEFAULT_NSFW_THRESHOLD","GuardIntegrityError","message","expectedSha256","actualSha256","buildId","GuardProofError","code","scan","sha256Hex","data","buf","digest","b","fileFingerprint","file","headBuf","meta","metaBytes","combined","requestUploadProof","security","workerSha256","fp","body","res","rawText","json","err","token","exp","reportIntegrityViolation","detail","hashWorkerScript","workerUrl","url","verifyWorkerIntegrity","expected","fileToPixels","maxSide","bitmap","w","h","scale","canvas","ctx","isImageFile","ext","guessFilename","fallback","defaultWorkerUrl","NsfwScanner","options","resolve","reject","onMessage","ev","msg","e","payload","pixels","width","height","startedAt","pending","durationMs","result","sec","check","GuardUpload","GuardUploadBlockedError","guardToken","fieldName","urlKey","filename","tokenHeader","fd","k","v","headers","raw","GuardUploadHttpError","errMsg","status","createGuardUpload","loadGuardManifest","securityFromManifest","manifest","endpoints","createGuardUploadOptions","manifestUrl","base","b64urlEncode","bytes","bin","b64urlDecode","s","pad","b64","out","i","hmacSign","secret","key","sig","canonicalProofMessage","p","signProofPayload","verifyProofToken","nowMs","dot","expectedSig","actualSig","diff","WORKER_MODULE_ID"],"mappings":"AAoIO,MAAMA,IAAmB,mCAEnBC,IAAsB,MAEtBC,IAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAEaC,IAAyB;ACjJ/B,MAAMC,UAA4B,MAAM;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAYC,GAAiBC,GAAwBC,GAAsBC,GAAkB;AAC3F,UAAMH,CAAO,GACb,KAAK,OAAO,uBACZ,KAAK,iBAAiBC,GACtB,KAAK,eAAeC,GACpB,KAAK,UAAUC;AAAA,EACjB;AACF;AAEO,MAAMC,UAAwB,MAAM;AAAA,EAChC;AAAA,EACA;AAAA,EAET,YAAYJ,GAAiBK,GAAcC,GAAmB;AAC5D,UAAMN,CAAO,GACb,KAAK,OAAO,mBACZ,KAAK,OAAOK,GACZ,KAAK,OAAOC;AAAA,EACd;AACF;ACzBA,eAAsBC,EAAUC,GAAiD;AAC/E,QAAMC,IAAMD,aAAgB,aAAaA,IAAO,IAAI,WAAWA,CAAI,GAC7DE,IAAS,MAAM,OAAO,OAAO,OAAO,WAAWD,CAAmB;AACxE,SAAO,CAAC,GAAG,IAAI,WAAWC,CAAM,CAAC,EAAE,IAAI,CAACC,MAAMA,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AACxF;ACFA,eAAsBC,EAAgBC,GAA6B;AAEjE,QAAMC,IAAU,MADHD,EAAK,MAAM,GAAG,KAAK,EACL,YAAA,GACrBE,IAAO,GAAGF,EAAK,IAAI,IAAIA,EAAK,QAAQ,0BAA0B,KAC9DG,IAAY,IAAI,cAAc,OAAOD,CAAI,GACzCE,IAAW,IAAI,WAAWD,EAAU,SAASF,EAAQ,UAAU;AACrE,SAAAG,EAAS,IAAID,GAAW,CAAC,GACzBC,EAAS,IAAI,IAAI,WAAWH,CAAO,GAAGE,EAAU,MAAM,GAC/CT,EAAUU,CAAQ;AAC3B;ACFA,eAAsBC,EACpBL,GACAP,GACAa,GACAC,GACwB;AACxB,QAAMC,IAAK,MAAMT,EAAgBC,CAAI,GAC/BS,IAAO;AAAA,IACX,SAASH,EAAS;AAAA,IAClB,cAAAC;AAAA,IACA,iBAAiBC;AAAA,IACjB,WAAWf,EAAK;AAAA,IAChB,MAAMA,EAAK;AAAA,IACX,QAAQA,EAAK;AAAA,IACb,YAAYA,EAAK;AAAA,EAAA,GAGbiB,IAAM,MAAM,MAAMJ,EAAS,eAAe;AAAA,IAC9C,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,GAAGA,EAAS;AAAA,IAAA;AAAA,IAEd,aAAaA,EAAS,oBAAoB;AAAA,IAC1C,MAAM,KAAK,UAAUG,CAAI;AAAA,EAAA,CAC1B,GAEKE,IAAU,MAAMD,EAAI,KAAA;AAC1B,MAAIE;AACJ,MAAI;AACF,IAAAA,IAAO,KAAK,MAAMD,CAAO;AAAA,EAC3B,QAAQ;AACN,UAAM,IAAIpB;AAAA,MACR,4BAA4BmB,EAAI,MAAM;AAAA,MACtC;AAAA,MACAjB;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,CAACiB,EAAI,IAAI;AACX,UAAMlB,IAAO,OAAOoB,EAAK,QAAS,WAAWA,EAAK,OAAO,gBACnDC,IAAM,OAAOD,EAAK,SAAU,WAAWA,EAAK,QAAQ,kBAAkBF,EAAI,MAAM;AACtF,UAAM,IAAInB,EAAgBsB,GAAKrB,GAAMC,CAAI;AAAA,EAC3C;AAEA,QAAMqB,IAAQF,EAAK,OACbG,IAAMH,EAAK;AACjB,MAAI,OAAOE,KAAU,YAAY,OAAOC,KAAQ;AAC9C,UAAM,IAAIxB,EAAgB,8BAA8B,0BAA0BE,CAAI;AAGxF,SAAO,EAAE,OAAAqB,GAAO,KAAAC,EAAA;AAClB;AAEA,eAAsBC,EACpBV,GACAW,GAKe;AACf,MAAKX,EAAS;AACd,QAAI;AACF,YAAM,MAAMA,EAAS,gBAAgB;AAAA,QACnC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAGA,EAAS,aAAA;AAAA,QAC3D,aAAaA,EAAS,oBAAoB;AAAA,QAC1C,MAAM,KAAK,UAAU;AAAA,UACnB,SAASA,EAAS;AAAA,UAClB,OAAO;AAAA,UACP,GAAGW;AAAA,UACH,KAAI,oBAAI,KAAA,GAAO,YAAA;AAAA,QAAY,CAC5B;AAAA,MAAA,CACF;AAAA,IACH,QAAQ;AAAA,IAER;AACF;ACtFA,eAAsBC,EAAiBC,GAA0C;AAC/E,QAAMC,IAAM,OAAOD,KAAc,WAAWA,IAAYA,EAAU,MAC5DT,IAAM,MAAM,MAAMU,GAAK,EAAE,OAAO,YAAY;AAClD,MAAI,CAACV,EAAI;AACP,UAAM,IAAI,MAAM,iCAAiCA,EAAI,MAAM,KAAKU,CAAG,GAAG;AAExE,QAAMxB,IAAM,MAAMc,EAAI,YAAA;AACtB,SAAOhB,EAAUE,CAAG;AACtB;AAEA,eAAsByB,EACpBF,GACA/B,GAC2G;AAC3G,QAAMC,IAAe,MAAM6B,EAAiBC,CAAS,GAC/CG,IAAWlC,EAAe,YAAA;AAEhC,SADeC,EAAa,YAAA,MACbiC,IAAiB,EAAE,IAAI,IAAM,cAAAjC,EAAA,IACrC,EAAE,IAAI,IAAO,cAAAA,GAAc,gBAAgBiC,EAAA;AACpD;ACbA,eAAsBC,EACpBvB,GACAwB,IAAU,KACY;AACtB,QAAMC,IAAS,MAAM,kBAAkBzB,CAAI;AAC3C,MAAI;AACF,QAAI0B,IAAID,EAAO,OACXE,IAAIF,EAAO;AACf,UAAMG,IAAQ,KAAK,IAAI,GAAGJ,IAAU,KAAK,IAAIE,GAAGC,CAAC,CAAC;AAClD,IAAIC,IAAQ,MACVF,IAAI,KAAK,IAAI,GAAG,KAAK,MAAMA,IAAIE,CAAK,CAAC,GACrCD,IAAI,KAAK,IAAI,GAAG,KAAK,MAAMA,IAAIC,CAAK,CAAC;AAGvC,UAAMC,IAAS,OAAO,kBAAoB,MACtC,IAAI,gBAAgBH,GAAGC,CAAC,IACxB,SAAS,cAAc,QAAQ;AACnC,IAAME,aAAkB,iBAIrBA,EAA2B,QAAQH,GACnCG,EAA2B,SAASF;AAGvC,UAAMG,IAAMD,EAAO,WAAW,IAAI;AAClC,QAAI,CAACC,EAAK,OAAM,IAAI,MAAM,+BAA+B;AAEzD,WAAAA,EAAI,UAAUL,GAAQ,GAAG,GAAGC,GAAGC,CAAC,GAEzB,EAAE,QADSG,EAAI,aAAa,GAAG,GAAGJ,GAAGC,CAAC,EAClB,MAAM,OAAOD,GAAG,QAAQC,EAAA;AAAA,EACrD,UAAA;AACE,IAAAF,EAAO,QAAA;AAAA,EACT;AACF;AAEO,SAASM,EAAY/B,GAA4B;AACtD,MAAIA,EAAK,KAAM,QAAOA,EAAK,KAAK,WAAW,QAAQ;AACnD,MAAIA,aAAgB,MAAM;AACxB,UAAMgC,IAAMhC,EAAK,KAAK,MAAM,GAAG,EAAE,IAAA,GAAO,YAAA,KAAiB;AACzD,WAAO,CAAC,OAAO,QAAQ,OAAO,QAAQ,OAAO,OAAO,MAAM,EAAE,SAASgC,CAAG;AAAA,EAC1E;AACA,SAAO;AACT;AAEO,SAASC,EAAcjC,GAAmBkC,IAAW,cAAsB;AAChF,SAAIlC,aAAgB,QAAQA,EAAK,OAAaA,EAAK,OAE5C,UADKA,EAAK,MAAM,MAAM,GAAG,EAAE,CAAC,GAAG,QAAQ,QAAQ,KAAK,KAAK,KAC5C;AACtB;ACzCA,SAASmC,IAA+B;AACtC,SAAO;AACT;AAQO,MAAMC,EAAY;AAAA,EACf,SAAwB;AAAA,EACxB,cAAoC;AAAA,EACpC,cAA8B;AAAA,EAC9B,SAAS;AAAA,EACT,mBAAmB;AAAA;AAAA,EAE3B,uBAAuB;AAAA,EAEN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAYC,IAA8B,IAAI;AAC5C,SAAK,UAAUA,EAAQ,WAAWvD,GAClC,KAAK,aAAauD,EAAQ,cAActD,GACxC,KAAK,gBAAgBsD,EAAQ,iBAAiB,MAC9C,KAAK,cAAcA,EAAQ,eAAerD,GAC1C,KAAK,eAAeqD,EAAQ,gBAAgB,KAC5C,KAAK,gBAAgBA,EAAQ,eAC7B,KAAK,YAAYA,EAAQ,WACzB,KAAK,WAAWA,EAAQ,UACxB,KAAK,iBAAiBA,EAAQ,gBAC9B,KAAK,cAAcA,EAAQ,aAC3B,KAAK,iBAAiBA,EAAQ;AAAA,EAChC;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,eAA6B;AAC3B,UAAMjB,IAAM,KAAK,aAAae,EAAA;AAC9B,QAAI,CAACf,EAAK,OAAM,IAAI,MAAM,wBAAwB;AAClD,WAAOA;AAAA,EACT;AAAA,EAEA,cAA+C;AAC7C,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI,MAAK;AACT,aAAI,KAAK,cAAoB,KAAK,eAElC,KAAK,eAAe,YAAY;AAC9B,cAAM,KAAK,gBAAA,GACX,KAAK,aAAA,GAEL,MAAM,IAAI,QAAc,CAACkB,GAASC,MAAW;AAC3C,gBAAMC,IAAY,CAACC,MAAuC;AACxD,kBAAMC,IAAMD,EAAG;AACf,oBAAQC,EAAI,MAAA;AAAA,cACV,KAAK;AACH,qBAAK,iBAAiBA,EAAI,UAAU;AAAA,kBAClC,QAAQ;AAAA,kBACR,QAAQA,EAAI;AAAA,kBACZ,OAAOA,EAAI;AAAA,kBACX,MAAMA,EAAI;AAAA,gBAAA,CACX;AACD;AAAA,cACF,KAAK;AACH,qBAAK,SAAS,IACd,KAAK,QAAQ,oBAAoB,WAAWF,CAAS,GACrDF,EAAA;AACA;AAAA,cACF,KAAK;AACH,qBAAK,QAAQ,oBAAoB,WAAWE,CAAS,GACrDD,EAAO,IAAI,MAAMG,EAAI,OAAO,CAAC;AAC7B;AAAA,YAEA;AAAA,UAEN;AAEA,eAAK,OAAQ,iBAAiB,WAAWF,CAAS,GAClD,KAAK,OAAQ,iBAAiB,SAAS,CAACG,MAAM;AAC5C,YAAAJ,EAAO,IAAI,MAAMI,EAAE,WAAW,cAAc,CAAC;AAAA,UAC/C,CAAC;AAED,gBAAMC,IAA2B;AAAA,YAC/B,MAAM;AAAA,YACN,MAAM;AAAA,cACJ,SAAS,KAAK;AAAA,cACd,YAAY,KAAK;AAAA,cACjB,eAAe,KAAK;AAAA,cACpB,eAAe,KAAK;AAAA,cACpB,aAAa,KAAK;AAAA,YAAA;AAAA,UACpB;AAEF,eAAK,OAAQ,YAAYA,CAAO;AAAA,QAClC,CAAC;AAAA,MACH,GAAA,EAAK,QAAQ,MAAM;AACjB,aAAK,cAAc;AAAA,MACrB,CAAC,GAEM,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,KAAK5C,GAAwC;AACjD,QAAI,CAAC+B,EAAY/B,CAAI;AACnB,YAAM,IAAI,MAAM,qBAAqB;AAGvC,UAAM,KAAK,KAAA,GACX,KAAK,cAAA;AAEL,UAAM,EAAE,QAAA6C,GAAQ,OAAAC,GAAO,QAAAC,EAAA,IAAW,MAAMxB,EAAavB,GAAM,KAAK,YAAY,GACtEgD,IAAY,YAAY,IAAA;AAE9B,WAAO,IAAI,QAAoB,CAACV,GAASC,MAAW;AAClD,UAAI,KAAK,aAAa;AACpB,QAAAA,EAAO,IAAI,MAAM,iBAAiB,CAAC;AACnC;AAAA,MACF;AAEA,WAAK,cAAc,EAAE,SAAAD,GAAS,QAAAC,GAAQ,WAAAS,EAAA,GACtC,KAAK,aAAA;AAEL,YAAMR,IAAY,CAACC,MAAuC;AACxD,cAAMC,IAAMD,EAAG;AACf,YAAIC,EAAI,SAAS,YAAYA,EAAI,SAAS,QAAS;AAEnD,aAAK,QAAQ,oBAAoB,WAAWF,CAAS;AACrD,cAAMS,IAAU,KAAK;AAErB,YADA,KAAK,cAAc,MACf,CAACA,EAAS;AAEd,cAAMC,IAAa,KAAK,MAAM,YAAY,IAAA,IAAQD,EAAQ,SAAS;AAEnE,YAAIP,EAAI,SAAS,SAAS;AACxB,UAAAO,EAAQ,OAAO,IAAI,MAAMP,EAAI,OAAO,CAAC;AACrC;AAAA,QACF;AAEA,cAAMS,IAAqB;AAAA,UACzB,MAAMT,EAAI;AAAA,UACV,WAAWA,EAAI;AAAA,UACf,QAAQA,EAAI;AAAA,UACZ,QAAQA,EAAI;AAAA,UACZ,YAAAQ;AAAA,QAAA;AAEF,aAAK,iBAAiBC,CAAM,GAC5BF,EAAQ,QAAQE,CAAM;AAAA,MACxB;AAEA,WAAK,OAAQ,iBAAiB,WAAWX,CAAS;AAElD,YAAM5C,IAAMiD,EAAO,OAAO,MAAM,CAAC;AACjC,WAAK,OAAQ;AAAA,QACX;AAAA,UACE,MAAM;AAAA,UACN,MAAM,EAAE,QAAQjD,GAAK,OAAAkD,GAAO,QAAAC,EAAA;AAAA,QAAO;AAAA,QAErC,CAACnD,CAAG;AAAA,MAAA;AAAA,IAER,CAAC;AAAA,EACH;AAAA,EAEA,UAAgB;AACd,SAAK,aAAa,OAAO,IAAI,MAAM,kBAAkB,CAAC,GACtD,KAAK,cAAc,MACnB,KAAK,QAAQ,UAAA,GACb,KAAK,SAAS,MACd,KAAK,SAAS,IACd,KAAK,cAAc,MACnB,KAAK,mBAAmB,IACxB,KAAK,uBAAuB;AAAA,EAC9B;AAAA,EAEA,MAAc,kBAAiC;AAC7C,QAAI,KAAK,iBAAkB;AAE3B,UAAMwD,IAAM,KAAK,UACXjC,IAAY,KAAK,aAAA,GACjB9B,IAAe,MAAM6B,EAAiBC,CAAS;AAIrD,QAHA,KAAK,uBAAuB9B,GAGxB,EADkB+D,GAAK,iBAAiB,MAAS,CAAC,CAACA,GAAK,eACxC;AAClB,WAAK,mBAAmB;AACxB;AAAA,IACF;AAEA,UAAMC,IAAQ,MAAMhC,EAAsBF,GAAWiC,EAAK,YAAY;AACtE,QAAI,CAACC,EAAM;AACT,kBAAMrC,EAAyBoC,GAAM;AAAA,QACnC,gBAAgBC,EAAM;AAAA,QACtB,cAAcA,EAAM;AAAA,QACpB,WAAW,OAAOlC,KAAc,WAAWA,IAAYA,EAAU;AAAA,MAAA,CAClE,GACK,IAAIjC;AAAA,QACR;AAAA,QACAmE,EAAM;AAAA,QACNA,EAAM;AAAA,QACND,EAAK;AAAA,MAAA;AAIT,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEQ,eAAqB;AAC3B,QAAI,KAAK,OAAQ;AACjB,UAAMhC,IAAM,KAAK,aAAae,EAAA;AAC9B,QAAI,CAACf;AACH,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAGJ,SAAK,SAAS,IAAI,OAAOA,GAAK,EAAE,MAAM,UAAU;AAAA,EAClD;AACF;AC1OO,MAAMkC,EAA2C;AAAA,EAC9C;AAAA,EAER,YAAYjB,IAA8B,IAAI;AAC5C,SAAK,UAAU,IAAID,EAAYC,CAAO;AAAA,EACxC;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA,EAEA,OAAsB;AACpB,WAAO,KAAK,QAAQ,KAAA;AAAA,EACtB;AAAA,EAEA,KAAKrC,GAAwC;AAC3C,WAAO,KAAK,QAAQ,KAAKA,CAAI;AAAA,EAC/B;AAAA,EAEA,MAAM,OAAOA,GAAmBqC,GAA+C;AAC7E,UAAM5C,IAAO,MAAM,KAAK,QAAQ,KAAKO,CAAI;AACzC,QAAI,CAACP,EAAK;AACR,YAAM,IAAI8D,EAAwB9D,EAAK,UAAU,iBAAiBA,CAAI;AAGxE,UAAM2D,IAAM,KAAK,QAAQ,YAAA;AACzB,QAAII;AAEJ,QAAIJ,KAAO,CAACf,EAAQ,WAAW;AAC7B,YAAM9B,IAAe,KAAK,QAAQ;AAClC,UAAI,CAACA;AACH,cAAM,IAAIhB,EAAgB,iBAAiB,uBAAuBE,CAAI;AAGxE,MAAA+D,KADc,MAAMnD,EAAmBL,GAAMP,GAAM2D,GAAK7C,CAAY,GACjD;AAAA,IACrB;AAEA,UAAMkD,IAAYpB,EAAQ,aAAa,QACjCqB,IAASrB,EAAQ,UAAU,OAC3BsB,IAAW1B,EAAcjC,CAAI,GAC7B4D,IAAcvB,EAAQ,oBAAoB,iBAE1CwB,IAAK,IAAI,SAAA;AAEf,QADAA,EAAG,OAAOJ,GAAWzD,GAAM2D,CAAQ,GAC/BtB,EAAQ;AACV,iBAAW,CAACyB,GAAGC,CAAC,KAAK,OAAO,QAAQ1B,EAAQ,WAAW;AACrD,QAAAwB,EAAG,OAAOC,GAAGC,CAAC;AAIlB,UAAMC,IAAkC,EAAE,GAAI3B,EAAQ,WAAW,CAAA,EAAC;AAClE,IAAImB,MAAYQ,EAAQJ,CAAW,IAAIJ;AAEvC,UAAM9C,IAAM,MAAM,MAAM2B,EAAQ,UAAU;AAAA,MACxC,QAAQ;AAAA,MACR,SAAA2B;AAAA,MACA,MAAMH;AAAA,MACN,aAAaxB,EAAQ;AAAA,IAAA,CACtB,GAEK1B,IAAU,MAAMD,EAAI,KAAA;AAC1B,QAAIuD;AACJ,QAAI;AACF,MAAAA,IAAM,KAAK,MAAMtD,CAAO;AAAA,IAC1B,QAAQ;AACN,YAAM,IAAIuD;AAAA,QACR,4BAA4BxD,EAAI,MAAM;AAAA,QACtCA,EAAI;AAAA,QACJC;AAAA,QACAlB;AAAA,MAAA;AAAA,IAEJ;AAEA,UAAMgB,IAAOwD,GACP7C,IAAMX,EAAKiD,CAAM;AACvB,QAAI,CAAChD,EAAI,MAAM,OAAOU,KAAQ,YAAY,CAACA,GAAK;AAC9C,YAAM+C,IACH,OAAO1D,EAAK,SAAU,YAAYA,EAAK,SACvC,OAAOA,EAAK,WAAY,YAAYA,EAAK,WAC1C,gBAAgBC,EAAI,MAAM;AAC5B,YAAM,IAAIwD,EAAqBC,GAAQzD,EAAI,QAAQuD,GAAKxE,CAAI;AAAA,IAC9D;AAEA,WAAO,EAAE,KAAA2B,GAAK,MAAA3B,GAAM,KAAAwE,GAAK,YAAAT,EAAA;AAAA,EAC3B;AAAA,EAEA,UAAgB;AACd,SAAK,QAAQ,QAAA;AAAA,EACf;AACF;AAGO,MAAMD,UAAgC,MAAM;AAAA,EACxC;AAAA,EAET,YAAYpE,GAAiBM,GAAkB;AAC7C,UAAMN,CAAO,GACb,KAAK,OAAO,2BACZ,KAAK,OAAOM;AAAA,EACd;AACF;AAGO,MAAMyE,UAA6B,MAAM;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY/E,GAAiBiF,GAAgB3D,GAAehB,GAAkB;AAC5E,UAAMN,CAAO,GACb,KAAK,OAAO,wBACZ,KAAK,SAASiF,GACd,KAAK,OAAO3D,GACZ,KAAK,OAAOhB;AAAA,EACd;AACF;AAGO,SAAS4E,EAAkBhC,GAAmD;AACnF,SAAO,IAAIiB,EAAYjB,CAAO;AAChC;ACjIA,eAAsBiC,EAAkBlD,IAAM,wBAAgD;AAC5F,QAAMV,IAAM,MAAM,MAAMU,GAAK,EAAE,OAAO,YAAY;AAClD,MAAI,CAACV,EAAI,GAAI,OAAM,IAAI,MAAM,8BAA8BA,EAAI,MAAM,EAAE;AACvE,QAAME,IAAQ,MAAMF,EAAI,KAAA;AACxB,MAAI,CAACE,EAAK,WAAW,CAACA,EAAK;AACzB,UAAM,IAAI,MAAM,iDAAiD;AAEnE,SAAOA;AACT;AAEO,SAAS2D,EACdC,GACAC,GACqB;AACrB,SAAO;AAAA,IACL,SAASD,EAAS;AAAA,IAClB,cAAcA,EAAS;AAAA,IACvB,eAAeC,EAAU;AAAA,IACzB,gBAAgBA,EAAU;AAAA,IAC1B,cAAc;AAAA,EAAA;AAElB;AAEA,eAAsBC,EACpBC,GACAF,GACAG,GAC6B;AAC7B,QAAMJ,IAAW,MAAMF,EAAkBK,CAAW;AACpD,SAAO;AAAA,IACL,GAAGC;AAAA,IACH,UAAUL,EAAqBC,GAAUC,CAAS;AAAA,EAAA;AAEtD;ACrBA,SAASI,EAAaC,GAA2B;AAC/C,MAAIC,IAAM;AACV,aAAWjF,KAAKgF,EAAO,CAAAC,KAAO,OAAO,aAAajF,CAAC;AACnD,SAAO,KAAKiF,CAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC5E;AAEA,SAASC,EAAaC,GAAuB;AAC3C,QAAMC,IAAMD,EAAE,SAAS,MAAM,IAAI,KAAK,IAAI,OAAO,IAAKA,EAAE,SAAS,CAAE,GAC7DE,IAAMF,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,IAAIC,GAChDH,IAAM,KAAKI,CAAG,GACdC,IAAM,IAAI,WAAWL,EAAI,MAAM;AACrC,WAASM,IAAI,GAAGA,IAAIN,EAAI,QAAQM,IAAK,CAAAD,EAAIC,CAAC,IAAIN,EAAI,WAAWM,CAAC;AAC9D,SAAOD;AACT;AAEA,eAAeE,EAASC,GAAgBpG,GAAkC;AACxE,QAAMqG,IAAM,MAAM,OAAO,OAAO;AAAA,IAC9B;AAAA,IACA,IAAI,YAAA,EAAc,OAAOD,CAAM;AAAA,IAC/B,EAAE,MAAM,QAAQ,MAAM,UAAA;AAAA,IACtB;AAAA,IACA,CAAC,MAAM;AAAA,EAAA,GAEHE,IAAM,MAAM,OAAO,OAAO,KAAK,QAAQD,GAAK,IAAI,YAAA,EAAc,OAAOrG,CAAO,CAAC;AACnF,SAAO0F,EAAa,IAAI,WAAWY,CAAG,CAAC;AACzC;AAEO,SAASC,EAAsBC,GAA8B;AAClE,SAAO,CAACA,EAAE,GAAGA,EAAE,KAAKA,EAAE,IAAIA,EAAE,IAAIA,EAAE,GAAG,QAAQ,CAAC,GAAG,OAAOA,EAAE,GAAG,GAAGA,EAAE,KAAK,EAAE,KAAK,GAAG;AACnF;AAEA,eAAsBC,EACpBL,GACA3C,GACiB;AACjB,QAAMnC,IAAOoE,EAAa,IAAI,YAAA,EAAc,OAAO,KAAK,UAAUjC,CAAO,CAAC,CAAC,GACrE6C,IAAM,MAAMH,EAASC,GAAQG,EAAsB9C,CAAO,CAAC;AACjE,SAAO,GAAGnC,CAAI,IAAIgF,CAAG;AACvB;AAEA,eAAsBI,EACpBN,GACAzE,GACAgF,IAAQ,KAAK,OACmG;AAChH,QAAMC,IAAMjF,EAAM,YAAY,GAAG;AACjC,MAAIiF,KAAO,EAAG,QAAO,EAAE,IAAI,IAAO,QAAQ,kBAAA;AAE1C,MAAInD;AACJ,MAAI;AACF,UAAMhC,IAAO,IAAI,YAAA,EAAc,OAAOoE,EAAalE,EAAM,MAAM,GAAGiF,CAAG,CAAC,CAAC;AACvE,IAAAnD,IAAU,KAAK,MAAMhC,CAAI;AAAA,EAC3B,QAAQ;AACN,WAAO,EAAE,IAAI,IAAO,QAAQ,kBAAA;AAAA,EAC9B;AAEA,MAAIgC,EAAQ,MAAM,EAAG,QAAO,EAAE,IAAI,IAAO,QAAQ,uBAAuB,SAAAA,EAAA;AACxE,MAAIA,EAAQ,MAAM,MAAOkD,EAAO,QAAO,EAAE,IAAI,IAAO,QAAQ,WAAW,SAAAlD,EAAA;AAEvE,QAAMoD,IAAc,MAAMV,EAASC,GAAQG,EAAsB9C,CAAO,CAAC,GACnEqD,IAAYnF,EAAM,MAAMiF,IAAM,CAAC;AACrC,MAAIC,EAAY,WAAWC,EAAU;AACnC,WAAO,EAAE,IAAI,IAAO,QAAQ,sBAAsB,SAAArD,EAAA;AAEpD,MAAIsD,IAAO;AACX,WAAS,IAAI,GAAG,IAAIF,EAAY,QAAQ;AACtC,IAAAE,KAAQF,EAAY,WAAW,CAAC,IAAIC,EAAU,WAAW,CAAC;AAE5D,SAAIC,MAAS,IAAU,EAAE,IAAI,IAAO,QAAQ,sBAAsB,SAAAtD,EAAA,IAE3D,EAAE,IAAI,IAAM,SAAAA,EAAA;AACrB;AChDO,MAAMuD,IAAmB;"}
@@ -0,0 +1,12 @@
1
+ import type { ScanResult } from '../types';
2
+ export declare class GuardIntegrityError extends Error {
3
+ readonly buildId?: string;
4
+ readonly expectedSha256: string;
5
+ readonly actualSha256: string;
6
+ constructor(message: string, expectedSha256: string, actualSha256: string, buildId?: string);
7
+ }
8
+ export declare class GuardProofError extends Error {
9
+ readonly code: string;
10
+ readonly scan?: ScanResult;
11
+ constructor(message: string, code: string, scan?: ScanResult);
12
+ }
@@ -0,0 +1,2 @@
1
+ /** 업로드 파일 바인딩용 fingerprint (크기·타입·앞 64KB) */
2
+ export declare function fileFingerprint(file: Blob): Promise<string>;
@@ -0,0 +1,11 @@
1
+ import type { GuardManifest, GuardSecurityConfig, GuardUploadOptions } from '../types';
2
+ /** `/guard-manifest.json` 로드 */
3
+ export declare function loadGuardManifest(url?: string): Promise<GuardManifest>;
4
+ export declare function securityFromManifest(manifest: GuardManifest, endpoints: {
5
+ proofEndpoint: string;
6
+ reportEndpoint?: string;
7
+ }): GuardSecurityConfig;
8
+ export declare function createGuardUploadOptions(manifestUrl: string, endpoints: {
9
+ proofEndpoint: string;
10
+ reportEndpoint?: string;
11
+ }, base?: GuardUploadOptions): Promise<GuardUploadOptions>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 업로드 proof 토큰 — 서버·클라이언트 공용 (HMAC-SHA256)
3
+ * 형식: base64url(JSON payload).base64url(signature)
4
+ */
5
+ export interface GuardProofPayload {
6
+ v: 1;
7
+ bid: string;
8
+ wh: string;
9
+ fp: string;
10
+ ns: number;
11
+ exp: number;
12
+ nonce: string;
13
+ }
14
+ export declare function canonicalProofMessage(p: GuardProofPayload): string;
15
+ export declare function signProofPayload(secret: string, payload: GuardProofPayload): Promise<string>;
16
+ export declare function verifyProofToken(secret: string, token: string, nowMs?: number): Promise<{
17
+ ok: true;
18
+ payload: GuardProofPayload;
19
+ } | {
20
+ ok: false;
21
+ reason: string;
22
+ payload?: GuardProofPayload;
23
+ }>;
@@ -0,0 +1,12 @@
1
+ import type { ScanResult } from '../types';
2
+ import type { GuardSecurityConfig } from '../types';
3
+ export interface ProofResponse {
4
+ token: string;
5
+ exp: number;
6
+ }
7
+ export declare function requestUploadProof(file: Blob, scan: ScanResult, security: GuardSecurityConfig, workerSha256: string): Promise<ProofResponse>;
8
+ export declare function reportIntegrityViolation(security: GuardSecurityConfig, detail: {
9
+ expectedSha256: string;
10
+ actualSha256: string;
11
+ workerUrl: string;
12
+ }): Promise<void>;
@@ -0,0 +1,3 @@
1
+ /** ArrayBuffer → SHA-256 hex (브라우저 Web Crypto) */
2
+ export declare function sha256Hex(data: ArrayBuffer | Uint8Array): Promise<string>;
3
+ export declare function sha256HexFromString(text: string): Promise<string>;
@@ -0,0 +1,9 @@
1
+ export declare function hashWorkerScript(workerUrl: string | URL): Promise<string>;
2
+ export declare function verifyWorkerIntegrity(workerUrl: string | URL, expectedSha256: string): Promise<{
3
+ ok: true;
4
+ actualSha256: string;
5
+ } | {
6
+ ok: false;
7
+ actualSha256: string;
8
+ expectedSha256: string;
9
+ }>;