@prosopo/provider 5.12.2 → 5.12.3

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.
Files changed (47) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +8 -5
  2. package/.turbo/turbo-build$colon$tsc.log +27 -27
  3. package/.turbo/turbo-build.log +9 -6
  4. package/CHANGELOG.md +15 -0
  5. package/dist/api/captcha/assetSeal.d.ts +15 -0
  6. package/dist/api/captcha/assetSeal.d.ts.map +1 -0
  7. package/dist/api/captcha/assetSeal.js +22 -0
  8. package/dist/api/captcha/assetSeal.js.map +1 -0
  9. package/dist/api/captcha/assetSealFormat.d.ts +23 -0
  10. package/dist/api/captcha/assetSealFormat.d.ts.map +1 -0
  11. package/dist/api/captcha/assetSealFormat.js +87 -0
  12. package/dist/api/captcha/assetSealFormat.js.map +1 -0
  13. package/dist/api/captcha/getImageCaptchaChallenge.d.ts.map +1 -1
  14. package/dist/api/captcha/getImageCaptchaChallenge.js +2 -1
  15. package/dist/api/captcha/getImageCaptchaChallenge.js.map +1 -1
  16. package/dist/api/captcha/sealedAssetsResolver.d.ts +22 -0
  17. package/dist/api/captcha/sealedAssetsResolver.d.ts.map +1 -0
  18. package/dist/api/captcha/sealedAssetsResolver.js +69 -0
  19. package/dist/api/captcha/sealedAssetsResolver.js.map +1 -0
  20. package/dist/api/captcha/signedAssetsResolver.d.ts +2 -1
  21. package/dist/api/captcha/signedAssetsResolver.d.ts.map +1 -1
  22. package/dist/api/captcha/signedAssetsResolver.js +6 -5
  23. package/dist/api/captcha/signedAssetsResolver.js.map +1 -1
  24. package/dist/cjs/api/captcha/assetSeal.cjs +22 -0
  25. package/dist/cjs/api/captcha/assetSealFormat.cjs +95 -0
  26. package/dist/cjs/api/captcha/getImageCaptchaChallenge.cjs +2 -1
  27. package/dist/cjs/api/captcha/sealedAssetsResolver.cjs +70 -0
  28. package/dist/cjs/api/captcha/signedAssetsResolver.cjs +6 -4
  29. package/dist/edge/assetSealHandler.d.ts +9 -0
  30. package/dist/edge/assetSealHandler.d.ts.map +1 -0
  31. package/dist/edge/assetSealHandler.js +74 -0
  32. package/dist/edge/assetSealHandler.js.map +1 -0
  33. package/dist/tests/unit/api/captcha/assetSeal.unit.test.d.ts +2 -0
  34. package/dist/tests/unit/api/captcha/assetSeal.unit.test.d.ts.map +1 -0
  35. package/dist/tests/unit/api/captcha/assetSeal.unit.test.js +132 -0
  36. package/dist/tests/unit/api/captcha/assetSeal.unit.test.js.map +1 -0
  37. package/dist/tests/unit/api/captcha/sealedAssetsResolver.unit.test.d.ts +2 -0
  38. package/dist/tests/unit/api/captcha/sealedAssetsResolver.unit.test.d.ts.map +1 -0
  39. package/dist/tests/unit/api/captcha/sealedAssetsResolver.unit.test.js +151 -0
  40. package/dist/tests/unit/api/captcha/sealedAssetsResolver.unit.test.js.map +1 -0
  41. package/dist/tests/unit/edge/assetSealHandler.unit.test.d.ts +2 -0
  42. package/dist/tests/unit/edge/assetSealHandler.unit.test.d.ts.map +1 -0
  43. package/dist/tests/unit/edge/assetSealHandler.unit.test.js +145 -0
  44. package/dist/tests/unit/edge/assetSealHandler.unit.test.js.map +1 -0
  45. package/edge/entry.ts +62 -0
  46. package/package.json +4 -2
  47. package/scripts/buildEdgeScript.mjs +31 -0
@@ -1,5 +1,6 @@
1
+ import { DEFAULT_ASSET_TOKEN_TTL_SECONDS } from "@prosopo/datasets";
1
2
  import type { Asset, AssetsResolver } from "@prosopo/types";
2
- export declare const DEFAULT_ASSET_TOKEN_TTL_SECONDS = 300;
3
+ export { DEFAULT_ASSET_TOKEN_TTL_SECONDS };
3
4
  export interface SignedAssetsResolverOptions {
4
5
  securityKey: string;
5
6
  ttlSeconds?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"signedAssetsResolver.d.ts","sourceRoot":"","sources":["../../../src/api/captcha/signedAssetsResolver.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5D,eAAO,MAAM,+BAA+B,MAAM,CAAC;AAEnD,MAAM,WAAW,2BAA2B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,qBAAa,oBAAqB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,EAAE,2BAA2B;IAOhD,OAAO,CAAC,IAAI;IAUZ,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK;IA0BrC,OAAO,CAAC,KAAK;CAUb;AAQD,wBAAgB,uBAAuB,CACtC,QAAQ,CAAC,EAAE,MAAM,GACf,cAAc,GAAG,SAAS,CAqB5B"}
1
+ {"version":3,"file":"signedAssetsResolver.d.ts","sourceRoot":"","sources":["../../../src/api/captcha/signedAssetsResolver.ts"],"names":[],"mappings":"AAmCA,OAAO,EACN,+BAA+B,EAG/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5D,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAE3C,MAAM,WAAW,2BAA2B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,qBAAa,oBAAqB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,EAAE,2BAA2B;IAOhD,OAAO,CAAC,IAAI;IAYZ,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK;IA0BrC,OAAO,CAAC,KAAK;CAUb;AAQD,wBAAgB,uBAAuB,CACtC,QAAQ,CAAC,EAAE,MAAM,GACf,cAAc,GAAG,SAAS,CAqB5B"}
@@ -1,14 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { DEFAULT_ASSET_TOKEN_TTL_SECONDS, assetTokenBase, encodeAssetToken } from "@prosopo/datasets";
3
+ //#region src/api/captcha/signedAssetsResolver.ts
2
4
  var SignedAssetsResolver = class {
3
5
  constructor(options) {
4
6
  this.securityKey = options.securityKey;
5
- this.ttlSeconds = options.ttlSeconds ?? 300;
7
+ this.ttlSeconds = options.ttlSeconds ?? DEFAULT_ASSET_TOKEN_TTL_SECONDS;
6
8
  this.clientIp = options.clientIp;
7
9
  this.now = options.now ?? (() => Date.now());
8
10
  }
9
11
  sign(path, expires) {
10
- const base = `${this.securityKey}${path}${expires}${this.clientIp ?? ""}`;
11
- return createHash("sha256").update(base).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
12
+ return encodeAssetToken(new Uint8Array(createHash("sha256").update(assetTokenBase(this.securityKey, path, expires, this.clientIp)).digest()));
12
13
  }
13
14
  resolveAsset(assetURI) {
14
15
  const url = this.toUrl(assetURI);
@@ -45,9 +46,9 @@ function getSignedAssetsResolver(clientIp) {
45
46
  if (!securityKey) return;
46
47
  return new SignedAssetsResolver({
47
48
  securityKey,
48
- ttlSeconds: Number.parseInt(process.env.PROSOPO_ASSET_TOKEN_TTL_SECONDS ?? "", 10) || 300,
49
+ ttlSeconds: Number.parseInt(process.env.PROSOPO_ASSET_TOKEN_TTL_SECONDS ?? "", 10) || DEFAULT_ASSET_TOKEN_TTL_SECONDS,
49
50
  ...process.env.PROSOPO_ASSET_TOKEN_BIND_IP === "true" && clientIp ? { clientIp } : {}
50
51
  });
51
52
  }
52
53
  //#endregion
53
- export { SignedAssetsResolver, getSignedAssetsResolver };
54
+ export { DEFAULT_ASSET_TOKEN_TTL_SECONDS, SignedAssetsResolver, getSignedAssetsResolver };
@@ -1 +1 @@
1
- {"version":3,"file":"signedAssetsResolver.js","sourceRoot":"","sources":["../../../src/api/captcha/signedAssetsResolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signedAssetsResolver.js","sourceRoot":"","sources":["../../../src/api/captcha/signedAssetsResolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ const require_assetSealFormat = require("./assetSealFormat.cjs");
2
+ let node_crypto = require("node:crypto");
3
+ //#region src/api/captcha/assetSeal.ts
4
+ var CIPHER = "aes-256-gcm";
5
+ var GCM_OPTIONS = { authTagLength: 16 };
6
+ var sealAssetPath = (params) => {
7
+ if (!require_assetSealFormat.isSafeAssetPath(params.path)) throw new Error(`Refusing to seal an unusable asset path: ${params.path}`);
8
+ const iv = (0, node_crypto.randomBytes)(12);
9
+ const cipher = (0, node_crypto.createCipheriv)(CIPHER, params.key, iv, GCM_OPTIONS);
10
+ cipher.setAAD(require_assetSealFormat.additionalData(params.keyId));
11
+ const ciphertext = Buffer.concat([cipher.update(require_assetSealFormat.encodePayload({
12
+ expiresAtSeconds: params.expiresAtSeconds,
13
+ path: params.path
14
+ })), cipher.final()]);
15
+ return require_assetSealFormat.encodeEnvelope({
16
+ keyId: params.keyId,
17
+ iv: new Uint8Array(iv),
18
+ sealed: new Uint8Array(Buffer.concat([ciphertext, cipher.getAuthTag()]))
19
+ });
20
+ };
21
+ //#endregion
22
+ exports.sealAssetPath = sealAssetPath;
@@ -0,0 +1,95 @@
1
+ var AAD_BYTES = 2;
2
+ var EXPIRY_BYTES = 4;
3
+ var MAX_KEY_ID = 255;
4
+ var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
5
+ var textEncoder = new TextEncoder();
6
+ var textDecoder = new TextDecoder("utf-8", { fatal: true });
7
+ var toBase64Url = (bytes) => {
8
+ let binary = "";
9
+ for (const byte of bytes) binary += String.fromCharCode(byte);
10
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11
+ };
12
+ var fromBase64Url = (value) => {
13
+ if (!BASE64URL_PATTERN.test(value)) return;
14
+ const normalised = value.replace(/-/g, "+").replace(/_/g, "/");
15
+ const padded = normalised.padEnd(normalised.length + (4 - normalised.length % 4) % 4, "=");
16
+ try {
17
+ const binary = atob(padded);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
20
+ return bytes;
21
+ } catch {
22
+ return;
23
+ }
24
+ };
25
+ var additionalData = (keyId) => new Uint8Array([1, keyId]);
26
+ var encodeEnvelope = (envelope) => {
27
+ const bytes = new Uint8Array(AAD_BYTES + envelope.iv.length + envelope.sealed.length);
28
+ bytes.set(additionalData(envelope.keyId), 0);
29
+ bytes.set(envelope.iv, AAD_BYTES);
30
+ bytes.set(envelope.sealed, AAD_BYTES + envelope.iv.length);
31
+ return toBase64Url(bytes);
32
+ };
33
+ var decodeEnvelope = (blob) => {
34
+ const bytes = fromBase64Url(blob);
35
+ if (!bytes || bytes.length <= 14) return;
36
+ if (bytes[0] !== 1) return;
37
+ const keyId = bytes[1];
38
+ if (void 0 === keyId) return;
39
+ return {
40
+ keyId,
41
+ iv: bytes.slice(AAD_BYTES, 14),
42
+ sealed: bytes.slice(14)
43
+ };
44
+ };
45
+ var encodePayload = (payload) => {
46
+ const path = textEncoder.encode(payload.path);
47
+ const bytes = new Uint8Array(EXPIRY_BYTES + path.length);
48
+ new DataView(bytes.buffer).setUint32(0, payload.expiresAtSeconds, false);
49
+ bytes.set(path, EXPIRY_BYTES);
50
+ return bytes;
51
+ };
52
+ var decodePayload = (bytes) => {
53
+ if (bytes.length <= EXPIRY_BYTES) return;
54
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
55
+ try {
56
+ return {
57
+ expiresAtSeconds: view.getUint32(0, false),
58
+ path: textDecoder.decode(bytes.subarray(EXPIRY_BYTES))
59
+ };
60
+ } catch {
61
+ return;
62
+ }
63
+ };
64
+ var isSafeAssetPath = (path, requiredPrefix) => {
65
+ if (!path.startsWith("/") || path.length > 512) return false;
66
+ if (path.includes("..") || path.includes("//")) return false;
67
+ if (/[\u0000-\u001f\u007f]/.test(path)) return false;
68
+ return void 0 === requiredPrefix || path.startsWith(requiredPrefix);
69
+ };
70
+ var parseSealKeys = (spec) => {
71
+ const keys = /* @__PURE__ */ new Map();
72
+ for (const entry of spec.split(",")) {
73
+ const trimmed = entry.trim();
74
+ if ("" === trimmed) continue;
75
+ const separator = trimmed.indexOf(":");
76
+ if (separator <= 0) throw new Error("Malformed seal key entry: expected \"keyId:base64key\"");
77
+ const keyId = Number(trimmed.slice(0, separator));
78
+ if (!Number.isInteger(keyId) || keyId < 0 || keyId > MAX_KEY_ID) throw new Error(`Seal key id must be an integer in [0, ${MAX_KEY_ID}]`);
79
+ const key = fromBase64Url(trimmed.slice(separator + 1).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
80
+ if (!key || key.length !== 32) throw new Error(`Seal key ${keyId} must be 32 base64-encoded bytes`);
81
+ keys.set(keyId, key);
82
+ }
83
+ if (0 === keys.size) throw new Error("No seal keys configured");
84
+ return keys;
85
+ };
86
+ //#endregion
87
+ exports.additionalData = additionalData;
88
+ exports.decodeEnvelope = decodeEnvelope;
89
+ exports.decodePayload = decodePayload;
90
+ exports.encodeEnvelope = encodeEnvelope;
91
+ exports.encodePayload = encodePayload;
92
+ exports.fromBase64Url = fromBase64Url;
93
+ exports.isSafeAssetPath = isSafeAssetPath;
94
+ exports.parseSealKeys = parseSealKeys;
95
+ exports.toBase64Url = toBase64Url;
@@ -7,6 +7,7 @@ const require_testSiteKey = require("../testSiteKey.cjs");
7
7
  const require_normalizeRequestIp = require("../../utils/normalizeRequestIp.cjs");
8
8
  const require_maintenanceModeResponses = require("./maintenanceModeResponses.cjs");
9
9
  const require_trafficFilterRequestTime = require("./trafficFilterRequestTime.cjs");
10
+ const require_sealedAssetsResolver = require("./sealedAssetsResolver.cjs");
10
11
  const require_signedAssetsResolver = require("./signedAssetsResolver.cjs");
11
12
  let _prosopo_common = require("@prosopo/common");
12
13
  let _prosopo_types = require("@prosopo/types");
@@ -112,7 +113,7 @@ var getImageCaptchaChallenge_default = (env, userAccessRulesStorage) => async (r
112
113
  }));
113
114
  });
114
115
  const taskData = await tasks.imgCaptchaManager.getRandomCaptchasAndRequestHash(datasetId, user, ipAddress, captchaConfig, clientRecord.settings.imageThreshold ?? .8, validSessionId, req.ipInfo);
115
- const assetsResolver = require_signedAssetsResolver.getSignedAssetsResolver(ipAddress.toString()) ?? env.assetsResolver;
116
+ const assetsResolver = require_sealedAssetsResolver.getSealedAssetsResolver() ?? require_signedAssetsResolver.getSignedAssetsResolver(ipAddress.toString()) ?? env.assetsResolver;
116
117
  const captchaResponse = {
117
118
  [_prosopo_types.ApiParams.status]: "ok",
118
119
  [_prosopo_types.ApiParams.captchas]: taskData.captchas.map((captcha) => ({
@@ -0,0 +1,70 @@
1
+ const require_assetSealFormat = require("./assetSealFormat.cjs");
2
+ const require_assetSeal = require("./assetSeal.cjs");
3
+ var SealedAssetsResolver = class {
4
+ constructor(options) {
5
+ this.key = options.key;
6
+ this.keyId = options.keyId;
7
+ this.ttlSeconds = options.ttlSeconds ?? 300;
8
+ this.urlPrefix = options.urlPrefix ?? "s";
9
+ this.now = options.now ?? (() => Date.now());
10
+ }
11
+ resolveAsset(assetURI) {
12
+ const url = this.toUrl(assetURI);
13
+ if (!url) return {
14
+ URI: assetURI,
15
+ getURL: () => assetURI
16
+ };
17
+ if (url.search !== "") throw new Error(`Cannot seal an asset URL that has a query string: ${assetURI}`);
18
+ const blob = require_assetSeal.sealAssetPath({
19
+ path: url.pathname,
20
+ key: this.key,
21
+ keyId: this.keyId,
22
+ expiresAtSeconds: Math.floor(this.now() / 1e3) + this.ttlSeconds
23
+ });
24
+ const sealed = `${url.origin}/${this.urlPrefix}/${blob}${extensionOf(url.pathname)}`;
25
+ return {
26
+ URI: assetURI,
27
+ getURL: () => sealed
28
+ };
29
+ }
30
+ toUrl(assetURI) {
31
+ try {
32
+ const url = new URL(assetURI);
33
+ return url.protocol === "http:" || url.protocol === "https:" ? url : void 0;
34
+ } catch {
35
+ return;
36
+ }
37
+ }
38
+ };
39
+ var extensionOf = (pathname) => {
40
+ const lastDot = pathname.lastIndexOf(".");
41
+ if (lastDot <= pathname.lastIndexOf("/") + 1) return "";
42
+ const extension = pathname.slice(lastDot);
43
+ return /^\.[a-zA-Z0-9]{1,8}$/.test(extension) ? extension : "";
44
+ };
45
+ function getSealedAssetsResolver() {
46
+ const spec = process.env.PROSOPO_ASSET_SEAL_KEYS;
47
+ if (!spec) return;
48
+ const keys = require_assetSealFormat.parseSealKeys(spec);
49
+ const keyId = activeKeyId(keys);
50
+ const key = keys.get(keyId);
51
+ if (!key) throw new Error(`PROSOPO_ASSET_SEAL_ACTIVE_KEY_ID ${keyId} is not in PROSOPO_ASSET_SEAL_KEYS`);
52
+ return new SealedAssetsResolver({
53
+ key,
54
+ keyId,
55
+ ttlSeconds: Number.parseInt(process.env.PROSOPO_ASSET_SEAL_TTL_SECONDS ?? "", 10) || 300,
56
+ urlPrefix: process.env.PROSOPO_ASSET_SEAL_URL_PREFIX || "s"
57
+ });
58
+ }
59
+ var activeKeyId = (keys) => {
60
+ const configured = process.env.PROSOPO_ASSET_SEAL_ACTIVE_KEY_ID;
61
+ if (configured) {
62
+ const parsed = Number.parseInt(configured, 10);
63
+ if (!Number.isInteger(parsed)) throw new Error("PROSOPO_ASSET_SEAL_ACTIVE_KEY_ID must be an integer");
64
+ return parsed;
65
+ }
66
+ return Math.max(...keys.keys());
67
+ };
68
+ //#endregion
69
+ exports.SealedAssetsResolver = SealedAssetsResolver;
70
+ exports.getSealedAssetsResolver = getSealedAssetsResolver;
@@ -1,14 +1,15 @@
1
1
  let node_crypto = require("node:crypto");
2
+ let _prosopo_datasets = require("@prosopo/datasets");
3
+ //#region src/api/captcha/signedAssetsResolver.ts
2
4
  var SignedAssetsResolver = class {
3
5
  constructor(options) {
4
6
  this.securityKey = options.securityKey;
5
- this.ttlSeconds = options.ttlSeconds ?? 300;
7
+ this.ttlSeconds = options.ttlSeconds ?? _prosopo_datasets.DEFAULT_ASSET_TOKEN_TTL_SECONDS;
6
8
  this.clientIp = options.clientIp;
7
9
  this.now = options.now ?? (() => Date.now());
8
10
  }
9
11
  sign(path, expires) {
10
- const base = `${this.securityKey}${path}${expires}${this.clientIp ?? ""}`;
11
- return (0, node_crypto.createHash)("sha256").update(base).digest("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
12
+ return (0, _prosopo_datasets.encodeAssetToken)(new Uint8Array((0, node_crypto.createHash)("sha256").update((0, _prosopo_datasets.assetTokenBase)(this.securityKey, path, expires, this.clientIp)).digest()));
12
13
  }
13
14
  resolveAsset(assetURI) {
14
15
  const url = this.toUrl(assetURI);
@@ -45,10 +46,11 @@ function getSignedAssetsResolver(clientIp) {
45
46
  if (!securityKey) return;
46
47
  return new SignedAssetsResolver({
47
48
  securityKey,
48
- ttlSeconds: Number.parseInt(process.env.PROSOPO_ASSET_TOKEN_TTL_SECONDS ?? "", 10) || 300,
49
+ ttlSeconds: Number.parseInt(process.env.PROSOPO_ASSET_TOKEN_TTL_SECONDS ?? "", 10) || _prosopo_datasets.DEFAULT_ASSET_TOKEN_TTL_SECONDS,
49
50
  ...process.env.PROSOPO_ASSET_TOKEN_BIND_IP === "true" && clientIp ? { clientIp } : {}
50
51
  });
51
52
  }
52
53
  //#endregion
54
+ exports.DEFAULT_ASSET_TOKEN_TTL_SECONDS = _prosopo_datasets.DEFAULT_ASSET_TOKEN_TTL_SECONDS;
53
55
  exports.SignedAssetsResolver = SignedAssetsResolver;
54
56
  exports.getSignedAssetsResolver = getSignedAssetsResolver;
@@ -0,0 +1,9 @@
1
+ export interface AssetSealHandlerOptions {
2
+ readonly keys: ReadonlyMap<number, Uint8Array>;
3
+ readonly urlPrefix: string;
4
+ readonly requiredPathPrefix?: string;
5
+ readonly now?: () => number;
6
+ }
7
+ export type AssetSealHandler = (request: Request) => Promise<Request | Response>;
8
+ export declare const createAssetSealHandler: (options: AssetSealHandlerOptions) => AssetSealHandler;
9
+ //# sourceMappingURL=assetSealHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetSealHandler.d.ts","sourceRoot":"","sources":["../../src/edge/assetSealHandler.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC9B,OAAO,EAAE,OAAO,KACZ,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,CAAC;AAWjC,eAAO,MAAM,sBAAsB,YACzB,uBAAuB,KAC9B,gBA4EF,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { additionalData, decodeEnvelope, decodePayload, isSafeAssetPath, } from "../api/captcha/assetSealFormat.js";
2
+ const TAG_BYTES = 16;
3
+ const notFound = () => new Response("Not found", {
4
+ status: 404,
5
+ headers: {
6
+ "content-type": "text/plain; charset=utf-8",
7
+ "cache-control": "no-store",
8
+ },
9
+ });
10
+ export const createAssetSealHandler = (options) => {
11
+ const imported = new Map();
12
+ const now = options.now ?? (() => Date.now());
13
+ const marker = `/${options.urlPrefix}/`;
14
+ const keyFor = (keyId) => {
15
+ const raw = options.keys.get(keyId);
16
+ if (!raw) {
17
+ return undefined;
18
+ }
19
+ const existing = imported.get(keyId);
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const promise = crypto.subtle.importKey("raw", raw.slice(), { name: "AES-GCM" }, false, ["decrypt"]);
24
+ imported.set(keyId, promise);
25
+ return promise;
26
+ };
27
+ const open = async (blob) => {
28
+ const envelope = decodeEnvelope(blob);
29
+ if (!envelope || envelope.sealed.length <= TAG_BYTES) {
30
+ return undefined;
31
+ }
32
+ const key = keyFor(envelope.keyId);
33
+ if (!key) {
34
+ return undefined;
35
+ }
36
+ let payload;
37
+ try {
38
+ const plaintext = await crypto.subtle.decrypt({
39
+ name: "AES-GCM",
40
+ iv: envelope.iv,
41
+ additionalData: additionalData(envelope.keyId),
42
+ tagLength: TAG_BYTES * 8,
43
+ }, await key, envelope.sealed);
44
+ payload = decodePayload(new Uint8Array(plaintext));
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ if (!payload || payload.expiresAtSeconds <= Math.floor(now() / 1000)) {
50
+ return undefined;
51
+ }
52
+ return isSafeAssetPath(payload.path, options.requiredPathPrefix)
53
+ ? payload.path
54
+ : undefined;
55
+ };
56
+ return async (request) => {
57
+ const url = new URL(request.url);
58
+ if (!url.pathname.startsWith(marker)) {
59
+ return request;
60
+ }
61
+ const path = await open(stripExtension(url.pathname.slice(marker.length)));
62
+ if (!path) {
63
+ return notFound();
64
+ }
65
+ url.pathname = path;
66
+ url.search = "";
67
+ return new Request(url, request);
68
+ };
69
+ };
70
+ const stripExtension = (segment) => {
71
+ const dot = segment.indexOf(".");
72
+ return dot === -1 ? segment : segment.slice(0, dot);
73
+ };
74
+ //# sourceMappingURL=assetSealHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetSealHandler.js","sourceRoot":"","sources":["../../src/edge/assetSealHandler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=assetSeal.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetSeal.unit.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/unit/api/captcha/assetSeal.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,132 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { sealAssetPath, unsealAssetPath, } from "../../../../api/captcha/assetSeal.js";
3
+ import { SEAL_KEY_BYTES, decodeEnvelope, encodeEnvelope, isSafeAssetPath, parseSealKeys, toBase64Url, } from "../../../../api/captcha/assetSealFormat.js";
4
+ const KEY = new Uint8Array(SEAL_KEY_BYTES).fill(7);
5
+ const OTHER_KEY = new Uint8Array(SEAL_KEY_BYTES).fill(9);
6
+ const KEY_ID = 1;
7
+ const NOW_SECONDS = 1_700_000_000;
8
+ const PATH = "/v5_dataset_flat/images/abc.webp";
9
+ const keys = (entries = [[KEY_ID, KEY]]) => new Map(entries);
10
+ const seal = (overrides = {}) => sealAssetPath({
11
+ path: overrides.path ?? PATH,
12
+ key: overrides.key ?? KEY,
13
+ keyId: overrides.keyId ?? KEY_ID,
14
+ expiresAtSeconds: NOW_SECONDS + (overrides.ttl ?? 300),
15
+ });
16
+ const unseal = (blob, overrides = {}) => unsealAssetPath({
17
+ blob,
18
+ keys: overrides.keys ?? keys(),
19
+ nowSeconds: overrides.nowSeconds ?? NOW_SECONDS,
20
+ ...(overrides.requiredPathPrefix !== undefined
21
+ ? { requiredPathPrefix: overrides.requiredPathPrefix }
22
+ : {}),
23
+ });
24
+ describe("sealing a path", () => {
25
+ test("opens again to the path it was given", () => {
26
+ expect(unseal(seal())).toBe(PATH);
27
+ });
28
+ test("says nothing about the path it carries", () => {
29
+ const blob = seal();
30
+ expect(blob).not.toContain("abc");
31
+ expect(blob).not.toContain("images");
32
+ expect(blob).not.toContain("v5_dataset_flat");
33
+ });
34
+ test("is different every time, for the same path", () => {
35
+ const blobs = new Set(Array.from({ length: 50 }, () => seal()));
36
+ expect(blobs.size).toBe(50);
37
+ });
38
+ test("is usable in a URL path segment", () => {
39
+ expect(seal()).toMatch(/^[A-Za-z0-9_-]+$/);
40
+ });
41
+ test("refuses to seal a path that could not be served", () => {
42
+ expect(() => seal({ path: "../../etc/passwd" })).toThrow();
43
+ expect(() => seal({ path: "no-leading-slash" })).toThrow();
44
+ });
45
+ });
46
+ describe("opening a seal", () => {
47
+ test("rejects one whose expiry has passed", () => {
48
+ const blob = seal({ ttl: 60 });
49
+ expect(unseal(blob, { nowSeconds: NOW_SECONDS + 59 })).toBe(PATH);
50
+ expect(unseal(blob, { nowSeconds: NOW_SECONDS + 60 })).toBeUndefined();
51
+ expect(unseal(blob, { nowSeconds: NOW_SECONDS + 61 })).toBeUndefined();
52
+ });
53
+ test("rejects one sealed under a key we do not hold", () => {
54
+ const blob = seal({ key: OTHER_KEY, keyId: 2 });
55
+ expect(unseal(blob)).toBeUndefined();
56
+ });
57
+ test("rejects one whose key id has been swapped", () => {
58
+ const envelope = decodeEnvelope(seal());
59
+ if (!envelope)
60
+ throw new Error("expected an envelope");
61
+ const swapped = encodeEnvelope({ ...envelope, keyId: 2 });
62
+ expect(unseal(swapped, {
63
+ keys: keys([
64
+ [KEY_ID, KEY],
65
+ [2, KEY],
66
+ ]),
67
+ })).toBeUndefined();
68
+ });
69
+ test("rejects a tampered blob", () => {
70
+ const blob = seal();
71
+ const flipped = `${blob.slice(0, -2)}${"A" === blob.slice(-2, -1) ? "B" : "A"}${blob.slice(-1)}`;
72
+ expect(unseal(flipped)).toBeUndefined();
73
+ });
74
+ test("rejects truncation", () => {
75
+ expect(unseal(seal().slice(0, 20))).toBeUndefined();
76
+ });
77
+ test("rejects noise", () => {
78
+ for (const blob of ["", "!!!!", "a", toBase64Url(new Uint8Array(8))]) {
79
+ expect(unseal(blob)).toBeUndefined();
80
+ }
81
+ });
82
+ test("enforces the path prefix when one is required", () => {
83
+ expect(unseal(seal(), { requiredPathPrefix: "/v5_dataset_flat/" })).toBe(PATH);
84
+ expect(unseal(seal({ path: "/elsewhere/a.webp" }), {
85
+ requiredPathPrefix: "/v5_dataset_flat/",
86
+ })).toBeUndefined();
87
+ });
88
+ test("opens a blob sealed under any key still configured", () => {
89
+ const old = seal({ key: KEY, keyId: 1 });
90
+ const fresh = seal({ key: OTHER_KEY, keyId: 2 });
91
+ const both = keys([
92
+ [1, KEY],
93
+ [2, OTHER_KEY],
94
+ ]);
95
+ expect(unseal(old, { keys: both })).toBe(PATH);
96
+ expect(unseal(fresh, { keys: both })).toBe(PATH);
97
+ });
98
+ });
99
+ describe("isSafeAssetPath", () => {
100
+ test("accepts an ordinary dataset path", () => {
101
+ expect(isSafeAssetPath(PATH)).toBe(true);
102
+ });
103
+ test("rejects traversal, protocol-relative paths and control characters", () => {
104
+ expect(isSafeAssetPath("/a/../b.webp")).toBe(false);
105
+ expect(isSafeAssetPath("//evil.example/a.webp")).toBe(false);
106
+ expect(isSafeAssetPath("/a\nHost: evil.example")).toBe(false);
107
+ expect(isSafeAssetPath("relative.webp")).toBe(false);
108
+ });
109
+ test("rejects a path longer than any real item", () => {
110
+ expect(isSafeAssetPath(`/${"a".repeat(512)}`)).toBe(false);
111
+ });
112
+ });
113
+ describe("parseSealKeys", () => {
114
+ const encoded = toBase64Url(KEY);
115
+ test("reads id and key pairs", () => {
116
+ const parsed = parseSealKeys(`1:${encoded},2:${toBase64Url(OTHER_KEY)}`);
117
+ expect(Array.from(parsed.keys()).sort()).toEqual([1, 2]);
118
+ expect(parsed.get(1)).toEqual(KEY);
119
+ });
120
+ test("tolerates whitespace and trailing separators", () => {
121
+ expect(parseSealKeys(` 1:${encoded} , `).size).toBe(1);
122
+ });
123
+ test("refuses a key that is not 32 bytes", () => {
124
+ expect(() => parseSealKeys(`1:${toBase64Url(new Uint8Array(16))}`)).toThrow(/32/);
125
+ });
126
+ test("refuses malformed entries", () => {
127
+ expect(() => parseSealKeys(encoded)).toThrow(/keyId:base64key/);
128
+ expect(() => parseSealKeys(`x:${encoded}`)).toThrow(/integer/);
129
+ expect(() => parseSealKeys("")).toThrow(/No seal keys/);
130
+ });
131
+ });
132
+ //# sourceMappingURL=assetSeal.unit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetSeal.unit.test.js","sourceRoot":"","sources":["../../../../../src/tests/unit/api/captcha/assetSeal.unit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sealedAssetsResolver.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedAssetsResolver.unit.test.d.ts","sourceRoot":"","sources":["../../../../../src/tests/unit/api/captcha/sealedAssetsResolver.unit.test.ts"],"names":[],"mappings":""}