From 512b29c54d0ad45f8c33871cd00188776070a22c Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Fri, 23 May 2025 20:35:37 +0700 Subject: [PATCH] add edge function for detect and verify faces with aws rekognition --- sigap-website/package-lock.json | 1637 ++++++++++++++++- sigap-website/package.json | 7 +- .../supabase/functions/detect-face/index.ts | 417 +++-- .../supabase/functions/shared/aws-utils.ts | 258 +++ .../supabase/functions/verify-face/index.ts | 398 ++-- 5 files changed, 2448 insertions(+), 269 deletions(-) create mode 100644 sigap-website/supabase/functions/shared/aws-utils.ts diff --git a/sigap-website/package-lock.json b/sigap-website/package-lock.json index c96a7d3..25af89a 100644 --- a/sigap-website/package-lock.json +++ b/sigap-website/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@aws-sdk/client-rekognition": "^3.449.0", "@evyweb/ioctopus": "^1.2.0", "@faker-js/faker": "^9.7.0", "@hookform/resolvers": "^4.1.2", @@ -41,6 +42,7 @@ "@turf/turf": "^7.2.0", "@types/mapbox-gl": "^3.4.1", "autoprefixer": "10.4.20", + "aws-sdk": "^2.1692.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cobe": "^0.6.3", @@ -121,6 +123,633 @@ "node": ">=6.0.0" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-rekognition": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-rekognition/-/client-rekognition-3.816.0.tgz", + "integrity": "sha512-7mdkIlbmsgJN8sCANMtsf2L5kWj5rIDmADlVfNto26mDaZQCE8KwNJx5E0AkcTTAYwIl62qml3G01uUDPskCcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-node": "3.816.0", + "@aws-sdk/middleware-host-header": "3.804.0", + "@aws-sdk/middleware-logger": "3.804.0", + "@aws-sdk/middleware-recursion-detection": "3.804.0", + "@aws-sdk/middleware-user-agent": "3.816.0", + "@aws-sdk/region-config-resolver": "3.808.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@aws-sdk/util-user-agent-browser": "3.804.0", + "@aws-sdk/util-user-agent-node": "3.816.0", + "@smithy/config-resolver": "^4.1.2", + "@smithy/core": "^3.3.3", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", + "@smithy/util-endpoints": "^3.0.4", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.3", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.3", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-rekognition/node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "node_modules/@aws-sdk/client-rekognition/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.816.0.tgz", + "integrity": "sha512-2D2bc6wVDgGxwHjqSyxGYXUHa3ni0R21Isiq0OzJk6nBc8Vi6HpYoimok6UaUfPJsG6OEWfQ9vubcR30BCBmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/middleware-host-header": "3.804.0", + "@aws-sdk/middleware-logger": "3.804.0", + "@aws-sdk/middleware-recursion-detection": "3.804.0", + "@aws-sdk/middleware-user-agent": "3.816.0", + "@aws-sdk/region-config-resolver": "3.808.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@aws-sdk/util-user-agent-browser": "3.804.0", + "@aws-sdk/util-user-agent-node": "3.816.0", + "@smithy/config-resolver": "^4.1.2", + "@smithy/core": "^3.3.3", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", + "@smithy/util-endpoints": "^3.0.4", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.816.0.tgz", + "integrity": "sha512-Lx50wjtyarzKpMFV6V+gjbSZDgsA/71iyifbClGUSiNPoIQ4OCV0KVOmAAj7mQRVvGJqUMWKVM+WzK79CjbjWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/core": "^3.3.3", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/signature-v4": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/util-middleware": "^4.0.2", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.816.0.tgz", + "integrity": "sha512-wUJZwRLe+SxPxRV9AENYBLrJZRrNIo+fva7ZzejsC83iz7hdfq6Rv6B/aHEdPwG/nQC4+q7UUvcRPlomyrpsBA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.816.0.tgz", + "integrity": "sha512-gcWGzMQ7yRIF+ljTkR8Vzp7727UY6cmeaPrFQrvcFB8PhOqWpf7g0JsgOf5BSaP8CkkSQcTQHc0C5ZYAzUFwPg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/util-stream": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.816.0.tgz", + "integrity": "sha512-60FiF4PFgrnIQ7vizAMN4imrjFzlXPEvXKk05cs47W7qtA54MudRvTITN9Gji4vD5j6CDp9fOb0HS0+N83YEAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/credential-provider-env": "3.816.0", + "@aws-sdk/credential-provider-http": "3.816.0", + "@aws-sdk/credential-provider-process": "3.816.0", + "@aws-sdk/credential-provider-sso": "3.816.0", + "@aws-sdk/credential-provider-web-identity": "3.816.0", + "@aws-sdk/nested-clients": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/credential-provider-imds": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.816.0.tgz", + "integrity": "sha512-NsjMzCHWXpFXNdJYliY4GM2vtsmP8w9OyjrNcHq4mmdp7lAzM3BvRKavVuhBXrSHQFzLkGWaTGd+fkkTZtYHXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.816.0", + "@aws-sdk/credential-provider-http": "3.816.0", + "@aws-sdk/credential-provider-ini": "3.816.0", + "@aws-sdk/credential-provider-process": "3.816.0", + "@aws-sdk/credential-provider-sso": "3.816.0", + "@aws-sdk/credential-provider-web-identity": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/credential-provider-imds": "^4.0.4", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.816.0.tgz", + "integrity": "sha512-9Tm+AxMoV2Izvl5b9tyMQRbBwaex8JP06HN7ZeCXgC5sAsSN+o8dsThnEhf8jKN+uBpT6CLWKN1TXuUMrAmW1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.816.0.tgz", + "integrity": "sha512-wxNqvPrRpKwkNQ41uGE/y3Y95TsWnJnMpcTIMhGEub0Jq5ifnUEev3gJ60857uaut133mwpGfupE2fk8V68e6A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.816.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/token-providers": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.816.0.tgz", + "integrity": "sha512-guKv76eRTMQkGOHsv9tY8XP9y1TZ0U4w6RD5kHQYNuzoBf5HOpZcib9OGmWJA2n+BC+byCOOftl6mMmYtRfcpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/nested-clients": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.804.0.tgz", + "integrity": "sha512-bum1hLVBrn2lJCi423Z2fMUYtsbkGI2s4N+2RI2WSjvbaVyMSv/WcejIrjkqiiMR+2Y7m5exgoKeg4/TODLDPQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.804.0.tgz", + "integrity": "sha512-w/qLwL3iq0KOPQNat0Kb7sKndl9BtceigINwBU7SpkYWX9L/Lem6f8NPEKrC9Tl4wDBht3Yztub4oRTy/horJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.804.0.tgz", + "integrity": "sha512-zqHOrvLRdsUdN/ehYfZ9Tf8svhbiLLz5VaWUz22YndFv6m9qaAcijkpAOlKexsv3nLBMJdSdJ6GUTAeIy3BZzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.816.0.tgz", + "integrity": "sha512-bHRSlWZ0xDsFR8E2FwDb//0Ff6wMkVx4O+UKsfyNlAbtqCiiHRt5ANNfKPafr95cN2CCxLxiPvFTFVblQM5TsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@smithy/core": "^3.3.3", + "@smithy/protocol-http": "^5.1.0", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.816.0.tgz", + "integrity": "sha512-N1jSEa53QHLvIH37Rrm8cD9MlC6ucTEpDhbO+fN9UHBimRnpt9kqk0iiBCu5FK7GEKWYM5kiawfUtbB3PN65CQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.816.0", + "@aws-sdk/middleware-host-header": "3.804.0", + "@aws-sdk/middleware-logger": "3.804.0", + "@aws-sdk/middleware-recursion-detection": "3.804.0", + "@aws-sdk/middleware-user-agent": "3.816.0", + "@aws-sdk/region-config-resolver": "3.808.0", + "@aws-sdk/types": "3.804.0", + "@aws-sdk/util-endpoints": "3.808.0", + "@aws-sdk/util-user-agent-browser": "3.804.0", + "@aws-sdk/util-user-agent-node": "3.816.0", + "@smithy/config-resolver": "^4.1.2", + "@smithy/core": "^3.3.3", + "@smithy/fetch-http-handler": "^5.0.2", + "@smithy/hash-node": "^4.0.2", + "@smithy/invalid-dependency": "^4.0.2", + "@smithy/middleware-content-length": "^4.0.2", + "@smithy/middleware-endpoint": "^4.1.6", + "@smithy/middleware-retry": "^4.1.7", + "@smithy/middleware-serde": "^4.0.5", + "@smithy/middleware-stack": "^4.0.2", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/node-http-handler": "^4.0.4", + "@smithy/protocol-http": "^5.1.0", + "@smithy/smithy-client": "^4.2.6", + "@smithy/types": "^4.2.0", + "@smithy/url-parser": "^4.0.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.14", + "@smithy/util-defaults-mode-node": "^4.0.14", + "@smithy/util-endpoints": "^3.0.4", + "@smithy/util-middleware": "^4.0.2", + "@smithy/util-retry": "^4.0.3", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.808.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.808.0.tgz", + "integrity": "sha512-9x2QWfphkARZY5OGkl9dJxZlSlYM2l5inFeo2bKntGuwg4A4YUe5h7d5yJ6sZbam9h43eBrkOdumx03DAkQF9A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/types": "^4.2.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.816.0.tgz", + "integrity": "sha512-OFeeq7j3JsI5OrUW4vw/Lckwq8I+HNffi7IXenfqzE83vhgXQF9BfCg+TGHDfxiDt6NUaS6F0QzhfkW6/AORlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.816.0", + "@aws-sdk/nested-clients": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/property-provider": "^4.0.2", + "@smithy/shared-ini-file-loader": "^4.0.2", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.804.0.tgz", + "integrity": "sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.808.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.808.0.tgz", + "integrity": "sha512-N6Lic98uc4ADB7fLWlzx+1uVnq04VgVjngZvwHoujcRg9YDhIg9dUDiTzD5VZv13g1BrPYmvYP1HhsildpGV6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/types": "^4.2.0", + "@smithy/util-endpoints": "^3.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.804.0.tgz", + "integrity": "sha512-KfW6T6nQHHM/vZBBdGn6fMyG/MgX5lq82TDdX4HRQRRuHKLgBWGpKXqqvBwqIaCdXwWHgDrg2VQups6GqOWW2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.804.0", + "@smithy/types": "^4.2.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.816.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.816.0.tgz", + "integrity": "sha512-Q6dxmuj4hL7pudhrneWEQ7yVHIQRBFr0wqKLF1opwOi1cIePuoEbPyJ2jkel6PDEv1YMfvsAKaRshp6eNA8VHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.816.0", + "@aws-sdk/types": "3.804.0", + "@smithy/node-config-provider": "^4.1.1", + "@smithy/types": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -5781,6 +6410,599 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.3.tgz", + "integrity": "sha512-AqXFf6DXnuRBXy4SoK/n1mfgHaKaq36bmkphmD1KO0nHq6xK/g9KHSW4HEsPQUBCGdIEfuJifGHwxFXPIFay9Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.3.tgz", + "integrity": "sha512-N5e7ofiyYDmHxnPnqF8L4KtsbSDwyxFRfDK9bp1d9OyPO4ytRLd0/XxCqi5xVaaqB65v4woW8uey6jND6zxzxQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.2", + "@smithy/types": "^4.3.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.4.0.tgz", + "integrity": "sha512-dDYISQo7k0Ml/rXlFIjkTmTcQze/LxhtIRAEmZ6HJ/EI0inVxVEVnrUXJ7jPx6ZP0GHUhFm40iQcCgS5apXIXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.6", + "@smithy/protocol-http": "^5.1.1", + "@smithy/types": "^4.3.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.3", + "@smithy/util-stream": "^4.2.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.5.tgz", + "integrity": "sha512-saEAGwrIlkb9XxX/m5S5hOtzjoJPEK6Qw2f9pYTbIsMPOFyGSXBBTw95WbOyru8A1vIS2jVCCU1Qhz50QWG3IA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.2", + "@smithy/property-provider": "^4.0.3", + "@smithy/types": "^4.3.0", + "@smithy/url-parser": "^4.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.3.tgz", + "integrity": "sha512-yBZwavI31roqTndNI7ONHqesfH01JmjJK6L3uUpZAhyAmr86LN5QiPzfyZGIxQmed8VEK2NRSQT3/JX5V1njfQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.1", + "@smithy/querystring-builder": "^4.0.3", + "@smithy/types": "^4.3.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.3.tgz", + "integrity": "sha512-W5Uhy6v/aYrgtjh9y0YP332gIQcwccQ+EcfWhllL0B9rPae42JngTTUpb8W6wuxaNFzqps4xq5klHckSSOy5fw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.3.tgz", + "integrity": "sha512-1Bo8Ur1ZGqxvwTqBmv6DZEn0rXtwJGeqiiO2/JFcCtz3nBakOqeXbJBElXJMMzd0ghe8+eB6Dkw98nMYctgizg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.3.tgz", + "integrity": "sha512-NE/Zph4BP5u16bzYq2csq9qD0T6UBLeg4AuNrwNJ7Gv9uLYaGEgelZUOdRndGdMGcUfSGvNlXGb2aA2hPCwJ6g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.1", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.7.tgz", + "integrity": "sha512-KDzM7Iajo6K7eIWNNtukykRT4eWwlHjCEsULZUaSfi/SRSBK8BPRqG5FsVfp58lUxcvre8GT8AIPIqndA0ERKw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.4.0", + "@smithy/middleware-serde": "^4.0.6", + "@smithy/node-config-provider": "^4.1.2", + "@smithy/shared-ini-file-loader": "^4.0.3", + "@smithy/types": "^4.3.0", + "@smithy/url-parser": "^4.0.3", + "@smithy/util-middleware": "^4.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.8.tgz", + "integrity": "sha512-e2OtQgFzzlSG0uCjcJmi02QuFSRTrpT11Eh2EcqqDFy7DYriteHZJkkf+4AsxsrGDugAtPFcWBz1aq06sSX5fQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.2", + "@smithy/protocol-http": "^5.1.1", + "@smithy/service-error-classification": "^4.0.4", + "@smithy/smithy-client": "^4.3.0", + "@smithy/types": "^4.3.0", + "@smithy/util-middleware": "^4.0.3", + "@smithy/util-retry": "^4.0.4", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.6.tgz", + "integrity": "sha512-YECyl7uNII+jCr/9qEmCu8xYL79cU0fqjo0qxpcVIU18dAPHam/iYwcknAu4Jiyw1uN+sAx7/SMf/Kmef/Jjsg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.1", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.3.tgz", + "integrity": "sha512-baeV7t4jQfQtFxBADFmnhmqBmqR38dNU5cvEgHcMK/Kp3D3bEI0CouoX2Sr/rGuntR+Eg0IjXdxnGGTc6SbIkw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.2.tgz", + "integrity": "sha512-SUvNup8iU1v7fmM8XPk+27m36udmGCfSz+VZP5Gb0aJ3Ne0X28K/25gnsrg3X1rWlhcnhzNUUysKW/Ied46ivQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.3", + "@smithy/shared-ini-file-loader": "^4.0.3", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.5.tgz", + "integrity": "sha512-T7QglZC1vS7SPT44/1qSIAQEx5bFKb3LfO6zw/o4Xzt1eC5HNoH1TkS4lMYA9cWFbacUhx4hRl/blLun4EOCkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.3", + "@smithy/protocol-http": "^5.1.1", + "@smithy/querystring-builder": "^4.0.3", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.3.tgz", + "integrity": "sha512-Wcn17QNdawJZcZZPBuMuzyBENVi1AXl4TdE0jvzo4vWX2x5df/oMlmr/9M5XAAC6+yae4kWZlOYIsNsgDrMU9A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.1.tgz", + "integrity": "sha512-Vsay2mzq05DwNi9jK01yCFtfvu9HimmgC7a4HTs7lhX12Sx8aWsH0mfz6q/02yspSp+lOB+Q2HJwi4IV2GKz7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.3.tgz", + "integrity": "sha512-UUzIWMVfPmDZcOutk2/r1vURZqavvQW0OHvgsyNV0cKupChvqg+/NKPRMaMEe+i8tP96IthMFeZOZWpV+E4RAw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.3.tgz", + "integrity": "sha512-K5M4ZJQpFCblOJ5Oyw7diICpFg1qhhR47m2/5Ef1PhGE19RaIZf50tjYFrxa6usqcuXyTiFPGo4d1geZdH4YcQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.4.tgz", + "integrity": "sha512-W5ScbQ1bTzgH91kNEE2CvOzM4gXlDOqdow4m8vMFSIXCel2scbHwjflpVNnC60Y3F1m5i7w2gQg9lSnR+JsJAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.3.tgz", + "integrity": "sha512-vHwlrqhZGIoLwaH8vvIjpHnloShqdJ7SUPNM2EQtEox+yEDFTVQ7E+DLZ+6OhnYEgFUwPByJyz6UZaOu2tny6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.1.tgz", + "integrity": "sha512-zy8Repr5zvT0ja+Tf5wjV/Ba6vRrhdiDcp/ww6cvqYbSEudIkziDe3uppNRlFoCViyJXdPnLcwyZdDLA4CHzSg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.1", + "@smithy/types": "^4.3.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.3", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.3.0.tgz", + "integrity": "sha512-DNsRA38pN6tYHUjebmwD9e4KcgqTLldYQb2gC6K+oxXYdCTxPn6wV9+FvOa6wrU2FQEnGJoi+3GULzOTKck/tg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.4.0", + "@smithy/middleware-endpoint": "^4.1.7", + "@smithy/middleware-stack": "^4.0.3", + "@smithy/protocol-http": "^5.1.1", + "@smithy/types": "^4.3.0", + "@smithy/util-stream": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.0.tgz", + "integrity": "sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.3.tgz", + "integrity": "sha512-n5/DnosDu/tweOqUUNtUbu7eRIR4J/Wz9nL7V5kFYQQVb8VYdj7a4G5NJHCw6o21ul7CvZoJkOpdTnsQDLT0tQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.3", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.15.tgz", + "integrity": "sha512-bJJ/B8owQbHAflatSq92f9OcV8858DJBQF1Y3GRjB8psLyUjbISywszYPFw16beREHO/C3I3taW4VGH+tOuwrQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.3", + "@smithy/smithy-client": "^4.3.0", + "@smithy/types": "^4.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.15.tgz", + "integrity": "sha512-8CUrEW2Ni5q+NmYkj8wsgkfqoP7l4ZquptFbq92yQE66xevc4SxqP2zH6tMtN158kgBqBDsZ+qlrRwXWOjCR8A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.3", + "@smithy/credential-provider-imds": "^4.0.5", + "@smithy/node-config-provider": "^4.1.2", + "@smithy/property-provider": "^4.0.3", + "@smithy/smithy-client": "^4.3.0", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.5.tgz", + "integrity": "sha512-PjDpqLk24/vAl340tmtCA++Q01GRRNH9cwL9qh46NspAX9S+IQVcK+GOzPt0GLJ6KYGyn8uOgo2kvJhiThclJw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.2", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.3.tgz", + "integrity": "sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.4.tgz", + "integrity": "sha512-Aoqr9W2jDYGrI6OxljN8VmLDQIGO4VdMAUKMf9RGqLG8hn6or+K41NEy1Y5dtum9q8F7e0obYAuKl2mt/GnpZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.4", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.1.tgz", + "integrity": "sha512-W3IR0x5DY6iVtjj5p902oNhD+Bz7vs5S+p6tppbPa509rV9BdeXZjGuRSCtVEad9FA0Mba+tNUtUmtnSI1nwUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.3", + "@smithy/node-http-handler": "^4.0.5", + "@smithy/types": "^4.3.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.4.tgz", + "integrity": "sha512-73aeIvHjtSB6fd9I08iFaQIGTICKpLrI3EtlWAkStVENGo1ARMq9qdoD4QwkY0RUp6A409xlgbD9NCCfCF5ieg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.3", + "@smithy/types": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@snaplet/copycat": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@snaplet/copycat/-/copycat-6.0.0.tgz", @@ -9706,6 +10928,78 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -9789,6 +11083,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -9967,6 +11267,24 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -9980,6 +11298,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -11068,6 +12402,23 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", @@ -12067,6 +13418,28 @@ "license": "BSD-3-Clause", "peer": true }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -12168,6 +13541,21 @@ "dev": true, "license": "MIT" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -12711,6 +14099,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -13005,7 +14405,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -13039,6 +14438,22 @@ "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==", "license": "MIT" }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -13065,6 +14480,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -13107,6 +14534,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -13174,6 +14619,24 @@ "@types/estree": "*" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -13187,6 +14650,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -13207,6 +14685,12 @@ "dev": true, "license": "MIT" }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -13306,6 +14790,15 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", @@ -15151,6 +16644,15 @@ "splaytree-ts": "^1.0.2" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -15517,6 +17019,15 @@ "node": ">=6" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17184,6 +18695,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -17191,6 +18719,12 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -17307,6 +18841,23 @@ "node": ">=4.0.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -17944,6 +19495,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -18918,6 +20481,22 @@ "dev": true, "license": "MIT" }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -18970,6 +20549,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19252,6 +20844,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -19388,6 +21001,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/sigap-website/package.json b/sigap-website/package.json index d339cb0..96720df 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -11,6 +11,7 @@ "seed:reset": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" }, "dependencies": { + "@aws-sdk/client-rekognition": "^3.449.0", "@evyweb/ioctopus": "^1.2.0", "@faker-js/faker": "^9.7.0", "@hookform/resolvers": "^4.1.2", @@ -47,6 +48,7 @@ "@turf/turf": "^7.2.0", "@types/mapbox-gl": "^3.4.1", "autoprefixer": "10.4.20", + "aws-sdk": "^2.1692.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cobe": "^0.6.3", @@ -77,8 +79,7 @@ "uuid": "^11.1.0", "vaul": "^1.1.2", "zod": "^3.24.2", - "zustand": "^5.0.3", - "@aws-sdk/client-rekognition": "^3.449.0" + "zustand": "^5.0.3" }, "devDependencies": { "@snaplet/copycat": "^6.0.0", @@ -104,4 +105,4 @@ "overrides": { "react-is": "^19.0.0-rc-69d4b800-20241021" } -} \ No newline at end of file +} diff --git a/sigap-website/supabase/functions/detect-face/index.ts b/sigap-website/supabase/functions/detect-face/index.ts index 4b33063..8e16c34 100644 --- a/sigap-website/supabase/functions/detect-face/index.ts +++ b/sigap-website/supabase/functions/detect-face/index.ts @@ -1,142 +1,295 @@ +// detect-face/index.ts // Follow this setup guide to integrate the Deno language server with your editor: // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. + // Setup type definitions for built-in Supabase Runtime APIs import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -const AWS_REGION = Deno.env.get('AWS_REGION'); -const AWS_ACCESS_KEY = Deno.env.get('AWS_ACCESS_KEY'); -const AWS_SECRET_KEY = Deno.env.get('AWS_SECRET_KEY'); -serve(async (req)=>{ - console.log('AWS_REGION:', AWS_REGION); - console.log('AWS_ACCESS_KEY:', AWS_ACCESS_KEY?.slice(0, 5)); // for security, partial only - console.log('AWS_SECRET_KEY:', AWS_SECRET_KEY?.slice(0, 5)); // for security, partial only - try { - // Check if we have AWS credentials - if (!AWS_REGION || !AWS_ACCESS_KEY || !AWS_SECRET_KEY) { - return new Response(JSON.stringify({ - error: 'AWS credentials are not configured' - }), { - status: 500, - headers: { - 'Content-Type': 'application/json' - } - }); - } - // Parse the multipart form data to get the image - const formData = await req.formData(); - const image = formData.get('image'); - if (!image || !(image instanceof File)) { - return new Response(JSON.stringify({ - error: 'Image file is required' - }), { - status: 400, - headers: { - 'Content-Type': 'application/json' - } - }); - } - // Convert image to base64 - const imageBuffer = await image.arrayBuffer(); - const base64Image = btoa(String.fromCharCode(...new Uint8Array(imageBuffer))); - // Create AWS signature for authorization - const date = new Date(); - const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, ''); - const dateStamp = amzDate.substring(0, 8); - const host = `rekognition.${AWS_REGION}.amazonaws.com`; - const endpoint = `https://${host}/`; - const request = { - "Image": { - "Bytes": base64Image - }, - "Attributes": [ - "ALL" - ] - }; - // AWS Signature V4 calculation - const method = 'POST'; - const service = 'rekognition'; - const contentType = 'application/x-amz-json-1.1'; - const amzTarget = 'RekognitionService.DetectFaces'; - const canonicalUri = '/'; - const canonicalQueryString = ''; - const payloadHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(request))).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join('')); - const canonicalHeaders = `content-type:${contentType}\n` + `host:${host}\n` + `x-amz-date:${amzDate}\n` + `x-amz-target:${amzTarget}\n`; - const signedHeaders = 'content-type;host;x-amz-date;x-amz-target'; - const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`; - const algorithm = 'AWS4-HMAC-SHA256'; - const credentialScope = `${dateStamp}/${AWS_REGION}/${service}/aws4_request`; - const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalRequest)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''))}`; - const getSignatureKey = async (key, dateStamp, regionName, serviceName)=>{ - const kDate = await crypto.subtle.importKey("raw", new TextEncoder().encode(`AWS4${key}`), { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]); - const kRegion = await crypto.subtle.sign("HMAC", kDate, new TextEncoder().encode(regionName)); - const kService = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kRegion, { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]), new TextEncoder().encode(serviceName)); - return crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kService, { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]), new TextEncoder().encode("aws4_request")); - }; - const signingKey = await getSignatureKey(AWS_SECRET_KEY, dateStamp, AWS_REGION, service); - const signature = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", signingKey, { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]), new TextEncoder().encode(stringToSign)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join('')); - const authHeader = `${algorithm} ` + `Credential=${AWS_ACCESS_KEY}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, ` + `Signature=${signature}`; - // Make request to AWS Rekognition - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': contentType, - 'X-Amz-Date': amzDate, - 'X-Amz-Target': amzTarget, - 'Authorization': authHeader - }, - body: JSON.stringify(request) - }); - const data = await response.json(); - return new Response(JSON.stringify({ - success: true, - faceDetails: data.FaceDetails || [], - count: (data.FaceDetails || []).length - }), { - headers: { - 'Content-Type': 'application/json' - } - }); - } catch (error) { - console.error("Error in detect-face function:", error); - return new Response(JSON.stringify({ - error: "Failed to process the image", - details: error instanceof Error ? error.message : String(error) - }), { - status: 500, - headers: { - 'Content-Type': 'application/json' - } - }); +import { RekognitionClient, DetectFacesCommand } from "npm:@aws-sdk/client-rekognition@^3.0.0"; +import { + validateAWSCredentials, + validateFile, + createErrorResponse, + createSuccessResponse, + type AWSCredentials +} from "../shared/aws-utils.ts"; + +interface DetectFaceResponse { + success: true; + faceDetails: any[]; + count: number; + imageSize?: number; + processingTime?: number; +} + +// Configuration constants +const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB +const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/png']; + +// Logger utility for structured logs +const logger = { + info: (message: string, data?: any) => { + console.log(`[INFO] [${new Date().toISOString()}] [detect-face] ${message}`, data ? data : ''); + }, + warn: (message: string, data?: any) => { + console.warn(`[WARN] [${new Date().toISOString()}] [detect-face] ${message}`, data ? data : ''); + }, + error: (message: string, error?: any) => { + console.error(`[ERROR] [${new Date().toISOString()}] [detect-face] ${message}`, error ? error : ''); + }, + debug: (message: string, data?: any) => { + console.debug(`[DEBUG] [${new Date().toISOString()}] [detect-face] ${message}`, data ? data : ''); + }, + success: (message: string, data?: any) => { + console.log(`[SUCCESS] [${new Date().toISOString()}] [detect-face] ${message}`, data ? data : ''); } -}); /* To invoke locally: +}; - 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) - 2. Make an HTTP request: +async function fileToUint8Array(file: File): Promise { + const buffer = await file.arrayBuffer(); + return new Uint8Array(buffer); +} - curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/detect-face' \ - --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ - --header 'Content-Type: application/json' \ - --data '{"name":"Functions"}' +serve(async (req: Request): Promise => { + const startTime = Date.now(); + const requestId = crypto.randomUUID(); -*/ + logger.info(`Starting face detection request [ID: ${requestId}]`); + + try { + // Validate HTTP method + if (req.method !== 'POST') { + logger.warn(`Invalid HTTP method ${req.method} [ID: ${requestId}]`); + return createErrorResponse('Method not allowed. Use POST', 405); + } + + // Validate AWS credentials + logger.debug(`Validating AWS credentials [ID: ${requestId}]`); + const { credentials, error: credError } = validateAWSCredentials(); + if (credError || !credentials) { + logger.error(`AWS credentials validation failed [ID: ${requestId}]`, credError); + return createErrorResponse(credError || 'AWS credentials not configured'); + } + + logger.debug(`AWS Region: ${credentials.region} [ID: ${requestId}]`); + + // Initialize Rekognition client + logger.debug(`Initializing Rekognition client [ID: ${requestId}]`); + const rekognitionClient = new RekognitionClient({ + region: credentials.region, + credentials: { + accessKeyId: credentials.accessKey, + secretAccessKey: credentials.secretKey + } + }); + + // Parse multipart form data + logger.debug(`Parsing form data [ID: ${requestId}]`); + let formData: FormData; + try { + formData = await req.formData(); + } catch (error) { + logger.error(`Failed to parse form data [ID: ${requestId}]`, error); + return createErrorResponse('Invalid form data. Expected multipart/form-data', 400); + } + + // Get and validate image file + const image = formData.get('image') as File | null; + logger.debug(`Validating image [ID: ${requestId}]`); + const validation = validateFile(image, 'image'); + + if (!validation.isValid) { + logger.warn(`Image validation failed [ID: ${requestId}]`, validation.error); + return createErrorResponse(validation.error || 'Invalid image file', 400); + } + + logger.info(`Processing image [ID: ${requestId}]`, { + name: image!.name, + size: `${(image!.size / 1024).toFixed(2)} KB`, + type: image!.type + }); + + // Convert image to Uint8Array + logger.debug(`Converting image to binary format [ID: ${requestId}]`); + let imageBytes: Uint8Array; + try { + imageBytes = await fileToUint8Array(image!); + } catch (error) { + logger.error(`Failed to convert image to bytes [ID: ${requestId}]`, error); + return createErrorResponse('Failed to process image data', 500); + } + + // Create DetectFaces command + logger.info(`Sending request to AWS Rekognition DetectFaces [ID: ${requestId}]`); + const detectFacesCommand = new DetectFacesCommand({ + Image: { + Bytes: imageBytes + }, + Attributes: ["ALL"] + }); + + // Execute AWS Rekognition request + let rekognitionResponse: any; + try { + rekognitionResponse = await rekognitionClient.send(detectFacesCommand); + } catch (error) { + logger.error(`AWS Rekognition request failed [ID: ${requestId}]`, error); + + // Handle specific AWS errors + if (error.name === 'InvalidImageFormatException') { + return createErrorResponse('Invalid image format. Please use JPEG or PNG format', 400); + } else if (error.name === 'ImageTooLargeException') { + return createErrorResponse('Image too large. Please reduce image size', 400); + } else if (error.name === 'InvalidParameterException') { + return createErrorResponse('Invalid parameters provided', 400); + } else if (error.name === 'InvalidS3ObjectException') { + return createErrorResponse('Invalid image data', 400); + } + + return createErrorResponse('Failed to analyze image with AWS Rekognition', 500); + } + + const processingTime = Date.now() - startTime; + const faceCount = (rekognitionResponse.FaceDetails || []).length; + + logger.success(`Face detection completed [ID: ${requestId}]`, { + facesDetected: faceCount, + processingTime: `${processingTime}ms`, + imageSize: `${(image!.size / 1024).toFixed(2)} KB` + }); + + // Log detailed face information if faces are detected + if (faceCount > 0) { + const faceDetails = rekognitionResponse.FaceDetails.map((face: any, index: number) => { + return { + faceIndex: index + 1, + ageRange: face.AgeRange ? `${face.AgeRange.Low}-${face.AgeRange.High}` : 'Unknown', + gender: face.Gender ? `${face.Gender.Value} (${face.Gender.Confidence.toFixed(2)}%)` : 'Unknown', + emotions: face.Emotions ? + face.Emotions.map((e: any) => `${e.Type}(${e.Confidence.toFixed(2)}%)`).join(', ') : + 'None detected', + quality: face.Quality ? + `Brightness=${face.Quality.Brightness.toFixed(2)}, Sharpness=${face.Quality.Sharpness.toFixed(2)}` : + 'Unknown' + }; + }); + + logger.info(`Face details [ID: ${requestId}]`, { faces: faceDetails }); + } else { + logger.info(`No faces detected in image [ID: ${requestId}]`); + } + + // Prepare response + const response: DetectFaceResponse = { + success: true, + faceDetails: rekognitionResponse.FaceDetails || [], + count: faceCount, + imageSize: image!.size, + processingTime + }; + + return createSuccessResponse(response); + + } catch (error) { + const processingTime = Date.now() - startTime; + logger.error(`Unexpected error in detect-face function [ID: ${requestId}]`, error); + logger.error(`Processing time before error: ${processingTime}ms [ID: ${requestId}]`); + + return createErrorResponse( + "An unexpected error occurred while processing the image", + 500 + ); + } +}); + +/* +To invoke locally: + +1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) +2. Make an HTTP request: + +curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/detect-face' \ + --header 'Authorization: Bearer [YOUR_ANON_KEY]' \ + --form 'image=@"path/to/your/image.jpg"' + +Example response: +{ + "success": true, + "faceDetails": [ + { + "BoundingBox": { + "Width": 0.23, + "Height": 0.34, + "Left": 0.35, + "Top": 0.25 + }, + "AgeRange": { + "Low": 25, + "High": 35 + }, + "Smile": { + "Value": true, + "Confidence": 95.5 + }, + "Eyeglasses": { + "Value": false, + "Confidence": 99.2 + }, + "Sunglasses": { + "Value": false, + "Confidence": 99.8 + }, + "Gender": { + "Value": "Male", + "Confidence": 96.8 + }, + "Beard": { + "Value": false, + "Confidence": 85.6 + }, + "Mustache": { + "Value": false, + "Confidence": 90.3 + }, + "EyesOpen": { + "Value": true, + "Confidence": 98.7 + }, + "MouthOpen": { + "Value": false, + "Confidence": 89.4 + }, + "Emotions": [ + { + "Type": "HAPPY", + "Confidence": 92.5 + }, + { + "Type": "CALM", + "Confidence": 5.2 + } + ], + "Landmarks": [...], + "Pose": { + "Roll": -2.1, + "Yaw": 1.8, + "Pitch": -3.5 + }, + "Quality": { + "Brightness": 78.5, + "Sharpness": 95.2 + }, + "Confidence": 99.8 + } + ], + "count": 1, + "imageSize": 1048576, + "processingTime": 1250 +} + +Environment Variables Required: +- AWS_ACCESS_KEY_ID +- AWS_SECRET_ACCESS_KEY +- AWS_REGION (optional, defaults to us-east-1) +*/ \ No newline at end of file diff --git a/sigap-website/supabase/functions/shared/aws-utils.ts b/sigap-website/supabase/functions/shared/aws-utils.ts new file mode 100644 index 0000000..aabaa36 --- /dev/null +++ b/sigap-website/supabase/functions/shared/aws-utils.ts @@ -0,0 +1,258 @@ +// utils/aws-utils.ts +// Shared utilities for AWS Rekognition functions + +export interface AWSCredentials { + region: string; + accessKey: string; + secretKey: string; +} + +export interface ValidationResult { + isValid: boolean; + error?: string; +} + +/** + * Validate AWS credentials from environment variables + */ +export function validateAWSCredentials(): { credentials?: AWSCredentials; error?: string } { + const region = Deno.env.get('AWS_REGION'); + const accessKey = Deno.env.get('AWS_ACCESS_KEY_ID'); + const secretKey = Deno.env.get('AWS_SECRET_ACCESS_KEY'); + + const requiredEnvVars = [ + { name: 'AWS_REGION', value: region }, + { name: 'AWS_ACCESS_KEY_ID', value: accessKey }, + { name: 'AWS_SECRET_ACCESS_KEY', value: secretKey } + ]; + + const missingVars = requiredEnvVars + .filter(envVar => !envVar.value) + .map(envVar => envVar.name); + + if (missingVars.length > 0) { + return { + error: `Missing required environment variables: ${missingVars.join(', ')}` + }; + } + + return { + credentials: { + region: region!, + accessKey: accessKey!, + secretKey: secretKey! + } + }; +} + +/** + * Validate uploaded file + */ +export function validateFile(file: File | null, fieldName: string): ValidationResult { + if (!file || !(file instanceof File)) { + return { + isValid: false, + error: `${fieldName} file is required` + }; + } + + // Check file size (max 5MB) + const MAX_FILE_SIZE = 5 * 1024 * 1024; + if (file.size > MAX_FILE_SIZE) { + return { + isValid: false, + error: `${fieldName} file size too large. Maximum 5MB allowed` + }; + } + + // Check file type + const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png']; + if (!ALLOWED_TYPES.includes(file.type)) { + return { + isValid: false, + error: `Invalid ${fieldName} file type. Only JPEG and PNG allowed` + }; + } + + return { isValid: true }; +} + +/** + * Convert file to base64 + */ +export async function fileToBase64(file: File): Promise { + const buffer = await file.arrayBuffer(); + return btoa(String.fromCharCode(...new Uint8Array(buffer))); +} + +/** + * Create AWS Signature V4 for Rekognition requests + */ +export async function createAWSSignature( + request: any, + service: string, + target: string, + credentials: AWSCredentials +): Promise<{ authHeader: string; amzDate: string }> { + const date = new Date(); + const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, ''); + const dateStamp = amzDate.substring(0, 8); + const host = `${service}.${credentials.region}.amazonaws.com`; + + // Request details + const method = 'POST'; + const contentType = 'application/x-amz-json-1.1'; + const canonicalUri = '/'; + const canonicalQueryString = ''; + + // Create payload hash + const payloadHash = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(JSON.stringify(request)) + ).then(hash => + Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + ); + + // Create canonical headers + const canonicalHeaders = + `content-type:${contentType}\n` + + `host:${host}\n` + + `x-amz-date:${amzDate}\n` + + `x-amz-target:${target}\n`; + + const signedHeaders = 'content-type;host;x-amz-date;x-amz-target'; + + // Create canonical request + const canonicalRequest = + `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`; + + // Create string to sign + const algorithm = 'AWS4-HMAC-SHA256'; + const credentialScope = `${dateStamp}/${credentials.region}/${service}/aws4_request`; + + const canonicalRequestHash = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(canonicalRequest) + ).then(hash => + Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + ); + + const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${canonicalRequestHash}`; + + // Create signing key + const signingKey = await getSignatureKey( + credentials.secretKey, + dateStamp, + credentials.region, + service + ); + + // Create signature + const signature = await crypto.subtle.sign( + "HMAC", + await crypto.subtle.importKey("raw", signingKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), + new TextEncoder().encode(stringToSign) + ).then(hash => + Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + ); + + // Create authorization header + const authHeader = + `${algorithm} ` + + `Credential=${credentials.accessKey}/${credentialScope}, ` + + `SignedHeaders=${signedHeaders}, ` + + `Signature=${signature}`; + + return { authHeader, amzDate }; +} + +/** + * Helper function to create AWS signing key + */ +async function getSignatureKey( + key: string, + dateStamp: string, + regionName: string, + serviceName: string +): Promise { + const kDate = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(`AWS4${key}`), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + + const kRegion = await crypto.subtle.sign( + "HMAC", + kDate, + new TextEncoder().encode(regionName) + ); + + const kService = await crypto.subtle.sign( + "HMAC", + await crypto.subtle.importKey("raw", kRegion, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), + new TextEncoder().encode(serviceName) + ); + + return crypto.subtle.sign( + "HMAC", + await crypto.subtle.importKey("raw", kService, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), + new TextEncoder().encode("aws4_request") + ); +} + +/** + * Make request to AWS Rekognition + */ +export async function makeRekognitionRequest( + request: any, + target: string, + credentials: AWSCredentials +): Promise { + const { authHeader, amzDate } = await createAWSSignature(request, 'rekognition', target, credentials); + const endpoint = `https://rekognition.${credentials.region}.amazonaws.com/`; + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-amz-json-1.1', + 'X-Amz-Date': amzDate, + 'X-Amz-Target': target, + 'Authorization': authHeader + }, + body: JSON.stringify(request) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`AWS Rekognition request failed: ${response.status} ${errorText}`); + } + + return response.json(); +} + +/** + * Create error response + */ +export function createErrorResponse(error: string, status: number = 500): Response { + return new Response(JSON.stringify({ error }), { + status, + headers: { 'Content-Type': 'application/json' } + }); +} + +/** + * Create success response + */ +export function createSuccessResponse(data: any): Response { + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' } + }); +} \ No newline at end of file diff --git a/sigap-website/supabase/functions/verify-face/index.ts b/sigap-website/supabase/functions/verify-face/index.ts index 350220f..14b9c56 100644 --- a/sigap-website/supabase/functions/verify-face/index.ts +++ b/sigap-website/supabase/functions/verify-face/index.ts @@ -1,151 +1,283 @@ +// verify-face/index.ts // Follow this setup guide to integrate the Deno language server with your editor: // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. + // Setup type definitions for built-in Supabase Runtime APIs import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; -const AWS_REGION = Deno.env.get('AWS_REGION'); -const AWS_ACCESS_KEY = Deno.env.get('AWS_ACCESS_KEY'); -const AWS_SECRET_KEY = Deno.env.get('AWS_SECRET_KEY'); -serve(async (req)=>{ +import { RekognitionClient, CompareFacesCommand } from "npm:@aws-sdk/client-rekognition@^3.0.0"; +import { + validateAWSCredentials, + validateFile, + createErrorResponse, + createSuccessResponse +} from "../shared/aws-utils.ts"; + +interface VerifyFaceResponse { + success: true; + matched: boolean; + similarity: number; + similarityThreshold: number; + faceMatches: any[]; + unmatchedFaces: any[]; + idCardImageSize?: number; + selfieImageSize?: number; + processingTime?: number; + confidence?: 'HIGH' | 'MEDIUM' | 'LOW'; +} + +// Configuration constants +const DEFAULT_SIMILARITY_THRESHOLD = 70; +const HIGH_CONFIDENCE_THRESHOLD = 85; +const MEDIUM_CONFIDENCE_THRESHOLD = 75; +const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB +const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/png']; + +// Logger utility for structured logs +const logger = { + info: (message: string, data?: any) => { + console.log(`[INFO] [${new Date().toISOString()}] [verify-face] ${message}`, data ? data : ''); + }, + warn: (message: string, data?: any) => { + console.warn(`[WARN] [${new Date().toISOString()}] [verify-face] ${message}`, data ? data : ''); + }, + error: (message: string, error?: any) => { + console.error(`[ERROR] [${new Date().toISOString()}] [verify-face] ${message}`, error ? error : ''); + }, + debug: (message: string, data?: any) => { + console.debug(`[DEBUG] [${new Date().toISOString()}] [verify-face] ${message}`, data ? data : ''); + }, + success: (message: string, data?: any) => { + console.log(`[SUCCESS] [${new Date().toISOString()}] [verify-face] ${message}`, data ? data : ''); + } +}; + +async function fileToUint8Array(file: File): Promise { + const buffer = await file.arrayBuffer(); + return new Uint8Array(buffer); +} + +serve(async (req: Request): Promise => { + const startTime = Date.now(); + const requestId = crypto.randomUUID(); + + logger.info(`Starting face verification request [ID: ${requestId}]`); + try { - // Check if we have AWS credentials - if (!AWS_REGION || !AWS_ACCESS_KEY || !AWS_SECRET_KEY) { - return new Response(JSON.stringify({ - error: 'AWS credentials are not configured' - }), { - status: 500, - headers: { - 'Content-Type': 'application/json' - } - }); + // Validate HTTP method + if (req.method !== 'POST') { + logger.warn(`Invalid HTTP method ${req.method} [ID: ${requestId}]`); + return createErrorResponse('Method not allowed. Use POST', 405); } - // Parse the multipart form data to get the images - const formData = await req.formData(); - const ktpImage = formData.get('ktp'); - const selfieImage = formData.get('selfie'); - if (!ktpImage || !(ktpImage instanceof File) || !selfieImage || !(selfieImage instanceof File)) { - return new Response(JSON.stringify({ - error: 'Both KTP and selfie images are required' - }), { - status: 400, - headers: { - 'Content-Type': 'application/json' - } - }); + + // Validate AWS credentials + logger.debug(`Validating AWS credentials [ID: ${requestId}]`); + const { credentials, error: credError } = validateAWSCredentials(); + if (credError || !credentials) { + logger.error(`AWS credentials validation failed [ID: ${requestId}]`, credError); + return createErrorResponse(credError || 'AWS credentials not configured'); } - // Convert images to base64 - const ktpBuffer = await ktpImage.arrayBuffer(); - const selfieBuffer = await selfieImage.arrayBuffer(); - const ktpBase64 = btoa(String.fromCharCode(...new Uint8Array(ktpBuffer))); - const selfieBase64 = btoa(String.fromCharCode(...new Uint8Array(selfieBuffer))); - // Create AWS signature for authorization - const date = new Date(); - const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, ''); - const dateStamp = amzDate.substring(0, 8); - const host = `rekognition.${AWS_REGION}.amazonaws.com`; - const endpoint = `https://${host}/`; - const request = { - "SourceImage": { - "Bytes": ktpBase64 - }, - "TargetImage": { - "Bytes": selfieBase64 - }, - "SimilarityThreshold": 70 - }; - // AWS Signature V4 calculation - const method = 'POST'; - const service = 'rekognition'; - const contentType = 'application/x-amz-json-1.1'; - const amzTarget = 'RekognitionService.CompareFaces'; - const canonicalUri = '/'; - const canonicalQueryString = ''; - const payloadHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(request))).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join('')); - const canonicalHeaders = `content-type:${contentType}\n` + `host:${host}\n` + `x-amz-date:${amzDate}\n` + `x-amz-target:${amzTarget}\n`; - const signedHeaders = 'content-type;host;x-amz-date;x-amz-target'; - const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`; - const algorithm = 'AWS4-HMAC-SHA256'; - const credentialScope = `${dateStamp}/${AWS_REGION}/${service}/aws4_request`; - const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalRequest)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''))}`; - const getSignatureKey = async (key, dateStamp, regionName, serviceName)=>{ - const kDate = await crypto.subtle.importKey("raw", new TextEncoder().encode(`AWS4${key}`), { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]); - const kRegion = await crypto.subtle.sign("HMAC", kDate, new TextEncoder().encode(regionName)); - const kService = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kRegion, { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]), new TextEncoder().encode(serviceName)); - return crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kService, { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]), new TextEncoder().encode("aws4_request")); - }; - const signingKey = await getSignatureKey(AWS_SECRET_KEY, dateStamp, AWS_REGION, service); - const signature = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", signingKey, { - name: "HMAC", - hash: "SHA-256" - }, false, [ - "sign" - ]), new TextEncoder().encode(stringToSign)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join('')); - const authHeader = `${algorithm} ` + `Credential=${AWS_ACCESS_KEY}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, ` + `Signature=${signature}`; - // Make request to AWS Rekognition - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': contentType, - 'X-Amz-Date': amzDate, - 'X-Amz-Target': amzTarget, - 'Authorization': authHeader - }, - body: JSON.stringify(request) + + logger.debug(`AWS Region: ${credentials.region} [ID: ${requestId}]`); + + // Initialize Rekognition client + logger.debug(`Initializing Rekognition client [ID: ${requestId}]`); + const rekognitionClient = new RekognitionClient({ + region: credentials.region, + credentials: { + accessKeyId: credentials.accessKey, + secretAccessKey: credentials.secretKey + } }); - const data = await response.json(); - // Determine if verification passed - const matched = !!(data.FaceMatches && data.FaceMatches.length > 0); - let highestSimilarity = 0; - if (matched && data.FaceMatches && data.FaceMatches.length > 0) { - highestSimilarity = Math.max(...data.FaceMatches.map((match)=>match.Similarity || 0)); + + // Parse multipart form data + logger.debug(`Parsing form data [ID: ${requestId}]`); + let formData: FormData; + try { + formData = await req.formData(); + } catch (error) { + logger.error(`Failed to parse form data [ID: ${requestId}]`, error); + return createErrorResponse('Invalid form data. Expected multipart/form-data', 400); } - return new Response(JSON.stringify({ + + // Get and validate both images + const idCardImage = formData.get('idCard') as File | null; + const selfieImage = formData.get('selfie') as File | null; + + // Validate IDCARD image + logger.debug(`Validating IDCARD image [ID: ${requestId}]`); + const idCardValidation = validateFile(idCardImage, 'IDCARD'); + if (!idCardValidation.isValid) { + logger.warn(`IDCARD image validation failed [ID: ${requestId}]`, idCardValidation.error); + return createErrorResponse(idCardValidation.error || 'Invalid IDCARD image', 400); + } + + // Validate selfie image + logger.debug(`Validating selfie image [ID: ${requestId}]`); + const selfieValidation = validateFile(selfieImage, 'selfie'); + if (!selfieValidation.isValid) { + logger.warn(`Selfie image validation failed [ID: ${requestId}]`, selfieValidation.error); + return createErrorResponse(selfieValidation.error || 'Invalid selfie image', 400); + } + + // Log image details + logger.info(`Processing images [ID: ${requestId}]`, { + idCard: { + name: idCardImage!.name, + size: `${(idCardImage!.size / 1024).toFixed(2)} KB`, + type: idCardImage!.type + }, + selfie: { + name: selfieImage!.name, + size: `${(selfieImage!.size / 1024).toFixed(2)} KB`, + type: selfieImage!.type + } + }); + + // Get similarity threshold from form data or use default + const thresholdParam = formData.get('similarity_threshold'); + let similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD; + + if (thresholdParam) { + const parsedThreshold = parseFloat(thresholdParam.toString()); + if (!isNaN(parsedThreshold) && parsedThreshold >= 0 && parsedThreshold <= 100) { + similarityThreshold = parsedThreshold; + logger.debug(`Using custom similarity threshold: ${similarityThreshold}% [ID: ${requestId}]`); + } else { + logger.warn(`Invalid similarity threshold provided, using default: ${DEFAULT_SIMILARITY_THRESHOLD}% [ID: ${requestId}]`); + } + } + + // Convert images to Uint8Array + logger.debug(`Converting images to binary format [ID: ${requestId}]`); + let idCardBytes: Uint8Array, selfieBytes: Uint8Array; + try { + [idCardBytes, selfieBytes] = await Promise.all([ + fileToUint8Array(idCardImage!), + fileToUint8Array(selfieImage!) + ]); + } catch (error) { + logger.error(`Failed to convert images to bytes [ID: ${requestId}]`, error); + return createErrorResponse('Failed to process image data', 500); + } + + // Create CompareFaces command + logger.info(`Sending request to AWS Rekognition CompareFaces with threshold: ${similarityThreshold}% [ID: ${requestId}]`); + const compareFacesCommand = new CompareFacesCommand({ + SourceImage: { + Bytes: idCardBytes + }, + TargetImage: { + Bytes: selfieBytes + }, + SimilarityThreshold: similarityThreshold + }); + + // Execute AWS Rekognition request + let rekognitionResponse: any; + try { + rekognitionResponse = await rekognitionClient.send(compareFacesCommand); + } catch (error) { + logger.error(`AWS Rekognition request failed [ID: ${requestId}]`, error); + + // Handle specific AWS errors + if (error.name === 'InvalidImageFormatException') { + return createErrorResponse('Invalid image format. Please use JPEG or PNG format', 400); + } else if (error.name === 'ImageTooLargeException') { + return createErrorResponse('Image too large. Please reduce image size', 400); + } else if (error.name === 'InvalidParameterException') { + return createErrorResponse('Invalid parameters provided', 400); + } + + return createErrorResponse('Failed to compare faces with AWS Rekognition', 500); + } + + // Process results + const processingTime = Date.now() - startTime; + const matched = !!(rekognitionResponse.FaceMatches && rekognitionResponse.FaceMatches.length > 0); + + let highestSimilarity = 0; + if (matched && rekognitionResponse.FaceMatches && rekognitionResponse.FaceMatches.length > 0) { + highestSimilarity = Math.max(...rekognitionResponse.FaceMatches.map((match: any) => match.Similarity || 0)); + } + + // Determine confidence level + let confidence: 'HIGH' | 'MEDIUM' | 'LOW'; + if (highestSimilarity >= HIGH_CONFIDENCE_THRESHOLD) { + confidence = 'HIGH'; + } else if (highestSimilarity >= MEDIUM_CONFIDENCE_THRESHOLD) { + confidence = 'MEDIUM'; + } else { + confidence = 'LOW'; + } + + // Log results in a structured format + logger.success(`Face verification completed [ID: ${requestId}]`, { + matched: matched, + similarity: `${highestSimilarity.toFixed(2)}%`, + confidence: confidence, + processingTime: `${processingTime}ms`, + faceMatchesCount: rekognitionResponse.FaceMatches?.length || 0, + unmatchedFacesCount: rekognitionResponse.UnmatchedFaces?.length || 0 + }); + + // Prepare response + const response: VerifyFaceResponse = { success: true, matched: matched, - similarity: highestSimilarity, - faceMatches: data.FaceMatches || [], - unmatchedFaces: data.UnmatchedFaces || [] - }), { - headers: { - 'Content-Type': 'application/json' - } - }); + similarity: parseFloat(highestSimilarity.toFixed(2)), + similarityThreshold: similarityThreshold, + faceMatches: rekognitionResponse.FaceMatches || [], + unmatchedFaces: rekognitionResponse.UnmatchedFaces || [], + idCardImageSize: idCardImage!.size, + selfieImageSize: selfieImage!.size, + processingTime, + confidence + }; + + return createSuccessResponse(response); + } catch (error) { - console.error("Error in verify-face function:", error); - return new Response(JSON.stringify({ - error: "Failed to verify faces", - details: error instanceof Error ? error.message : String(error) - }), { - status: 500, - headers: { - 'Content-Type': 'application/json' - } - }); + const processingTime = Date.now() - startTime; + logger.error(`Unexpected error in verify-face function [ID: ${requestId}]`, error); + logger.error(`Processing time before error: ${processingTime}ms [ID: ${requestId}]`); + + return createErrorResponse( + "An unexpected error occurred while verifying faces", + 500 + ); } -}); /* To invoke locally: +}); - 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) - 2. Make an HTTP request: +/* +To invoke locally: - curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/verify-face' \ - --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ - --header 'Content-Type: application/json' \ - --data '{"name":"Functions"}' +1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) +2. Make an HTTP request: -*/ +curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/verify-face' \ + --header 'Authorization: Bearer [YOUR_ANON_KEY]' \ + --form 'idCard=@"path/to/idCard.jpg"' \ + --form 'selfie=@"path/to/selfie.jpg"' \ + --form 'similarity_threshold=75' + +Example response: +{ + "success": true, + "matched": true, + "similarity": 87.45, + "similarityThreshold": 75, + "faceMatches": [...], + "unmatchedFaces": [], + "idCardImageSize": 1048576, + "selfieImageSize": 2097152, + "processingTime": 1450, + "confidence": "HIGH" +} + +Environment Variables Required: +- AWS_ACCESS_KEY_ID +- AWS_SECRET_ACCESS_KEY +- AWS_REGION (optional, defaults to us-east-1) +*/ \ No newline at end of file