@cloudware-de/gatekeeper-widget 0.0.0-stage → 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/LICENSE +17 -0
- package/dist/client/instance.d.ts +35 -0
- package/dist/client/instance.d.ts.map +1 -0
- package/dist/client/instance.js +93 -0
- package/dist/client/instance.js.map +1 -0
- package/dist/client/transport.d.ts +19 -0
- package/dist/client/transport.d.ts.map +1 -0
- package/dist/client/transport.js +41 -0
- package/dist/client/transport.js.map +1 -0
- package/dist/client/urls.d.ts +12 -0
- package/dist/client/urls.d.ts.map +1 -0
- package/dist/client/urls.js +27 -0
- package/dist/client/urls.js.map +1 -0
- package/dist/copy/load-notice.d.ts +8 -0
- package/dist/copy/load-notice.d.ts.map +1 -0
- package/dist/copy/load-notice.js +8 -0
- package/dist/copy/load-notice.js.map +1 -0
- package/dist/element/honeypot.d.ts +5 -0
- package/dist/element/honeypot.d.ts.map +1 -0
- package/dist/element/honeypot.js +5 -0
- package/dist/element/honeypot.js.map +1 -0
- package/dist/element/session.d.ts +32 -0
- package/dist/element/session.d.ts.map +1 -0
- package/dist/element/session.js +90 -0
- package/dist/element/session.js.map +1 -0
- package/dist/element/start.d.ts +25 -0
- package/dist/element/start.d.ts.map +1 -0
- package/dist/element/start.js +85 -0
- package/dist/element/start.js.map +1 -0
- package/dist/element/states.d.ts +26 -0
- package/dist/element/states.d.ts.map +1 -0
- package/dist/element/states.js +58 -0
- package/dist/element/states.js.map +1 -0
- package/dist/element/templates/host.d.ts +7 -0
- package/dist/element/templates/host.d.ts.map +1 -0
- package/dist/element/templates/host.js +11 -0
- package/dist/element/templates/host.js.map +1 -0
- package/dist/element/templates/names.d.ts +11 -0
- package/dist/element/templates/names.d.ts.map +1 -0
- package/dist/element/templates/names.js +20 -0
- package/dist/element/templates/names.js.map +1 -0
- package/dist/element/templates/sheets.d.ts +10 -0
- package/dist/element/templates/sheets.d.ts.map +1 -0
- package/dist/element/templates/sheets.js +26 -0
- package/dist/element/templates/sheets.js.map +1 -0
- package/dist/element/widget.d.ts +75 -0
- package/dist/element/widget.d.ts.map +1 -0
- package/dist/element/widget.js +852 -0
- package/dist/element/widget.js.map +1 -0
- package/dist/envelope/run.d.ts +20 -0
- package/dist/envelope/run.d.ts.map +1 -0
- package/dist/envelope/run.js +104 -0
- package/dist/envelope/run.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/pow/messages.d.ts +24 -0
- package/dist/pow/messages.d.ts.map +1 -0
- package/dist/pow/messages.js +2 -0
- package/dist/pow/messages.js.map +1 -0
- package/dist/pow/oom.d.ts +7 -0
- package/dist/pow/oom.d.ts.map +1 -0
- package/dist/pow/oom.js +19 -0
- package/dist/pow/oom.js.map +1 -0
- package/dist/pow/run.d.ts +13 -0
- package/dist/pow/run.d.ts.map +1 -0
- package/dist/pow/run.js +73 -0
- package/dist/pow/run.js.map +1 -0
- package/dist/pow/solve.d.ts +32 -0
- package/dist/pow/solve.d.ts.map +1 -0
- package/dist/pow/solve.js +114 -0
- package/dist/pow/solve.js.map +1 -0
- package/dist/pow/wasm-url.d.ts +13 -0
- package/dist/pow/wasm-url.d.ts.map +1 -0
- package/dist/pow/wasm-url.js +16 -0
- package/dist/pow/wasm-url.js.map +1 -0
- package/dist/pow/worker.d.ts +2 -0
- package/dist/pow/worker.d.ts.map +1 -0
- package/dist/pow/worker.js +43 -0
- package/dist/pow/worker.js.map +1 -0
- package/dist/runtime/workers.d.ts +11 -0
- package/dist/runtime/workers.d.ts.map +1 -0
- package/dist/runtime/workers.js +36 -0
- package/dist/runtime/workers.js.map +1 -0
- package/package.json +33 -3
- package/src/client/instance.ts +140 -0
- package/src/client/transport.ts +55 -0
- package/src/client/urls.ts +38 -0
- package/src/copy/load-notice.ts +8 -0
- package/src/element/honeypot.ts +4 -0
- package/src/element/session.ts +131 -0
- package/src/element/start.ts +99 -0
- package/src/element/states.ts +73 -0
- package/src/element/templates/compact.css +4 -0
- package/src/element/templates/contrast.css +5 -0
- package/src/element/templates/host.ts +11 -0
- package/src/element/templates/names.ts +23 -0
- package/src/element/templates/sheets.ts +28 -0
- package/src/element/templates/standard.css +5 -0
- package/src/element/widget.ts +982 -0
- package/src/envelope/run.ts +126 -0
- package/src/index.ts +77 -0
- package/src/pow/messages.ts +21 -0
- package/src/pow/oom.ts +20 -0
- package/src/pow/run.ts +78 -0
- package/src/pow/solve.ts +136 -0
- package/src/pow/wasm-url.ts +27 -0
- package/src/pow/worker.ts +46 -0
- package/src/runtime/workers.ts +40 -0
- package/README.md +0 -3
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Envelope-Hülle: geliefertes Programm isoliert ausführen, Attestation zurück.
|
|
3
|
+
* Spec: specs/002-widget/tasks.md T-208
|
|
4
|
+
* Spec: specs/002-widget/spec.md §Pflichtumfang §Formular-Kanten
|
|
5
|
+
* ADR-012: Schnittstelle, kein Generator-Inhalt, keine Rohsignal-Persistenz.
|
|
6
|
+
*/
|
|
7
|
+
import { isSecurityError, trackWorker, untrackWorker } from "../runtime/workers.js";
|
|
8
|
+
export interface DeliveredProgram {
|
|
9
|
+
readonly programId: string;
|
|
10
|
+
readonly programVersion: string;
|
|
11
|
+
readonly source: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type IsolatedRunner = (program: DeliveredProgram) => Promise<Uint8Array>;
|
|
15
|
+
|
|
16
|
+
export class EnvelopeIsolationError extends Error {
|
|
17
|
+
readonly code: "malformed_encoding" | "internal_error" | "unsupported_algorithm";
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
code: "malformed_encoding" | "internal_error" | "unsupported_algorithm",
|
|
21
|
+
message: string,
|
|
22
|
+
) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "EnvelopeIsolationError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function requireAttestation(bytes: Uint8Array): Uint8Array {
|
|
30
|
+
if (bytes.byteLength < 1) {
|
|
31
|
+
throw new EnvelopeIsolationError("malformed_encoding", "attestation must not be empty");
|
|
32
|
+
}
|
|
33
|
+
return bytes;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireProgram(program: DeliveredProgram): void {
|
|
37
|
+
if (program.programId.trim() === "") {
|
|
38
|
+
throw new EnvelopeIsolationError("malformed_encoding", "programId must not be empty");
|
|
39
|
+
}
|
|
40
|
+
if (program.programVersion.trim() === "") {
|
|
41
|
+
throw new EnvelopeIsolationError("malformed_encoding", "programVersion must not be empty");
|
|
42
|
+
}
|
|
43
|
+
if (program.source.length < 1) {
|
|
44
|
+
throw new EnvelopeIsolationError("malformed_encoding", "program source must not be empty");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readAttestation(data: unknown): Uint8Array {
|
|
49
|
+
if (data instanceof Uint8Array) {
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
if (data instanceof ArrayBuffer) {
|
|
53
|
+
return new Uint8Array(data);
|
|
54
|
+
}
|
|
55
|
+
if (data !== null && typeof data === "object" && "attestation" in data) {
|
|
56
|
+
return readAttestation((data as { attestation: unknown }).attestation);
|
|
57
|
+
}
|
|
58
|
+
throw new EnvelopeIsolationError("malformed_encoding", "program did not return attestation");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Browser-Default: Programm als Worker-Skript (kein eval, kein parent-Storage).
|
|
63
|
+
*/
|
|
64
|
+
export function workerRunner(program: DeliveredProgram, timeoutMs = 2000): Promise<Uint8Array> {
|
|
65
|
+
if (typeof Worker !== "function" || typeof Blob !== "function") {
|
|
66
|
+
return Promise.reject(new EnvelopeIsolationError("internal_error", "isolated worker is not available"));
|
|
67
|
+
}
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const blob = new Blob([program.source], { type: "text/javascript" });
|
|
70
|
+
const url = URL.createObjectURL(blob);
|
|
71
|
+
let worker: Worker;
|
|
72
|
+
try {
|
|
73
|
+
worker = new Worker(url);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
URL.revokeObjectURL(url);
|
|
76
|
+
if (isSecurityError(error)) {
|
|
77
|
+
reject(
|
|
78
|
+
new EnvelopeIsolationError(
|
|
79
|
+
"unsupported_algorithm",
|
|
80
|
+
"blob worker blocked by CSP",
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
reject(new EnvelopeIsolationError("internal_error", "isolated worker failed to start"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
trackWorker(worker);
|
|
89
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
90
|
+
const finish = (fn: () => void): void => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
untrackWorker(worker);
|
|
93
|
+
worker.terminate();
|
|
94
|
+
URL.revokeObjectURL(url);
|
|
95
|
+
fn();
|
|
96
|
+
};
|
|
97
|
+
timer = setTimeout(() => {
|
|
98
|
+
finish(() => {
|
|
99
|
+
reject(new EnvelopeIsolationError("internal_error", "isolated program timed out"));
|
|
100
|
+
});
|
|
101
|
+
}, timeoutMs);
|
|
102
|
+
worker.onmessage = (event: MessageEvent<unknown>) => {
|
|
103
|
+
finish(() => {
|
|
104
|
+
try {
|
|
105
|
+
resolve(requireAttestation(readAttestation(event.data)));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
reject(error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
worker.onerror = () => {
|
|
112
|
+
finish(() => {
|
|
113
|
+
reject(new EnvelopeIsolationError("internal_error", "isolated program failed"));
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function runDeliveredProgram(
|
|
120
|
+
program: DeliveredProgram,
|
|
121
|
+
options: { readonly runner?: IsolatedRunner; readonly timeoutMs?: number } = {},
|
|
122
|
+
): Promise<Uint8Array> {
|
|
123
|
+
requireProgram(program);
|
|
124
|
+
const runner = options.runner ?? ((item) => workerRunner(item, options.timeoutMs));
|
|
125
|
+
return requireAttestation(await runner(program));
|
|
126
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cloudware-de/gatekeeper-widget öffentlicher Einstieg. Tag: gatekeep-widget.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §Öffentliches Custom-Element-API
|
|
4
|
+
*/
|
|
5
|
+
import { defineGatekeepWidget } from "./element/widget.js";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
createInstanceClient,
|
|
9
|
+
InstanceClientError,
|
|
10
|
+
type ChallengeIssueRequest,
|
|
11
|
+
type InstanceClient,
|
|
12
|
+
type InstanceClientOptions,
|
|
13
|
+
type RedeemSuccess,
|
|
14
|
+
} from "./client/instance.js";
|
|
15
|
+
export { fetchTransport, type Transport, type TransportRequest, type TransportResponse } from "./client/transport.js";
|
|
16
|
+
export { InstanceUrlError, instanceRouteUrl, type InstanceRoute } from "./client/urls.js";
|
|
17
|
+
export { LOAD_NOTICE } from "./copy/load-notice.js";
|
|
18
|
+
export {
|
|
19
|
+
ARIA_LABELS,
|
|
20
|
+
CHALLENGE_EXPIRED_MESSAGE,
|
|
21
|
+
TOKEN_MISSING_MESSAGE,
|
|
22
|
+
WIDGET_UI_STATES,
|
|
23
|
+
challengeExpiredMessage,
|
|
24
|
+
isWidgetUiState,
|
|
25
|
+
labelForHost,
|
|
26
|
+
labelForState,
|
|
27
|
+
missingTokenMessage,
|
|
28
|
+
type WidgetUiState,
|
|
29
|
+
} from "./element/states.js";
|
|
30
|
+
export {
|
|
31
|
+
attestChallenge,
|
|
32
|
+
createBoundClient,
|
|
33
|
+
mergeRedeemBody,
|
|
34
|
+
programFromChallenge,
|
|
35
|
+
readBaseUrl,
|
|
36
|
+
readSiteKey,
|
|
37
|
+
readWasmUrl,
|
|
38
|
+
type RedeemIssueContext,
|
|
39
|
+
} from "./element/session.js";
|
|
40
|
+
export { HONEYPOT_FIELD } from "./element/honeypot.js";
|
|
41
|
+
export {
|
|
42
|
+
TEMPLATE_COMPACT,
|
|
43
|
+
TEMPLATE_CONTRAST,
|
|
44
|
+
TEMPLATE_STANDARD,
|
|
45
|
+
WIDGET_TEMPLATES,
|
|
46
|
+
parseTemplate,
|
|
47
|
+
type WidgetTemplate,
|
|
48
|
+
} from "./element/templates/names.js";
|
|
49
|
+
export {
|
|
50
|
+
HARD_TIMEOUT_MS,
|
|
51
|
+
TOKEN_TTL_MS,
|
|
52
|
+
expiresAtOf,
|
|
53
|
+
isChallengeExpired,
|
|
54
|
+
parseStartMode,
|
|
55
|
+
parseSubmitHold,
|
|
56
|
+
readSubmitHold,
|
|
57
|
+
type StartMode,
|
|
58
|
+
} from "./element/start.js";
|
|
59
|
+
export {
|
|
60
|
+
isSecurityError,
|
|
61
|
+
terminateTrackedWorkers,
|
|
62
|
+
trackWorker,
|
|
63
|
+
trackedWorkerCount,
|
|
64
|
+
untrackWorker,
|
|
65
|
+
} from "./runtime/workers.js";
|
|
66
|
+
export {
|
|
67
|
+
GatekeepWidget,
|
|
68
|
+
WidgetRedeemError,
|
|
69
|
+
defineGatekeepWidget,
|
|
70
|
+
type DelayFn,
|
|
71
|
+
type DelayHandle,
|
|
72
|
+
type WidgetPrepareHooks,
|
|
73
|
+
type WidgetProgressDetail,
|
|
74
|
+
type WidgetRedeemClient,
|
|
75
|
+
} from "./element/widget.js";
|
|
76
|
+
|
|
77
|
+
defineGatekeepWidget();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-Kanal: Challenge rein → Fortschritt → Solution raus.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §WASM-Toolchain und Auslieferung
|
|
4
|
+
*/
|
|
5
|
+
import type { ChallengeParams, Solution } from "@cloudware-de/gatekeeper-protocol";
|
|
6
|
+
|
|
7
|
+
export interface PowProgress {
|
|
8
|
+
readonly progress: number;
|
|
9
|
+
readonly completed: number;
|
|
10
|
+
readonly total: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PowSolveRequest {
|
|
14
|
+
readonly type: "solve";
|
|
15
|
+
readonly challenge: ChallengeParams;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type PowWorkerEvent =
|
|
19
|
+
| ({ readonly type: "progress" } & PowProgress)
|
|
20
|
+
| { readonly type: "solution"; readonly solution: Solution }
|
|
21
|
+
| { readonly type: "error"; readonly errorCodes: readonly string[] };
|
package/src/pow/oom.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM-Alloc/OOM erkennen, ohne neuen Fehlercode.
|
|
3
|
+
* Spec: specs/001-protocol-core/tasks.md T-036
|
|
4
|
+
* Spec: specs/002-widget/spec.md Pflichtumfang Start / Timeout
|
|
5
|
+
*/
|
|
6
|
+
export function isWasmOom(error: unknown): boolean {
|
|
7
|
+
if (error instanceof RangeError) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (!(error instanceof Error) || error.name === "AbortError") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (error.name === "RuntimeError" || error.name === "WebAssembly.RuntimeError") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
const text = `${error.name} ${error.message}`.toLowerCase();
|
|
17
|
+
return /out of memory|\boom\b|cannot allocate|allocation failed|memory access out of bounds/.test(
|
|
18
|
+
text,
|
|
19
|
+
);
|
|
20
|
+
}
|
package/src/pow/run.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-Standard, Hauptthread-Fallback ohne Worker.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §WASM-Toolchain und Auslieferung
|
|
4
|
+
* Spec: specs/002-widget/tasks.md T-207
|
|
5
|
+
* Spec: specs/002-widget/spec.md §Formular-Kanten (SecurityError)
|
|
6
|
+
*/
|
|
7
|
+
import { GatekeepError, type ChallengeParams, type Solution } from "@cloudware-de/gatekeeper-protocol";
|
|
8
|
+
import { isSecurityError, trackWorker, untrackWorker } from "../runtime/workers.js";
|
|
9
|
+
import type { PowSolveRequest, PowWorkerEvent } from "./messages.js";
|
|
10
|
+
import { solveChallenge, type SolveOptions } from "./solve.js";
|
|
11
|
+
|
|
12
|
+
export function workerAvailable(): boolean {
|
|
13
|
+
return typeof Worker === "function";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function solveInWorker(challenge: ChallengeParams, options: SolveOptions): Promise<Solution> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
let worker: Worker;
|
|
19
|
+
try {
|
|
20
|
+
worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
reject(error);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
trackWorker(worker);
|
|
26
|
+
const finish = (fn: () => void): void => {
|
|
27
|
+
untrackWorker(worker);
|
|
28
|
+
worker.terminate();
|
|
29
|
+
fn();
|
|
30
|
+
};
|
|
31
|
+
const request: PowSolveRequest = { type: "solve", challenge };
|
|
32
|
+
worker.onmessage = (event: MessageEvent<PowWorkerEvent>) => {
|
|
33
|
+
const data = event.data;
|
|
34
|
+
if (data.type === "progress") {
|
|
35
|
+
options.onProgress?.({
|
|
36
|
+
progress: data.progress,
|
|
37
|
+
completed: data.completed,
|
|
38
|
+
total: data.total,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (data.type === "solution") {
|
|
43
|
+
finish(() => resolve(data.solution));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const first = data.errorCodes[0];
|
|
47
|
+
const code =
|
|
48
|
+
first === "unsupported_algorithm" || first === "challenge_timeout"
|
|
49
|
+
? first
|
|
50
|
+
: "internal_error";
|
|
51
|
+
finish(() => reject(new GatekeepError(code, "pow worker failed")));
|
|
52
|
+
};
|
|
53
|
+
worker.onerror = () => {
|
|
54
|
+
finish(() => reject(new Error("pow worker failed to start")));
|
|
55
|
+
};
|
|
56
|
+
worker.postMessage(request);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runSolve(
|
|
61
|
+
challenge: ChallengeParams,
|
|
62
|
+
options: SolveOptions & { readonly preferWorker?: boolean } = {},
|
|
63
|
+
): Promise<Solution> {
|
|
64
|
+
const preferWorker = options.preferWorker !== false;
|
|
65
|
+
if (preferWorker && workerAvailable()) {
|
|
66
|
+
try {
|
|
67
|
+
return await solveInWorker(challenge, options);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof GatekeepError && error.code === "unsupported_algorithm") {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if (isSecurityError(error)) {
|
|
73
|
+
return solveChallenge(challenge, options);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return solveChallenge(challenge, options);
|
|
78
|
+
}
|
package/src/pow/solve.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministische PoW-Lösung über @cloudware-de/gatekeeper-protocol (ADR-007).
|
|
3
|
+
* Spec: specs/002-widget/tasks.md T-207
|
|
4
|
+
* Spec: specs/001-protocol-core/spec.md#proof-of-work-deterministischer-modus-als-vorgabe
|
|
5
|
+
*
|
|
6
|
+
* keyPrefix ist kein Challenge-Feld (ADR-005). verifySolution prüft, dass
|
|
7
|
+
* der Counter das claimed derivedKey ergibt (eine KDF je Teillösung).
|
|
8
|
+
* Der ehrliche Client läuft `subCount × LEVEL_CONFIGS.counter` KDF-Runden
|
|
9
|
+
* (Kalibrierung: c=5/15/30, 32 MiB) — nicht `counter = index`.
|
|
10
|
+
* Password = nonce ‖ u64be(counter). Fehlendes WASM / unbekannte KDF →
|
|
11
|
+
* unsupported_algorithm, nie PBKDF2-Ersatz.
|
|
12
|
+
* Spec: packages/protocol/calibrate/measure.ts
|
|
13
|
+
* Spec: specs/001-protocol-core/spec.md#schwierigkeitsstufen-und-eskalation
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
decodeBase64Url,
|
|
17
|
+
deriveSubSolution,
|
|
18
|
+
GatekeepError,
|
|
19
|
+
isKdfAvailable,
|
|
20
|
+
lookupLevelParams,
|
|
21
|
+
type ChallengeParams,
|
|
22
|
+
type Solution,
|
|
23
|
+
type SubSolution,
|
|
24
|
+
} from "@cloudware-de/gatekeeper-protocol";
|
|
25
|
+
import { isWasmOom } from "./oom.js";
|
|
26
|
+
import type { PowProgress } from "./messages.js";
|
|
27
|
+
|
|
28
|
+
export interface SolveOptions {
|
|
29
|
+
readonly onProgress?: (detail: PowProgress) => void;
|
|
30
|
+
/** Von der CE-Kaskade (wasm-url → GATEKEEP_WASM_URL → import.meta.url). */
|
|
31
|
+
readonly wasmUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function asBytes(value: unknown, label: string): Uint8Array {
|
|
35
|
+
if (value instanceof Uint8Array) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "string" && value.length > 0) {
|
|
39
|
+
return decodeBase64Url(value);
|
|
40
|
+
}
|
|
41
|
+
throw new GatekeepError("malformed_encoding", `${label} must be bytes or base64url`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HTTP issue is JSON (base64url salt/nonce). Tests pass Uint8Arrays.
|
|
46
|
+
*/
|
|
47
|
+
export function hydrateIssuedChallenge(raw: unknown): ChallengeParams {
|
|
48
|
+
if (raw === null || typeof raw !== "object") {
|
|
49
|
+
throw new GatekeepError("malformed_encoding", "challenge must be an object");
|
|
50
|
+
}
|
|
51
|
+
const record = raw as Record<string, unknown>;
|
|
52
|
+
if (typeof record.id !== "string" || typeof record.algorithm !== "string") {
|
|
53
|
+
throw new GatekeepError("malformed_encoding", "challenge id/algorithm required");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
id: record.id,
|
|
57
|
+
siteKey: typeof record.siteKey === "string" ? record.siteKey : "",
|
|
58
|
+
algorithm: record.algorithm,
|
|
59
|
+
salt: asBytes(record.salt, "salt"),
|
|
60
|
+
nonce: asBytes(record.nonce, "nonce"),
|
|
61
|
+
cost: Number(record.cost),
|
|
62
|
+
level: Number(record.level),
|
|
63
|
+
subCount: Number(record.subCount),
|
|
64
|
+
issuedAt: Number(record.issuedAt),
|
|
65
|
+
expiresAt: Number(record.expiresAt),
|
|
66
|
+
instrumentationProgramId:
|
|
67
|
+
typeof record.instrumentationProgramId === "string" ? record.instrumentationProgramId : "",
|
|
68
|
+
downgraded: record.downgraded === true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Search space for one sub-solution. Issued JSON `counter` wins;
|
|
74
|
+
* otherwise LEVEL_CONFIGS / FALLBACK via `lookupLevelParams`.
|
|
75
|
+
*/
|
|
76
|
+
export function searchSpaceFor(raw: unknown): number {
|
|
77
|
+
const record = raw !== null && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
|
78
|
+
const issued = record.counter;
|
|
79
|
+
if (typeof issued === "number" && Number.isInteger(issued) && issued >= 1) {
|
|
80
|
+
return issued;
|
|
81
|
+
}
|
|
82
|
+
const algorithm = typeof record.algorithm === "string" ? record.algorithm : "";
|
|
83
|
+
const level = typeof record.level === "number" ? record.level : Number(record.level);
|
|
84
|
+
const configured = lookupLevelParams(algorithm, level);
|
|
85
|
+
if (configured !== undefined && Number.isInteger(configured.counter) && configured.counter >= 1) {
|
|
86
|
+
return configured.counter;
|
|
87
|
+
}
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function solveChallenge(
|
|
92
|
+
challenge: ChallengeParams | unknown,
|
|
93
|
+
options: SolveOptions = {},
|
|
94
|
+
): Promise<Solution> {
|
|
95
|
+
const issued = hydrateIssuedChallenge(challenge);
|
|
96
|
+
const record = challenge as Record<string, unknown>;
|
|
97
|
+
const memoryCost = typeof record.memoryCost === "number" ? record.memoryCost : undefined;
|
|
98
|
+
const parallelism = typeof record.parallelism === "number" ? record.parallelism : undefined;
|
|
99
|
+
if (!isKdfAvailable(issued.algorithm)) {
|
|
100
|
+
throw new GatekeepError("unsupported_algorithm", `KDF not available: ${issued.algorithm}`);
|
|
101
|
+
}
|
|
102
|
+
const total = issued.subCount;
|
|
103
|
+
if (!Number.isInteger(total) || total < 1) {
|
|
104
|
+
throw new GatekeepError("incomplete_solution", "subCount must be a positive integer");
|
|
105
|
+
}
|
|
106
|
+
const search = searchSpaceFor(challenge);
|
|
107
|
+
const work = total * search;
|
|
108
|
+
const subSolutions: SubSolution[] = [];
|
|
109
|
+
let completed = 0;
|
|
110
|
+
for (let index = 0; index < total; index += 1) {
|
|
111
|
+
let derivedKey: Uint8Array | undefined;
|
|
112
|
+
let usedCounter = 0;
|
|
113
|
+
for (let counter = 0; counter < search; counter += 1) {
|
|
114
|
+
try {
|
|
115
|
+
derivedKey = await deriveSubSolution(issued, counter, { memoryCost, parallelism });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (isWasmOom(error)) {
|
|
118
|
+
throw new GatekeepError("challenge_timeout", "wasm memory allocation failed");
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
usedCounter = counter;
|
|
123
|
+
completed += 1;
|
|
124
|
+
options.onProgress?.({
|
|
125
|
+
progress: completed / work,
|
|
126
|
+
completed,
|
|
127
|
+
total: work,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (derivedKey === undefined) {
|
|
131
|
+
throw new GatekeepError("incomplete_solution", "search space must be a positive integer");
|
|
132
|
+
}
|
|
133
|
+
subSolutions.push({ index, counter: usedCounter, derivedKey });
|
|
134
|
+
}
|
|
135
|
+
return { challengeId: issued.id, subSolutions };
|
|
136
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM-Asset-Kaskade (Artikel 3). Kein Fremd-CDN.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §WASM-Toolchain und Auslieferung
|
|
4
|
+
*/
|
|
5
|
+
export function resolveWasmUrl(
|
|
6
|
+
attribute?: string,
|
|
7
|
+
moduleUrl: string = import.meta.url,
|
|
8
|
+
): string {
|
|
9
|
+
const fromAttr = attribute?.trim() ?? "";
|
|
10
|
+
if (fromAttr.length > 0) {
|
|
11
|
+
return fromAttr;
|
|
12
|
+
}
|
|
13
|
+
const fromWindow =
|
|
14
|
+
typeof window !== "undefined" ? window.GATEKEEP_WASM_URL?.trim() ?? "" : "";
|
|
15
|
+
if (fromWindow.length > 0) {
|
|
16
|
+
return fromWindow;
|
|
17
|
+
}
|
|
18
|
+
return new URL("./gatekeep.wasm", moduleUrl).href;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Window {
|
|
23
|
+
GATEKEEP_WASM_URL?: string;
|
|
24
|
+
/** T-212 Hold. false / "0" / "false" / "off" / "no" schaltet Auto-Submit aus. */
|
|
25
|
+
GATEKEEP_SUBMIT_HOLD?: string | boolean;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedizierter Worker: nur Berechnung über @cloudware-de/gatekeeper-protocol.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §WASM-Toolchain und Auslieferung
|
|
4
|
+
*/
|
|
5
|
+
import { GatekeepError, isErrorCode } from "@cloudware-de/gatekeeper-protocol";
|
|
6
|
+
import type { PowSolveRequest, PowWorkerEvent } from "./messages.js";
|
|
7
|
+
import { isWasmOom } from "./oom.js";
|
|
8
|
+
import { solveChallenge } from "./solve.js";
|
|
9
|
+
|
|
10
|
+
function post(event: PowWorkerEvent): void {
|
|
11
|
+
postMessage(event);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onmessage = (event: MessageEvent<PowSolveRequest>) => {
|
|
15
|
+
try {
|
|
16
|
+
const data = event.data;
|
|
17
|
+
if (data?.type !== "solve") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
void solveChallenge(data.challenge, {
|
|
21
|
+
onProgress: (detail) => {
|
|
22
|
+
post({ type: "progress", ...detail });
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
.then((solution) => {
|
|
26
|
+
post({ type: "solution", solution });
|
|
27
|
+
})
|
|
28
|
+
.catch((error: unknown) => {
|
|
29
|
+
postSolveError(error);
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
postSolveError(error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function postSolveError(error: unknown): void {
|
|
37
|
+
if (isWasmOom(error)) {
|
|
38
|
+
post({ type: "error", errorCodes: ["challenge_timeout"] });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const code =
|
|
42
|
+
error instanceof GatekeepError && isErrorCode(error.code)
|
|
43
|
+
? error.code
|
|
44
|
+
: "internal_error";
|
|
45
|
+
post({ type: "error", errorCodes: [code] });
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lebende Worker: Track, Terminate, SecurityError.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §Formular-Kanten T-212
|
|
4
|
+
* Envelope-Blob und PoW-Modul-Worker teilen dieselbe Menge.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const live = new Set<Worker>();
|
|
8
|
+
|
|
9
|
+
export function isSecurityError(error: unknown): boolean {
|
|
10
|
+
if (error instanceof DOMException) {
|
|
11
|
+
return error.name === "SecurityError";
|
|
12
|
+
}
|
|
13
|
+
return error instanceof Error && error.name === "SecurityError";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function trackWorker(worker: Worker): Worker {
|
|
17
|
+
live.add(worker);
|
|
18
|
+
return worker;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function untrackWorker(worker: Worker): void {
|
|
22
|
+
live.delete(worker);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function terminateTrackedWorkers(): number {
|
|
26
|
+
const count = live.size;
|
|
27
|
+
for (const worker of live) {
|
|
28
|
+
try {
|
|
29
|
+
worker.terminate();
|
|
30
|
+
} catch {
|
|
31
|
+
// bereits tot
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
live.clear();
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function trackedWorkerCount(): number {
|
|
39
|
+
return live.size;
|
|
40
|
+
}
|
package/README.md
DELETED