@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,43 @@
|
|
|
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 { isWasmOom } from "./oom.js";
|
|
7
|
+
import { solveChallenge } from "./solve.js";
|
|
8
|
+
function post(event) {
|
|
9
|
+
postMessage(event);
|
|
10
|
+
}
|
|
11
|
+
onmessage = (event) => {
|
|
12
|
+
try {
|
|
13
|
+
const data = event.data;
|
|
14
|
+
if (data?.type !== "solve") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
void solveChallenge(data.challenge, {
|
|
18
|
+
onProgress: (detail) => {
|
|
19
|
+
post({ type: "progress", ...detail });
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
.then((solution) => {
|
|
23
|
+
post({ type: "solution", solution });
|
|
24
|
+
})
|
|
25
|
+
.catch((error) => {
|
|
26
|
+
postSolveError(error);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
postSolveError(error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
function postSolveError(error) {
|
|
34
|
+
if (isWasmOom(error)) {
|
|
35
|
+
post({ type: "error", errorCodes: ["challenge_timeout"] });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const code = error instanceof GatekeepError && isErrorCode(error.code)
|
|
39
|
+
? error.code
|
|
40
|
+
: "internal_error";
|
|
41
|
+
post({ type: "error", errorCodes: [code] });
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/pow/worker.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAE/E,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,SAAS,IAAI,CAAC,KAAqB;IACjC,WAAW,CAAC,KAAK,CAAC,CAAC;AACrB,CAAC;AAED,SAAS,GAAG,CAAC,KAAoC,EAAE,EAAE;IACnD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACxB,IAAI,IAAI,EAAE,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,KAAK,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE;YAClC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE;gBACrB,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;YACxC,CAAC;SACF,CAAC;aACC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACjB,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;QACvC,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACxB,cAAc,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;IACP,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,cAAc,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;AACH,CAAC,CAAC;AAEF,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GACR,KAAK,YAAY,aAAa,IAAI,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC;QACvD,CAAC,CAAC,KAAK,CAAC,IAAI;QACZ,CAAC,CAAC,gBAAgB,CAAC;IACvB,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export declare function isSecurityError(error: unknown): boolean;
|
|
7
|
+
export declare function trackWorker(worker: Worker): Worker;
|
|
8
|
+
export declare function untrackWorker(worker: Worker): void;
|
|
9
|
+
export declare function terminateTrackedWorkers(): number;
|
|
10
|
+
export declare function trackedWorkerCount(): number;
|
|
11
|
+
//# sourceMappingURL=workers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workers.d.ts","sourceRoot":"","sources":["../../src/runtime/workers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAKvD;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAElD;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAWhD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C"}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
const live = new Set();
|
|
7
|
+
export function isSecurityError(error) {
|
|
8
|
+
if (error instanceof DOMException) {
|
|
9
|
+
return error.name === "SecurityError";
|
|
10
|
+
}
|
|
11
|
+
return error instanceof Error && error.name === "SecurityError";
|
|
12
|
+
}
|
|
13
|
+
export function trackWorker(worker) {
|
|
14
|
+
live.add(worker);
|
|
15
|
+
return worker;
|
|
16
|
+
}
|
|
17
|
+
export function untrackWorker(worker) {
|
|
18
|
+
live.delete(worker);
|
|
19
|
+
}
|
|
20
|
+
export function terminateTrackedWorkers() {
|
|
21
|
+
const count = live.size;
|
|
22
|
+
for (const worker of live) {
|
|
23
|
+
try {
|
|
24
|
+
worker.terminate();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// bereits tot
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
live.clear();
|
|
31
|
+
return count;
|
|
32
|
+
}
|
|
33
|
+
export function trackedWorkerCount() {
|
|
34
|
+
return live.size;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=workers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workers.js","sourceRoot":"","sources":["../../src/runtime/workers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;AAE/B,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,KAAK,YAAY,YAAY,EAAE,CAAC;QAClC,OAAO,KAAK,CAAC,IAAI,KAAK,eAAe,CAAC;IACxC,CAAC;IACD,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,KAAK,MAAM,MAAM,IAAI,IAAI,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,cAAc;QAChB,CAAC;IACH,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;IACb,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,IAAI,CAAC;AACnB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudware-de/gatekeeper-widget",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Cloudware Gatekeeper checkbox widget (Web Component). T-211 Artikel 8.",
|
|
6
|
+
"license": "GPL-2.0-or-later",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@cloudware-de/gatekeeper-protocol": "0.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^24.5.2",
|
|
26
|
+
"playwright": "1.63.0",
|
|
27
|
+
"tsx": "^4.20.5",
|
|
28
|
+
"typescript": "~5.9.2"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"lint": "node scripts/lint.mjs",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"test": "tsc -p tsconfig.json && tsx --test tests/states.test.ts tests/session.test.ts tests/t203-copy-hold.test.ts tests/t204-checkbox.test.ts tests/t205-load-notice.test.ts tests/t206-client.test.ts tests/t207-pow.test.ts tests/t208-envelope.test.ts tests/t209-token-form.test.ts tests/t210-start-timeout.test.ts tests/t211-artikel8.test.ts tests/t212-attributes.test.ts tests/t212-edge-cases.test.ts tests/t213-challenge-expiry.test.ts tests/t214-honeypot.test.ts tests/t215-validity-oom.test.ts tests/t216-templates.test.ts"
|
|
35
|
+
}
|
|
6
36
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST challenge und redeem gegen die gesetzte Instanz-URL.
|
|
3
|
+
* Spec: specs/001-protocol-core/contracts/protocol-v1.md
|
|
4
|
+
* Spec: specs/002-widget/tasks.md T-206
|
|
5
|
+
* Redeem-Body (Option A): challenge + solution + hostname.
|
|
6
|
+
*/
|
|
7
|
+
import { fetchTransport, type Transport, type TransportResponse } from "./transport.js";
|
|
8
|
+
import { instanceRouteUrl } from "./urls.js";
|
|
9
|
+
|
|
10
|
+
export interface ChallengeIssueRequest {
|
|
11
|
+
readonly hostname: string;
|
|
12
|
+
readonly action?: string;
|
|
13
|
+
readonly cdata?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RedeemSuccess {
|
|
17
|
+
readonly token: string;
|
|
18
|
+
readonly expires_in: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InstanceClientError extends Error {
|
|
22
|
+
readonly status: number;
|
|
23
|
+
readonly errorCodes: readonly string[];
|
|
24
|
+
readonly retryAfter: number | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
message: string,
|
|
28
|
+
status: number,
|
|
29
|
+
errorCodes: readonly string[],
|
|
30
|
+
retryAfter?: number,
|
|
31
|
+
) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "InstanceClientError";
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.errorCodes = errorCodes;
|
|
36
|
+
this.retryAfter = retryAfter;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InstanceClient {
|
|
41
|
+
readonly challengeUrl: string;
|
|
42
|
+
readonly redeemUrl: string;
|
|
43
|
+
requestChallenge(body: ChallengeIssueRequest): Promise<unknown>;
|
|
44
|
+
redeem(body: unknown): Promise<RedeemSuccess>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InstanceClientOptions {
|
|
48
|
+
readonly baseUrl: string;
|
|
49
|
+
readonly siteKey: string;
|
|
50
|
+
readonly transport?: Transport;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readErrorCodes(body: unknown): string[] {
|
|
54
|
+
if (body === null || typeof body !== "object" || !("error-codes" in body)) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const codes = (body as { "error-codes": unknown })["error-codes"];
|
|
58
|
+
if (!Array.isArray(codes)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return codes.filter((item): item is string => typeof item === "string");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readRetryAfter(body: unknown): number | undefined {
|
|
65
|
+
if (body === null || typeof body !== "object" || !("retry-after" in body)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const value = (body as { "retry-after": unknown })["retry-after"];
|
|
69
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requireHostname(body: ChallengeIssueRequest): void {
|
|
73
|
+
if (typeof body.hostname !== "string" || body.hostname.trim() === "") {
|
|
74
|
+
throw new InstanceClientError("hostname is required", 0, ["malformed_encoding"]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function requireRedeemSuccess(body: unknown): RedeemSuccess {
|
|
79
|
+
if (body === null || typeof body !== "object") {
|
|
80
|
+
throw new InstanceClientError("redeem response is not an object", 200, ["malformed_encoding"]);
|
|
81
|
+
}
|
|
82
|
+
const record = body as { token?: unknown; expires_in?: unknown };
|
|
83
|
+
if (typeof record.token !== "string" || record.token.length === 0) {
|
|
84
|
+
throw new InstanceClientError("redeem response missing token", 200, ["malformed_encoding"]);
|
|
85
|
+
}
|
|
86
|
+
if (typeof record.expires_in !== "number" || !Number.isFinite(record.expires_in)) {
|
|
87
|
+
throw new InstanceClientError("redeem response missing expires_in", 200, ["malformed_encoding"]);
|
|
88
|
+
}
|
|
89
|
+
return { token: record.token, expires_in: record.expires_in };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createInstanceClient(options: InstanceClientOptions): InstanceClient {
|
|
93
|
+
const challengeUrl = instanceRouteUrl(options.baseUrl, options.siteKey, "challenge");
|
|
94
|
+
const redeemUrl = instanceRouteUrl(options.baseUrl, options.siteKey, "redeem");
|
|
95
|
+
const transport = options.transport ?? fetchTransport;
|
|
96
|
+
|
|
97
|
+
async function post(url: string, body: unknown): Promise<TransportResponse> {
|
|
98
|
+
return transport({
|
|
99
|
+
url,
|
|
100
|
+
method: "POST",
|
|
101
|
+
body,
|
|
102
|
+
headers: {},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
challengeUrl,
|
|
108
|
+
redeemUrl,
|
|
109
|
+
async requestChallenge(body: ChallengeIssueRequest): Promise<unknown> {
|
|
110
|
+
requireHostname(body);
|
|
111
|
+
const payload: ChallengeIssueRequest = {
|
|
112
|
+
hostname: body.hostname,
|
|
113
|
+
...(body.action !== undefined ? { action: body.action } : {}),
|
|
114
|
+
...(body.cdata !== undefined ? { cdata: body.cdata } : {}),
|
|
115
|
+
};
|
|
116
|
+
const response = await post(challengeUrl, payload);
|
|
117
|
+
if (response.status === 200) {
|
|
118
|
+
return response.body;
|
|
119
|
+
}
|
|
120
|
+
throw new InstanceClientError(
|
|
121
|
+
"challenge request failed",
|
|
122
|
+
response.status,
|
|
123
|
+
readErrorCodes(response.body),
|
|
124
|
+
readRetryAfter(response.body),
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
async redeem(body: unknown): Promise<RedeemSuccess> {
|
|
128
|
+
const response = await post(redeemUrl, body);
|
|
129
|
+
if (response.status === 200) {
|
|
130
|
+
return requireRedeemSuccess(response.body);
|
|
131
|
+
}
|
|
132
|
+
throw new InstanceClientError(
|
|
133
|
+
"redeem request failed",
|
|
134
|
+
response.status,
|
|
135
|
+
readErrorCodes(response.body),
|
|
136
|
+
readRetryAfter(response.body),
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injizierbarer Transport. Default: fetch. Verify-Route nicht genutzt.
|
|
3
|
+
* Spec: specs/002-widget/tasks.md T-206
|
|
4
|
+
* Spec: specs/002-widget/spec.md §Adressierung
|
|
5
|
+
* Kein @cloudware-de/gatekeeper-protocol-Import — Fixture lädt dist/ ohne Import-Map.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function encodeBase64Url(bytes: Uint8Array): string {
|
|
9
|
+
let bin = "";
|
|
10
|
+
for (const byte of bytes) {
|
|
11
|
+
bin += String.fromCharCode(byte);
|
|
12
|
+
}
|
|
13
|
+
return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function jsonBody(body: unknown): string {
|
|
17
|
+
return JSON.stringify(body, (_key, value) => {
|
|
18
|
+
if (value instanceof Uint8Array) {
|
|
19
|
+
return encodeBase64Url(value);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export interface TransportRequest {
|
|
25
|
+
readonly url: string;
|
|
26
|
+
readonly method: "POST";
|
|
27
|
+
readonly body: unknown;
|
|
28
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TransportResponse {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
readonly body: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Transport = (request: TransportRequest) => Promise<TransportResponse>;
|
|
37
|
+
|
|
38
|
+
export async function fetchTransport(request: TransportRequest): Promise<TransportResponse> {
|
|
39
|
+
const response = await fetch(request.url, {
|
|
40
|
+
method: request.method,
|
|
41
|
+
headers: {
|
|
42
|
+
accept: "application/json",
|
|
43
|
+
"content-type": "application/json",
|
|
44
|
+
...request.headers,
|
|
45
|
+
},
|
|
46
|
+
body: jsonBody(request.body),
|
|
47
|
+
});
|
|
48
|
+
let body: unknown = null;
|
|
49
|
+
try {
|
|
50
|
+
body = await response.json();
|
|
51
|
+
} catch {
|
|
52
|
+
body = null;
|
|
53
|
+
}
|
|
54
|
+
return { status: response.status, body };
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modul-1-Adressierung: {base}/{siteKey}/challenge|redeem.
|
|
3
|
+
* Spec: specs/001-protocol-core/spec.md §Adressierung
|
|
4
|
+
* Spec: specs/002-widget/spec.md §Adressierung
|
|
5
|
+
* Spec: specs/002-widget/tasks.md T-206
|
|
6
|
+
*/
|
|
7
|
+
export type InstanceRoute = "challenge" | "redeem";
|
|
8
|
+
|
|
9
|
+
export class InstanceUrlError extends Error {
|
|
10
|
+
constructor(message: string) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "InstanceUrlError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function instanceRouteUrl(
|
|
17
|
+
baseUrl: string,
|
|
18
|
+
siteKey: string,
|
|
19
|
+
route: InstanceRoute,
|
|
20
|
+
): string {
|
|
21
|
+
if (siteKey.trim() === "") {
|
|
22
|
+
throw new InstanceUrlError("site-key is required");
|
|
23
|
+
}
|
|
24
|
+
let url: URL;
|
|
25
|
+
try {
|
|
26
|
+
url = new URL(baseUrl);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new InstanceUrlError("base-url is not a valid URL");
|
|
29
|
+
}
|
|
30
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
31
|
+
throw new InstanceUrlError("base-url must be http: or https:");
|
|
32
|
+
}
|
|
33
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
34
|
+
url.pathname = `${basePath}/${encodeURIComponent(siteKey)}/${route}`;
|
|
35
|
+
url.search = "";
|
|
36
|
+
url.hash = "";
|
|
37
|
+
return url.href;
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sichtbarer Rechenlast-Hinweis (Artikel 5).
|
|
3
|
+
* Spec: specs/002-widget/tasks.md T-205
|
|
4
|
+
* Spec: specs/002-widget/spec.md §Pflichtumfang
|
|
5
|
+
* Gleicher Zweck wie README.md §Rechenlast.
|
|
6
|
+
*/
|
|
7
|
+
export const LOAD_NOTICE =
|
|
8
|
+
"Ihr Browser löst eine speicher- bzw. iterationsgebundene Aufgabe. Das ist Absicht (Kosten pro Angriffsversuch), kein Mining.";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CE-Session: Attribute → Client, WASM-URL, Envelope.
|
|
3
|
+
* Spec: specs/002-widget/spec.md §Adressierung §WASM §Pflichtumfang
|
|
4
|
+
* T-212 Drift D1 D2 D3. Kein @cloudware-de/gatekeeper-protocol-Import.
|
|
5
|
+
*/
|
|
6
|
+
import { createInstanceClient, type InstanceClient } from "../client/instance.js";
|
|
7
|
+
import type { Transport } from "../client/transport.js";
|
|
8
|
+
import {
|
|
9
|
+
runDeliveredProgram,
|
|
10
|
+
type DeliveredProgram,
|
|
11
|
+
type IsolatedRunner,
|
|
12
|
+
} from "../envelope/run.js";
|
|
13
|
+
import { resolveWasmUrl } from "../pow/wasm-url.js";
|
|
14
|
+
|
|
15
|
+
export interface AttributeHost {
|
|
16
|
+
getAttribute(name: string): string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readSiteKey(host: AttributeHost): string {
|
|
20
|
+
return host.getAttribute("site-key")?.trim() ?? "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readBaseUrl(host: AttributeHost): string {
|
|
24
|
+
return host.getAttribute("base-url")?.trim() ?? "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readWasmUrl(host: AttributeHost, moduleUrl: string = import.meta.url): string {
|
|
28
|
+
return resolveWasmUrl(host.getAttribute("wasm-url") ?? "", moduleUrl);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createBoundClient(
|
|
32
|
+
host: AttributeHost,
|
|
33
|
+
transport?: Transport,
|
|
34
|
+
): InstanceClient | null {
|
|
35
|
+
const siteKey = readSiteKey(host);
|
|
36
|
+
const baseUrl = readBaseUrl(host);
|
|
37
|
+
if (siteKey.length < 1 || baseUrl.length < 1) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return createInstanceClient({
|
|
41
|
+
baseUrl,
|
|
42
|
+
siteKey,
|
|
43
|
+
...(transport !== undefined ? { transport } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function programFromChallenge(challenge: unknown): DeliveredProgram | null {
|
|
48
|
+
if (challenge === null || typeof challenge !== "object") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const record = challenge as Record<string, unknown>;
|
|
52
|
+
const nested = record.instrumentationProgram ?? record.program;
|
|
53
|
+
if (nested !== null && typeof nested === "object") {
|
|
54
|
+
const program = nested as Record<string, unknown>;
|
|
55
|
+
if (typeof program.source === "string" && program.source.length > 0) {
|
|
56
|
+
return {
|
|
57
|
+
programId: String(program.programId ?? record.instrumentationProgramId ?? ""),
|
|
58
|
+
programVersion: String(program.programVersion ?? "v1"),
|
|
59
|
+
source: program.source,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (typeof record.instrumentationSource === "string" && record.instrumentationSource.length > 0) {
|
|
64
|
+
return {
|
|
65
|
+
programId: String(record.instrumentationProgramId ?? ""),
|
|
66
|
+
programVersion: String(record.instrumentationProgramVersion ?? "v1"),
|
|
67
|
+
source: record.instrumentationSource,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function attestChallenge(
|
|
74
|
+
challenge: unknown,
|
|
75
|
+
runner?: IsolatedRunner,
|
|
76
|
+
): Promise<Uint8Array | null> {
|
|
77
|
+
const program = programFromChallenge(challenge);
|
|
78
|
+
if (program === null) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return runDeliveredProgram(program, runner !== undefined ? { runner } : {});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Option A: Redeem sendet Challenge + Solution + hostname (stateless MAC).
|
|
86
|
+
* Spec: specs/001-protocol-core/contracts/protocol-v1.md
|
|
87
|
+
* Spec: packages/server/src/handler.ts redeem
|
|
88
|
+
*/
|
|
89
|
+
export interface RedeemIssueContext {
|
|
90
|
+
readonly challenge: unknown;
|
|
91
|
+
readonly hostname: string;
|
|
92
|
+
readonly action?: string;
|
|
93
|
+
readonly cdata?: string;
|
|
94
|
+
/** Widget KDF wall time. Not PII. Server logs it only when GATEKEEP_POW_LOG=1. */
|
|
95
|
+
readonly solveMs?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function mergeRedeemBody(
|
|
99
|
+
solution: unknown,
|
|
100
|
+
attestation: Uint8Array | null,
|
|
101
|
+
context: RedeemIssueContext,
|
|
102
|
+
): unknown {
|
|
103
|
+
if (typeof context.hostname !== "string" || context.hostname.trim() === "") {
|
|
104
|
+
throw new Error("hostname is required");
|
|
105
|
+
}
|
|
106
|
+
const extra: Record<string, unknown> = {
|
|
107
|
+
challenge: context.challenge,
|
|
108
|
+
hostname: context.hostname,
|
|
109
|
+
};
|
|
110
|
+
if (typeof context.action === "string" && context.action.length > 0) {
|
|
111
|
+
extra.action = context.action;
|
|
112
|
+
}
|
|
113
|
+
if (typeof context.cdata === "string" && context.cdata.length > 0) {
|
|
114
|
+
extra.cdata = context.cdata;
|
|
115
|
+
}
|
|
116
|
+
if (attestation !== null) {
|
|
117
|
+
extra.instrumentationEnvelope = { attestation };
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
typeof context.solveMs === "number" &&
|
|
121
|
+
Number.isInteger(context.solveMs) &&
|
|
122
|
+
context.solveMs >= 0 &&
|
|
123
|
+
context.solveMs <= 120_000
|
|
124
|
+
) {
|
|
125
|
+
extra.solveMs = context.solveMs;
|
|
126
|
+
}
|
|
127
|
+
if (solution !== null && typeof solution === "object" && !Array.isArray(solution)) {
|
|
128
|
+
return { ...solution, ...extra };
|
|
129
|
+
}
|
|
130
|
+
return { solution, ...extra };
|
|
131
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startzeitpunkt und Hard-Timeout (ADR-008, 10 s).
|
|
3
|
+
* Spec: specs/002-widget/tasks.md T-210
|
|
4
|
+
* Spec: specs/002-widget/spec.md Pflichtumfang Start / Timeout
|
|
5
|
+
* Nicht aus @cloudware-de/gatekeeper-protocol importieren (T-204-Fixture).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const HARD_TIMEOUT_MS = 10_000;
|
|
9
|
+
/** Artikel 6; nicht aus @cloudware-de/gatekeeper-protocol importieren (T-204-Fixture). */
|
|
10
|
+
export const TOKEN_TTL_MS = 300_000;
|
|
11
|
+
|
|
12
|
+
export type StartMode = "interaction" | "auto" | "click";
|
|
13
|
+
|
|
14
|
+
export function parseStartMode(value: string | null): StartMode {
|
|
15
|
+
if (value === "auto") {
|
|
16
|
+
return "auto";
|
|
17
|
+
}
|
|
18
|
+
if (value === "click") {
|
|
19
|
+
return "click";
|
|
20
|
+
}
|
|
21
|
+
return "interaction";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FALSEY_FLAG = /^(0|false|off|no)$/i;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* T-212 Submit-Hold: Attribut → window.GATEKEEP_SUBMIT_HOLD → true.
|
|
28
|
+
* Aus = kein requestSubmit nach Erfolg; Validity blockiert leeren Submit weiter.
|
|
29
|
+
*/
|
|
30
|
+
export function parseBoolFlag(value: string | boolean | null | undefined): boolean | undefined {
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "boolean") {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (trimmed.length === 0) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return !FALSEY_FLAG.test(trimmed);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseSubmitHold(
|
|
45
|
+
attribute: string | null,
|
|
46
|
+
env: string | boolean | undefined,
|
|
47
|
+
): boolean {
|
|
48
|
+
const fromAttr = parseBoolFlag(attribute);
|
|
49
|
+
if (fromAttr !== undefined) {
|
|
50
|
+
return fromAttr;
|
|
51
|
+
}
|
|
52
|
+
const fromEnv = parseBoolFlag(env);
|
|
53
|
+
if (fromEnv !== undefined) {
|
|
54
|
+
return fromEnv;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readSubmitHold(host: { getAttribute(name: string): string | null }): boolean {
|
|
60
|
+
const env = typeof window !== "undefined" ? window.GATEKEEP_SUBMIT_HOLD : undefined;
|
|
61
|
+
return parseSubmitHold(host.getAttribute("submit-hold"), env);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function expiresAtOf(value: unknown): number | null {
|
|
65
|
+
if (value === null || typeof value !== "object") {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if ("expiresAt" in value) {
|
|
69
|
+
const raw = (value as { expiresAt: unknown }).expiresAt;
|
|
70
|
+
if (typeof raw === "number" && Number.isInteger(raw) && raw >= 0) {
|
|
71
|
+
return raw;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if ("challenge" in value) {
|
|
75
|
+
return expiresAtOf((value as { challenge: unknown }).challenge);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isChallengeExpired(value: unknown, nowMs: number): boolean {
|
|
81
|
+
const expiresAt = expiresAtOf(value);
|
|
82
|
+
return expiresAt !== null && nowMs >= expiresAt;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function issuedAtOf(challenge: unknown, fallbackMs: number): number {
|
|
86
|
+
if (challenge !== null && typeof challenge === "object" && "issuedAt" in challenge) {
|
|
87
|
+
const value = (challenge as { issuedAt: unknown }).issuedAt;
|
|
88
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return fallbackMs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isAbortError(err: unknown): boolean {
|
|
96
|
+
return err instanceof DOMException
|
|
97
|
+
? err.name === "AbortError"
|
|
98
|
+
: err instanceof Error && err.name === "AbortError";
|
|
99
|
+
}
|