diff --git a/.bruno/identity/login/code.bru b/.bruno/identity/login/code.bru index 60e315f..e96d53b 100644 --- a/.bruno/identity/login/code.bru +++ b/.bruno/identity/login/code.bru @@ -12,9 +12,8 @@ post { body:json { { - "deviceId": "", - "preAuthSessionId": "", - "userInputCode": "" + "email": "john.doe@fixture.none", + "otp": "" } } diff --git a/.bruno/identity/login/email.bru b/.bruno/identity/login/email.bru index b39f783..7025c71 100644 --- a/.bruno/identity/login/email.bru +++ b/.bruno/identity/login/email.bru @@ -12,7 +12,6 @@ post { body:json { { - "base": "http://localhost:5170", "email": "john.doe@fixture.none" } } diff --git a/.bruno/workspace/create.bru b/.bruno/workspace/create.bru index 38cd8e0..10b7829 100644 --- a/.bruno/workspace/create.bru +++ b/.bruno/workspace/create.bru @@ -12,7 +12,7 @@ post { body:json { { - "name": "valkyr" + "name": "" } } diff --git a/api/package.json b/api/package.json index 80a4434..fac7237 100644 --- a/api/package.json +++ b/api/package.json @@ -4,6 +4,8 @@ "start": "deno --allow-all --watch-hmr=routes/ server.ts" }, "dependencies": { + "@modules/identity": "workspace:*", + "@module/workspace": "workspace:*", "zod": "4.1.11" } } \ No newline at end of file diff --git a/api/server.ts b/api/server.ts index bdec019..9d7a316 100644 --- a/api/server.ts +++ b/api/server.ts @@ -1,15 +1,14 @@ import identity from "@modules/identity/server.ts"; import workspace from "@modules/workspace/server.ts"; -import database from "@platform/database/server.ts"; import { logger } from "@platform/logger"; import { context } from "@platform/relay"; import { Api } from "@platform/server/api.ts"; import server from "@platform/server/server.ts"; import socket from "@platform/socket/server.ts"; import { storage } from "@platform/storage"; -import supertokens from "@platform/supertoken/server.ts"; import { config } from "./config.ts"; +import session from "./services/session.ts"; const log = logger.prefix("Server"); @@ -21,10 +20,9 @@ const log = logger.prefix("Server"); // ### Platform -await database.bootstrap(); await server.bootstrap(); await socket.bootstrap(); -await supertokens.bootsrap(); +await session.bootstrap(); // ### Modules @@ -61,7 +59,7 @@ Deno.serve( await server.resolve(request); await socket.resolve(); - await supertokens.resolve(request); + await session.resolve(request); // ### Fetch // Execute fetch against the api instance. diff --git a/api/services/session.ts b/api/services/session.ts new file mode 100644 index 0000000..9ba21d2 --- /dev/null +++ b/api/services/session.ts @@ -0,0 +1,114 @@ +import "@modules/identity/server.ts"; + +import { getAccessControlMethods, identity } from "@modules/identity/server.ts"; +import { context, UnauthorizedError } from "@platform/relay"; +import { storage } from "@platform/storage"; + +const IDENTITY_RESOLVE_HEADER = "x-identity-resolver"; + +export default { + bootstrap: async () => { + bootstrapSessionContext(); + }, + + resolve: async (request: Request) => { + await resolvePrincipalSession(request); + }, +}; + +function bootstrapSessionContext() { + Object.defineProperties(context, { + /** + * TODO ... + */ + isAuthenticated: { + get() { + return storage.getStore()?.principal !== undefined; + }, + }, + + /** + * TODO ... + */ + session: { + get() { + const session = storage.getStore()?.session; + if (session === undefined) { + throw new UnauthorizedError(); + } + return session; + }, + }, + + /** + * TODO ... + */ + principal: { + get() { + const principal = storage.getStore()?.principal; + if (principal === undefined) { + throw new UnauthorizedError(); + } + return principal; + }, + }, + + /** + * TODO ... + */ + access: { + get() { + const access = storage.getStore()?.access; + if (access === undefined) { + throw new UnauthorizedError(); + } + return access; + }, + }, + }); +} + +async function resolvePrincipalSession(request: Request) { + // ### Resolver + // Check if the incoming request is tagged as a resolver check. + // If it is a resolver we break out of the session resolution + // to avoid an infinite resolution loop. + + const isResolver = request.headers.get(IDENTITY_RESOLVE_HEADER) !== null; + if (isResolver) { + return; + } + + // ### Cookie + // Check for the existence of cookie to pass onto the session + // resolver. + + const cookie = request.headers.get("cookie"); + if (cookie === null) { + return; + } + + // ### Session + // Fetch session from identity module and tag it as a resolution + // call so it can break out of a resolution loop. + + const session = await identity.resolve({ + headers: new Headers({ + cookie, + [IDENTITY_RESOLVE_HEADER]: "true", + }), + }); + + // ### Populate Context + // On successfull resolution we build the request identity context. + + if ("data" in session) { + const context = storage.getStore(); + if (context === undefined) { + return; + } + context.session = session.data.session; + context.principal = session.data.principal; + context.access = getAccessControlMethods(session.data.principal); + } +} diff --git a/deno.json b/deno.json index cc2dbae..e73f304 100644 --- a/deno.json +++ b/deno.json @@ -6,6 +6,7 @@ "apps/react", "modules/identity", "modules/workspace", + "platform/cerbos", "platform/config", "platform/database", "platform/logger", @@ -14,7 +15,6 @@ "platform/socket", "platform/spec", "platform/storage", - "platform/supertoken", "platform/vault" ], "imports": { @@ -22,6 +22,7 @@ "@modules/identity/server.ts": "./modules/identity/server.ts", "@modules/workspace/client.ts": "./modules/workspace/client.ts", "@modules/workspace/server.ts": "./modules/workspace/server.ts", + "@platform/cerbos": "./platform/cerbos/mod.ts", "@platform/config/": "./platform/config/", "@platform/database/": "./platform/database/", "@platform/logger": "./platform/logger/mod.ts", @@ -30,7 +31,6 @@ "@platform/socket/": "./platform/socket/", "@platform/spec/": "./platform/spec/", "@platform/storage": "./platform/storage/storage.ts", - "@platform/supertoken/": "./platform/supertoken/", "@platform/vault": "./platform/vault/vault.ts" }, "tasks": { diff --git a/deno.lock b/deno.lock index 2a0e7d7..461601f 100644 --- a/deno.lock +++ b/deno.lock @@ -11,13 +11,15 @@ "npm:@jsr/valkyr__event-store@2.0.1": "2.0.1", "npm:@jsr/valkyr__inverse@1.0.1": "1.0.1", "npm:@jsr/valkyr__json-rpc@1.1.0": "1.1.0", - "npm:@tailwindcss/vite@4.1.13": "4.1.13_vite@7.1.6__picomatch@4.0.3", + "npm:@tailwindcss/vite@4.1.13": "4.1.13_vite@7.1.6__picomatch@4.0.3_@types+node@24.2.0", "npm:@tanstack/react-query@5.89.0": "5.89.0_react@19.1.1", "npm:@tanstack/react-router-devtools@1.131.47": "1.131.47_@tanstack+react-router@1.131.47__react@19.1.1__react-dom@19.1.1___react@19.1.1_react@19.1.1_react-dom@19.1.1__react@19.1.1", "npm:@tanstack/react-router@1.131.47": "1.131.47_react@19.1.1_react-dom@19.1.1__react@19.1.1", + "npm:@types/node@*": "24.2.0", "npm:@types/react-dom@19.1.9": "19.1.9_@types+react@19.1.13", "npm:@types/react@19.1.13": "19.1.13", - "npm:@vitejs/plugin-react@4.7.0": "4.7.0_vite@7.1.6__picomatch@4.0.3_@babel+core@7.28.4", + "npm:@vitejs/plugin-react@4.7.0": "4.7.0_vite@7.1.6__picomatch@4.0.3_@babel+core@7.28.4_@types+node@24.2.0", + "npm:better-auth@1.3.16": "1.3.16_react@19.1.1_react-dom@19.1.1__react@19.1.1", "npm:cookie@1.0.2": "1.0.2", "npm:eslint-plugin-react-hooks@5.2.0": "5.2.0_eslint@9.35.0", "npm:eslint-plugin-react-refresh@0.4.20": "0.4.20_eslint@9.35.0", @@ -32,11 +34,10 @@ "npm:prettier@3.6.2": "3.6.2", "npm:react-dom@19.1.1": "19.1.1_react@19.1.1", "npm:react@19.1.1": "19.1.1", - "npm:supertokens-node@23.0.1": "23.0.1", "npm:tailwindcss@4.1.13": "4.1.13", "npm:typescript-eslint@8.44.0": "8.44.0_eslint@9.35.0_typescript@5.9.2_@typescript-eslint+parser@8.44.0__eslint@9.35.0__typescript@5.9.2", "npm:typescript@5.9.2": "5.9.2", - "npm:vite@7.1.6": "7.1.6_picomatch@4.0.3", + "npm:vite@7.1.6": "7.1.6_picomatch@4.0.3_@types+node@24.2.0", "npm:zod@4.1.11": "4.1.11" }, "npm": { @@ -177,6 +178,12 @@ "@babel/helper-validator-identifier" ] }, + "@better-auth/utils@0.3.0": { + "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==" + }, + "@better-fetch/fetch@1.1.18": { + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" + }, "@cerbos/core@0.24.1": { "integrity": "sha512-Gt9ETQR3WDVcPlxN+HiGUDtNgWFulwS5ZjBgzJFsdb7e2GCw0tOPE9Ex1qHNZvG/0JHpFWJWIiYaSKyXcp35YQ==", "dependencies": [ @@ -374,6 +381,9 @@ "levn" ] }, + "@hexagon/base64@1.1.28": { + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==" + }, "@humanfs/core@0.19.1": { "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==" }, @@ -530,12 +540,21 @@ ], "tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__testcontainers/2.0.2.tgz" }, + "@levischuck/tiny-cbor@0.2.11": { + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==" + }, "@mongodb-js/saslprep@1.3.0": { "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", "dependencies": [ "sparse-bitfield" ] }, + "@noble/ciphers@2.0.1": { + "integrity": "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==" + }, + "@noble/hashes@2.0.0": { + "integrity": "sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw==" + }, "@nodelib/fs.scandir@2.1.5": { "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dependencies": [ @@ -553,6 +572,49 @@ "fastq" ] }, + "@peculiar/asn1-android@2.5.0": { + "integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==", + "dependencies": [ + "@peculiar/asn1-schema", + "asn1js", + "tslib" + ] + }, + "@peculiar/asn1-ecc@2.5.0": { + "integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==", + "dependencies": [ + "@peculiar/asn1-schema", + "@peculiar/asn1-x509", + "asn1js", + "tslib" + ] + }, + "@peculiar/asn1-rsa@2.5.0": { + "integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==", + "dependencies": [ + "@peculiar/asn1-schema", + "@peculiar/asn1-x509", + "asn1js", + "tslib" + ] + }, + "@peculiar/asn1-schema@2.5.0": { + "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==", + "dependencies": [ + "asn1js", + "pvtsutils", + "tslib" + ] + }, + "@peculiar/asn1-x509@2.5.0": { + "integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==", + "dependencies": [ + "@peculiar/asn1-schema", + "asn1js", + "pvtsutils", + "tslib" + ] + }, "@rolldown/pluginutils@1.0.0-beta.27": { "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==" }, @@ -666,6 +728,21 @@ "os": ["win32"], "cpu": ["x64"] }, + "@simplewebauthn/browser@13.2.0": { + "integrity": "sha512-N3fuA1AAnTo5gCStYoIoiasPccC+xPLx2YU88Dv0GeAmPQTWHETlZQq5xZ0DgUq1H9loXMWQH5qqUjcI7BHJ1A==" + }, + "@simplewebauthn/server@13.1.2": { + "integrity": "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==", + "dependencies": [ + "@hexagon/base64", + "@levischuck/tiny-cbor", + "@peculiar/asn1-android", + "@peculiar/asn1-ecc", + "@peculiar/asn1-rsa", + "@peculiar/asn1-schema", + "@peculiar/asn1-x509" + ] + }, "@tailwindcss/node@4.1.13": { "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", "dependencies": [ @@ -765,7 +842,16 @@ "@tailwindcss/node", "@tailwindcss/oxide", "tailwindcss", - "vite" + "vite@7.1.6_picomatch@4.0.3" + ] + }, + "@tailwindcss/vite@4.1.13_vite@7.1.6__picomatch@4.0.3_@types+node@24.2.0": { + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", + "dependencies": [ + "@tailwindcss/node", + "@tailwindcss/oxide", + "tailwindcss", + "vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0" ] }, "@tanstack/history@1.131.2": { @@ -872,6 +958,12 @@ "@types/json-schema@7.0.15": { "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "@types/node@24.2.0": { + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "dependencies": [ + "undici-types" + ] + }, "@types/react-dom@19.1.9_@types+react@19.1.13": { "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "dependencies": [ @@ -1002,7 +1094,19 @@ "@rolldown/pluginutils", "@types/babel__core", "react-refresh", - "vite" + "vite@7.1.6_picomatch@4.0.3" + ] + }, + "@vitejs/plugin-react@4.7.0_vite@7.1.6__picomatch@4.0.3_@babel+core@7.28.4_@types+node@24.2.0": { + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dependencies": [ + "@babel/core", + "@babel/plugin-transform-react-jsx-self", + "@babel/plugin-transform-react-jsx-source", + "@rolldown/pluginutils", + "@types/babel__core", + "react-refresh", + "vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0" ] }, "acorn-jsx@5.3.2_acorn@8.15.0": { @@ -1015,12 +1119,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": true }, - "agent-base@6.0.2": { - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": [ - "debug" - ] - }, "ajv@6.12.6": { "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dependencies": [ @@ -1039,27 +1137,54 @@ "argparse@2.0.1": { "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "asynckit@0.4.0": { - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios@1.11.0": { - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "asn1js@3.0.6": { + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", "dependencies": [ - "follow-redirects", - "form-data", - "proxy-from-env" + "pvtsutils", + "pvutils", + "tslib" ] }, "balanced-match@1.0.2": { "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "base64-js@1.5.1": { - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, "baseline-browser-mapping@2.8.6": { "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", "bin": true }, + "better-auth@1.3.16_react@19.1.1_react-dom@19.1.1__react@19.1.1": { + "integrity": "sha512-WHU3QTtkBdwMlzM4AbOSoti0aPVTw1IfdomNCcP9Mo0J03ENn7z2a6/Vz9fimHmjpVWMqRvW72cYcc30LLFFOw==", + "dependencies": [ + "@better-auth/utils", + "@better-fetch/fetch", + "@noble/ciphers", + "@noble/hashes", + "@simplewebauthn/browser", + "@simplewebauthn/server", + "better-call", + "defu", + "jose", + "kysely", + "nanostores", + "react", + "react-dom", + "zod" + ], + "optionalPeers": [ + "react", + "react-dom" + ] + }, + "better-call@1.0.19": { + "integrity": "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==", + "dependencies": [ + "@better-auth/utils", + "@better-fetch/fetch", + "rou3", + "set-cookie-parser", + "uncrypto" + ] + }, "brace-expansion@1.1.12": { "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dependencies": [ @@ -1093,16 +1218,6 @@ "bson@6.10.4": { "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" }, - "buffer-equal-constant-time@1.0.1": { - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "buffer@6.0.3": { - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dependencies": [ - "base64-js", - "ieee754" - ] - }, "call-bind-apply-helpers@1.0.2": { "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": [ @@ -1145,36 +1260,18 @@ "color-name@1.1.4": { "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "combined-stream@1.0.8": { - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": [ - "delayed-stream" - ] - }, "concat-map@0.0.1": { "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "content-type@1.0.5": { - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" - }, "convert-source-map@2.0.0": { "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "cookie-es@1.2.2": { "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" }, - "cookie@0.7.2": { - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" - }, "cookie@1.0.2": { "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" }, - "cross-fetch@3.2.0": { - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dependencies": [ - "node-fetch" - ] - }, "cross-spawn@7.0.6": { "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": [ @@ -1183,15 +1280,9 @@ "which" ] }, - "crypto-js@4.2.0": { - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" - }, "csstype@3.1.3": { "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "dayjs@1.11.18": { - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==" - }, "debug@4.4.3": { "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": [ @@ -1201,8 +1292,8 @@ "deep-is@0.1.4": { "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, - "delayed-stream@1.0.0": { - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + "defu@6.1.4": { + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" }, "detect-libc@2.1.0": { "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==" @@ -1215,12 +1306,6 @@ "gopd" ] }, - "ecdsa-sig-formatter@1.0.11": { - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": [ - "safe-buffer" - ] - }, "electron-to-chromium@1.5.222": { "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==" }, @@ -1243,15 +1328,6 @@ "es-errors" ] }, - "es-set-tostringtag@2.1.0": { - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": [ - "es-errors", - "get-intrinsic", - "has-tostringtag", - "hasown" - ] - }, "esbuild@0.25.10": { "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "optionalDependencies": [ @@ -1458,19 +1534,6 @@ "flatted@3.3.3": { "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" }, - "follow-redirects@1.15.11": { - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==" - }, - "form-data@4.0.4": { - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dependencies": [ - "asynckit", - "combined-stream", - "es-set-tostringtag", - "hasown", - "mime-types" - ] - }, "fsevents@2.3.3": { "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "os": ["darwin"], @@ -1543,31 +1606,15 @@ "has-symbols@1.1.0": { "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, - "has-tostringtag@1.0.2": { - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": [ - "has-symbols" - ] - }, "hasown@2.0.2": { "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": [ "function-bind" ] }, - "https-proxy-agent@5.0.1": { - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": [ - "agent-base", - "debug" - ] - }, "idb@8.0.3": { "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==" }, - "ieee754@1.2.1": { - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, "ignore@5.3.2": { "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" }, @@ -1606,9 +1653,6 @@ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "bin": true }, - "jose@4.15.9": { - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" - }, "jose@6.1.0": { "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==" }, @@ -1639,42 +1683,15 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": true }, - "jsonwebtoken@9.0.2": { - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": [ - "jws", - "lodash.includes", - "lodash.isboolean", - "lodash.isinteger", - "lodash.isnumber", - "lodash.isplainobject", - "lodash.isstring", - "lodash.once", - "ms", - "semver@7.7.2" - ] - }, - "jwa@1.4.2": { - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "dependencies": [ - "buffer-equal-constant-time", - "ecdsa-sig-formatter", - "safe-buffer" - ] - }, - "jws@3.2.2": { - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": [ - "jwa", - "safe-buffer" - ] - }, "keyv@4.5.4": { "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": [ "json-buffer" ] }, + "kysely@0.28.5": { + "integrity": "sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA==" + }, "levn@0.4.1": { "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dependencies": [ @@ -1682,9 +1699,6 @@ "type-check" ] }, - "libphonenumber-js@1.12.21": { - "integrity": "sha512-z/o0jBYS3d8js1QBksrHxZUARjmM0S6uvpINkyJ9IcPkXIoUh5l4S3rTbGAlq9ThbCExdSV0wWp8gw7729A/ww==" - }, "lightningcss-darwin-arm64@1.30.1": { "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "os": ["darwin"], @@ -1759,30 +1773,9 @@ "p-locate" ] }, - "lodash.includes@4.3.0": { - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "lodash.isboolean@3.0.3": { - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "lodash.isinteger@4.0.4": { - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "lodash.isnumber@3.0.3": { - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "lodash.isplainobject@4.0.6": { - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "lodash.isstring@4.0.1": { - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "lodash.merge@4.6.2": { "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "lodash.once@4.1.1": { - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, "lru-cache@5.1.1": { "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dependencies": [ @@ -1811,15 +1804,6 @@ "picomatch@2.3.1" ] }, - "mime-db@1.52.0": { - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types@2.1.35": { - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": [ - "mime-db" - ] - }, "mingo@6.6.1": { "integrity": "sha512-KC6b1ODYoSdYu5fBm+SzQb7fa4ARmGwfa3Cf9F7U+2mnfD4Zhf89qQgO1cPTtaJ68w3ntIT5dVujgF52HvN7+g==" }, @@ -1848,7 +1832,7 @@ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "dependencies": [ "@types/whatwg-url", - "whatwg-url@14.2.0" + "whatwg-url" ] }, "mongodb@6.20.0": { @@ -1870,21 +1854,15 @@ "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", "bin": true }, + "nanostores@1.0.1": { + "integrity": "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==" + }, "natural-compare@1.4.0": { "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, - "node-fetch@2.7.0": { - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": [ - "whatwg-url@5.0.0" - ] - }, "node-releases@2.0.21": { "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==" }, - "nodemailer@6.10.1": { - "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==" - }, "object-inspect@1.13.4": { "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, @@ -1911,9 +1889,6 @@ "p-limit" ] }, - "pako@2.1.0": { - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, "parent-module@1.0.1": { "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dependencies": [ @@ -1938,12 +1913,6 @@ "picomatch@4.0.3": { "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" }, - "pkce-challenge@3.1.0": { - "integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==", - "dependencies": [ - "crypto-js" - ] - }, "postcss@8.5.6": { "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dependencies": [ @@ -1962,24 +1931,24 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "bin": true }, - "process@0.11.10": { - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, - "proxy-from-env@1.1.0": { - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "punycode@2.3.1": { "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "pvtsutils@1.3.6": { + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dependencies": [ + "tslib" + ] + }, + "pvutils@1.1.3": { + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" + }, "qs@6.14.0": { "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dependencies": [ "side-channel" ] }, - "querystringify@2.2.0": { - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "queue-microtask@1.2.3": { "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, @@ -1996,9 +1965,6 @@ "react@19.1.1": { "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==" }, - "requires-port@1.0.0": { - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "resolve-from@4.0.0": { "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, @@ -2037,6 +2003,9 @@ ], "bin": true }, + "rou3@0.5.1": { + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==" + }, "run-parallel@1.2.0": { "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dependencies": [ @@ -2049,15 +2018,9 @@ "tslib" ] }, - "safe-buffer@5.2.1": { - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, "scheduler@0.26.0": { "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, - "scmp@2.1.0": { - "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" - }, "semver@6.3.1": { "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": true @@ -2143,29 +2106,6 @@ "strip-json-comments@3.1.1": { "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, - "supertokens-js-override@0.0.4": { - "integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==" - }, - "supertokens-node@23.0.1": { - "integrity": "sha512-cCuY9Y5Mj93Pg1ktbqilouWgAoQWniQauftB4Ef6rfOchogx13XTo1pNP14zezn2rSf7WIPb9iaZb5zif6TKtQ==", - "dependencies": [ - "buffer", - "content-type", - "cookie@0.7.2", - "cross-fetch", - "debug", - "jose@4.15.9", - "libphonenumber-js", - "nodemailer", - "pako", - "pkce-challenge", - "process", - "set-cookie-parser", - "supertokens-js-override", - "tldts", - "twilio" - ] - }, "supports-color@7.2.0": { "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": [ @@ -2201,25 +2141,12 @@ "picomatch@4.0.3" ] }, - "tldts-core@6.1.86": { - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==" - }, - "tldts@6.1.86": { - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dependencies": [ - "tldts-core" - ], - "bin": true - }, "to-regex-range@5.0.1": { "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": [ "is-number" ] }, - "tr46@0.0.3": { - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "tr46@5.1.1": { "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dependencies": [ @@ -2235,19 +2162,6 @@ "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "twilio@4.23.0": { - "integrity": "sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==", - "dependencies": [ - "axios", - "dayjs", - "https-proxy-agent", - "jsonwebtoken", - "qs", - "scmp", - "url-parse", - "xmlbuilder" - ] - }, "type-check@0.4.0": { "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dependencies": [ @@ -2269,6 +2183,12 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "bin": true }, + "uncrypto@0.1.3": { + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" + }, + "undici-types@7.10.0": { + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, "update-browserslist-db@1.1.3_browserslist@4.26.2": { "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dependencies": [ @@ -2284,13 +2204,6 @@ "punycode" ] }, - "url-parse@1.5.10": { - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": [ - "querystringify", - "requires-port" - ] - }, "use-sync-external-store@1.5.0_react@19.1.1": { "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "dependencies": [ @@ -2316,8 +2229,24 @@ ], "bin": true }, - "webidl-conversions@3.0.1": { - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0": { + "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", + "dependencies": [ + "@types/node", + "esbuild", + "fdir", + "picomatch@4.0.3", + "postcss", + "rollup", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "optionalPeers": [ + "@types/node" + ], + "bin": true }, "webidl-conversions@7.0.0": { "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" @@ -2325,15 +2254,8 @@ "whatwg-url@14.2.0": { "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dependencies": [ - "tr46@5.1.1", - "webidl-conversions@7.0.0" - ] - }, - "whatwg-url@5.0.0": { - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": [ - "tr46@0.0.3", - "webidl-conversions@3.0.1" + "tr46", + "webidl-conversions" ] }, "which@2.0.2": { @@ -2346,9 +2268,6 @@ "word-wrap@1.2.5": { "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" }, - "xmlbuilder@13.0.2": { - "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==" - }, "yallist@3.1.1": { "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, @@ -2412,7 +2331,8 @@ "modules/identity": { "packageJson": { "dependencies": [ - "npm:supertokens-node@23.0.1", + "npm:better-auth@1.3.16", + "npm:cookie@1.0.2", "npm:zod@4.1.11" ] } @@ -2426,6 +2346,14 @@ ] } }, + "platform/cerbos": { + "packageJson": { + "dependencies": [ + "npm:@cerbos/http@0.23.1", + "npm:zod@4.1.11" + ] + } + }, "platform/config": { "packageJson": { "dependencies": [ @@ -2481,16 +2409,6 @@ ] } }, - "platform/supertoken": { - "packageJson": { - "dependencies": [ - "npm:@cerbos/http@0.23.1", - "npm:cookie@1.0.2", - "npm:supertokens-node@23.0.1", - "npm:zod@4.1.11" - ] - } - }, "platform/vault": { "packageJson": { "dependencies": [ diff --git a/docker-compose.yml b/docker-compose.yml index 4dbb9a7..7ddd5d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,49 +17,6 @@ services: networks: - localdev - # Super Tokens Database - # -------------------------------------------------------------------------------- - # Used by supertokens instance to store and manage principals. - - stdb: - image: 'postgres:latest' - environment: - POSTGRES_USER: supertokens_user - POSTGRES_PASSWORD: somePassword - POSTGRES_DB: supertokens - ports: - - 5432:5432 - networks: - - localdev - restart: unless-stopped - healthcheck: - test: ['CMD', 'pg_isready', '-U', 'supertokens_user', '-d', 'supertokens'] - interval: 5s - timeout: 5s - retries: 5 - - # Super Tokens - # -------------------------------------------------------------------------------- - - supertokens: - image: registry.supertokens.io/supertokens/supertokens-postgresql:latest - depends_on: - stdb: - condition: service_healthy - ports: - - 3567:3567 - environment: - POSTGRESQL_CONNECTION_URI: "postgresql://supertokens_user:somePassword@stdb:5432/supertokens" - networks: - - localdev - restart: unless-stopped - healthcheck: - test: > - bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"' - interval: 10s - timeout: 5s - retries: 5 - # Cerbos # -------------------------------------------------------------------------------- # Policy engine for application access control. diff --git a/modules/identity/.keys/private b/modules/identity/.keys/private deleted file mode 100644 index 42062bf..0000000 --- a/modules/identity/.keys/private +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCy5ZoXkKP9mZTk -sKbQdSwspHZqyMH33Gby23+9ycNHMIww7djcWFfPRW4s7tu3SNaac6qVg9OI43+Z -6BPXxuh4nhQ4LX5No9iVEmcWvZtKE4ghwzsoU0llT7+aKl9UYvgqU1YX4zyfiyo2 -bW0nVPasEHTyjLCVPK5BKlq+UmuyJTVcduALDnVETpUefu5Vca6tIRXsOovvAf5b -zmcxPccaXIatR/AeipxT0YWoInn8dxD3kyFgTPXtinuBZxvp6MUeSs5IE8OJRJRP -PEo1MQ9HFw9aYRIn9uIkbARbNZMGz77zB1+0TrPGyKOB5lLReWGMUFAJhjLrnTsY -z19se4kNAgMBAAECgf9QkG6A6ViiHIMnUskIDeP5Xir19d9kbGwrcn0F2OXYaX+l -Oot9w3KM6loRJx380/zk/e0Uch1MeZ2fyqQRUmAGQIzkXUm6LUWIekYQN6vZ3JlP -YA2/M+otdd8Tpws9hFSDMUlx0SP3GAi0cE48xdBkVAT0NjZ3Jjor7Wv6GLe//Kzg -1OVrbPAA/+RrPB+BQn5nmZFT0aLuLpyxB4f4ArHG/8DEBY49Syy7/3Ke0kfHMnhl -5Eg5Yau89wSLqEoUSuQvNixu/5nTTQ6v1VYPVG8D1hn773SbNoY9o5vZOPRl1P0q -9YC/qpzPJkm/A5TZLsoalIxuGTdwts+DaEeoKmECgYEA5CddLQbMNu9kYElxpSA3 -xXoTL71ZBCQsWExmJrcGe2lQhGO40lF8jE6QnEvMt0mp8Dg9n2ih4J87+2Ozb0fp -2G2ilNeMxM7keywA/+Cwg71QyImppU0lQ5PYLv+pllfxN8FPpLBluy7rDahzphkn -1rijqI5d4bHNG6IgD2ynteECgYEAyLs2eBWxX39Jff3OdpSVmHf7NtacbtsUf1qM -RJSvLsiSwKn39n1+Y6ebzftxm/XD/j8FbN8XvMZMI4OrlfzP+YJaTybIbHrLzCE2 -B5E9j0GbJRhJ/D3l9FQBGdY4g5yC4mgbncXURQqqQTtKk2d+ixZSrw8iyDGN+aMJ -ybqZoK0CgYALb6GvARk5Y7R/Uw8cPMou3tiZWv9cQsfqQSIZrLDpfLTpfeokuKrq -iYGcI/yF725SOS91jxQWI0Upa6zx1gP1skEk/szyjIBNYD5IlSWj5NhoxOW5AG3u -vjlm2a/RdmUD62+njKP8xvRHQftSBw7FJ4okh8ZS6suiJ/U9cK/TYQKBgFg+jTyP -dNGhuKJN0NUqjvVfUa4S/ORzJXizStTfdIAhpvpR/nN7SfPvfDw6nQBOM+JyvCTX -kqznlBNM0EL4yElNN/xx9UxTU4Ki2wjKngB7fAP7wJLGd3BI+c7s8R1S0etMj091 -59KOVLimoytYJTZqEuFoywatWlfzh9sKUH1lAoGBAID6mqGL3SZhh+i2/kAytfzw -UswTQqA0CCBTzN/Eo1QozmUVTLQPj8rBchNSoiSc92y+lPIL8ePdU7imRB77i+9D -9MSmc5u3ACACOSkwF0JCEGN+Rju4HR5wwm3h6Kvf/FQ3yvSEOKAWhqXIY95qtYTU -j3O+iJbY32pbQsawIAkw ------END PRIVATE KEY----- \ No newline at end of file diff --git a/modules/identity/.keys/public b/modules/identity/.keys/public deleted file mode 100644 index a228182..0000000 --- a/modules/identity/.keys/public +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsuWaF5Cj/ZmU5LCm0HUs -LKR2asjB99xm8tt/vcnDRzCMMO3Y3FhXz0VuLO7bt0jWmnOqlYPTiON/megT18bo -eJ4UOC1+TaPYlRJnFr2bShOIIcM7KFNJZU+/mipfVGL4KlNWF+M8n4sqNm1tJ1T2 -rBB08oywlTyuQSpavlJrsiU1XHbgCw51RE6VHn7uVXGurSEV7DqL7wH+W85nMT3H -GlyGrUfwHoqcU9GFqCJ5/HcQ95MhYEz17Yp7gWcb6ejFHkrOSBPDiUSUTzxKNTEP -RxcPWmESJ/biJGwEWzWTBs++8wdftE6zxsijgeZS0XlhjFBQCYYy6507GM9fbHuJ -DQIDAQAB ------END PUBLIC KEY----- \ No newline at end of file diff --git a/modules/identity/cerbos/policies/role.yaml b/modules/identity/cerbos/policies/role.yaml index 147908c..72f3dad 100644 --- a/modules/identity/cerbos/policies/role.yaml +++ b/modules/identity/cerbos/policies/role.yaml @@ -11,4 +11,4 @@ resourcePolicy: - actions: ["manage"] effect: EFFECT_ALLOW - roles: ["admin"] + roles: ["super"] diff --git a/modules/identity/config.ts b/modules/identity/config.ts index 50506a1..e6decac 100644 --- a/modules/identity/config.ts +++ b/modules/identity/config.ts @@ -1,4 +1,5 @@ import { getEnvironmentVariable } from "@platform/config/environment.ts"; +import { SerializeOptions } from "cookie"; import z from "zod"; export const config = { @@ -7,4 +8,37 @@ export const config = { type: z.url(), fallback: "http://localhost:8370", }), + internal: { + privateKey: getEnvironmentVariable({ + key: "IDENTITY_PRIVATE_KEY", + type: z.string(), + fallback: + "-----BEGIN PRIVATE KEY-----\n" + + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2WYKMJZUWff5XOWC\n" + + "XGuU+wmsRzhQGEIzfUoL6rrGoaehRANCAATCpiGiFQxTA76EIVG0cBbj+AFt6BuJ\n" + + "t4q+zoInPUzkChCdwI+XfAYokrZwBjcyRGluC02HaN3cptrmjYSGSMSx\n" + + "-----END PRIVATE KEY-----", + }), + publicKey: getEnvironmentVariable({ + key: "IDENTITY_PUBLIC_KEY", + type: z.string(), + fallback: + "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwqYhohUMUwO+hCFRtHAW4/gBbegb\n" + + "ibeKvs6CJz1M5AoQncCPl3wGKJK2cAY3MkRpbgtNh2jd3Kba5o2EhkjEsQ==\n" + + "-----END PUBLIC KEY-----", + }), + }, + cookie: (maxAge: number) => + ({ + httpOnly: true, + secure: getEnvironmentVariable({ + key: "AUTH_COOKIE_SECURE", + type: z.coerce.boolean(), + fallback: "false", + }), // Set to true for HTTPS in production + maxAge, + path: "/", + sameSite: "strict", + }) satisfies SerializeOptions, }; diff --git a/platform/supertoken/principal.ts b/modules/identity/models/principal.ts similarity index 54% rename from platform/supertoken/principal.ts rename to modules/identity/models/principal.ts index 14be140..c0884f2 100644 --- a/platform/supertoken/principal.ts +++ b/modules/identity/models/principal.ts @@ -1,6 +1,18 @@ -import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { makeDocumentParser } from "@platform/database/utilities.ts"; import z from "zod"; +export enum PrincipalTypeId { + User = 1, + Group = 2, + Other = 99, +} + +export const PRINCIPAL_TYPE_NAMES = { + [PrincipalTypeId.User]: "User", + [PrincipalTypeId.Group]: "Group", + [PrincipalTypeId.Other]: "Other", +}; + /* |-------------------------------------------------------------------------------- | Schema @@ -9,33 +21,21 @@ import z from "zod"; export const PrincipalSchema = z.object({ id: z.string(), + type: z.strictObject({ + id: z.enum(PrincipalTypeId), + name: z.string(), + }), roles: z.array(z.string()), attr: z.record(z.string(), z.any()), }); /* |-------------------------------------------------------------------------------- - | Utilities + | Parsers |-------------------------------------------------------------------------------- */ -/** - * Get principal roles from the provided userId. - * - * @param userId - User to get principal roles from. - */ -export async function getPrincipalRoles(userId: string): Promise { - return (await UserMetadata.getUserMetadata(userId)).metadata?.roles ?? []; -} - -/** - * Get principal attributes from the provided userId. - * - * @param userId - User to get principal attributes from. - */ -export async function getPrincipalAttributes(userId: string): Promise> { - return (await UserMetadata.getUserMetadata(userId)).metadata?.attr ?? {}; -} +export const parsePrincipal = makeDocumentParser(PrincipalSchema); /* |-------------------------------------------------------------------------------- diff --git a/modules/identity/models/session.ts b/modules/identity/models/session.ts new file mode 100644 index 0000000..5fdc4f6 --- /dev/null +++ b/modules/identity/models/session.ts @@ -0,0 +1,26 @@ +import z from "zod"; + +/* + |-------------------------------------------------------------------------------- + | Schema + |-------------------------------------------------------------------------------- + */ + +export const SessionSchema = z.object({ + id: z.string(), + userId: z.string(), + token: z.string(), + ipAddress: z.string().nullable().optional(), + userAgent: z.string().nullable().optional(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + expiresAt: z.coerce.date(), +}); + +/* + |-------------------------------------------------------------------------------- + | Types + |-------------------------------------------------------------------------------- + */ + +export type Session = z.infer; diff --git a/modules/identity/package.json b/modules/identity/package.json index 83b28c9..bde3ca5 100644 --- a/modules/identity/package.json +++ b/modules/identity/package.json @@ -8,10 +8,14 @@ "./server.ts": "./server.ts" }, "dependencies": { + "@platform/cerbos": "workspace:*", "@platform/config": "workspace:*", "@platform/logger": "workspace:*", "@platform/relay": "workspace:*", - "supertokens-node": "23.0.1", + "@platform/storage": "workspace:*", + "@platform/vault": "workspace:*", + "better-auth": "1.3.16", + "cookie": "1.0.2", "zod": "4.1.11" } } \ No newline at end of file diff --git a/modules/identity/routes/identities/get/handle.ts b/modules/identity/routes/identities/get/handle.ts index b9b57fc..1d5f3d7 100644 --- a/modules/identity/routes/identities/get/handle.ts +++ b/modules/identity/routes/identities/get/handle.ts @@ -1,21 +1,17 @@ -import { ForbiddenError, NotFoundError } from "@platform/relay"; -import { getPrincipalAttributes, getPrincipalRoles } from "@platform/supertoken/principal.ts"; -import { getUserById } from "@platform/supertoken/users.ts"; - import route from "./spec.ts"; -export default route.access("session").handle(async ({ params: { id } }, { access }) => { - const user = await getUserById(id); - if (user === undefined) { - return new NotFoundError("Identity does not exist, or has been removed."); - } - const decision = await access.isAllowed({ kind: "identity", id: user.id, attr: {} }, "read"); - if (decision === false) { - return new ForbiddenError("You do not have permission to view this identity."); - } - return { - id: user.id, - roles: await getPrincipalRoles(id), - attr: await getPrincipalAttributes(id), - }; +export default route.access("session").handle(async ({ params: { id } }, { session, principal, access }) => { + // const user = await getUserById(id); + // if (user === undefined) { + // return new NotFoundError("Identity does not exist, or has been removed."); + // } + // const decision = await access.isAllowed({ kind: "identity", id: user.id, attr: {} }, "read"); + // if (decision === false) { + // return new ForbiddenError("You do not have permission to view this identity."); + // } + // return { + // id: user.id, + // roles: await getPrincipalRoles(id), + // attr: await getPrincipalAttributes(id), + // }; }); diff --git a/modules/identity/routes/identities/update/handle.ts b/modules/identity/routes/identities/update/handle.ts index 28c9d46..bb21965 100644 --- a/modules/identity/routes/identities/update/handle.ts +++ b/modules/identity/routes/identities/update/handle.ts @@ -1,15 +1,18 @@ -import { ForbiddenError } from "@platform/relay"; -import { getPrincipalAttributes } from "@platform/supertoken/principal.ts"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { ForbiddenError, NotFoundError } from "@platform/relay"; +import { getPrincipalById, setPrincipalAttributesById } from "../../../services/database.ts"; import route from "./spec.ts"; export default route.access("session").handle(async ({ params: { id }, body: ops }, { access }) => { - const decision = await access.isAllowed({ kind: "identity", id, attr: {} }, "update"); + const principal = await getPrincipalById(id); + if (principal === undefined) { + return new NotFoundError(); + } + const decision = await access.isAllowed({ kind: "identity", id: principal.id, attr: principal.attr }, "update"); if (decision === false) { return new ForbiddenError("You do not have permission to update this identity."); } - const attr = await getPrincipalAttributes(id); + const attr = principal.attr; for (const op of ops) { switch (op.type) { case "add": { @@ -36,5 +39,5 @@ export default route.access("session").handle(async ({ params: { id }, body: ops } } } - await UserMetadata.updateUserMetadata(id, { attr }); + await setPrincipalAttributesById(id, attr); }); diff --git a/modules/identity/routes/login/code/handle.ts b/modules/identity/routes/login/code/handle.ts index 7fa1649..a867fd1 100644 --- a/modules/identity/routes/login/code/handle.ts +++ b/modules/identity/routes/login/code/handle.ts @@ -1,25 +1,16 @@ -import { logger } from "@platform/logger"; import { NotFoundError } from "@platform/relay"; -import { getSessionHeaders } from "@platform/supertoken/session.ts"; -import Passwordless from "supertokens-node/recipe/passwordless"; +import { auth } from "../../../services/auth.ts"; +import { logger } from "../../../services/logger.ts"; import route from "./spec.ts"; -export default route.access("public").handle(async ({ body: { preAuthSessionId, deviceId, userInputCode } }) => { - const response = await Passwordless.consumeCode({ tenantId: "public", preAuthSessionId, deviceId, userInputCode }); - if (response.status !== "OK") { +export default route.access("public").handle(async ({ body: { email, otp } }) => { + const response = await auth.api.signInEmailOTP({ body: { email, otp }, asResponse: true, returnHeaders: true }); + if (response.status !== 200) { + logger.error("OTP Signin Failed", await response.json()); return new NotFoundError(); } - - logger.info({ - type: "code:claimed", - session: true, - message: "Identity resolved", - user: response.user.toJson(), - }); - return new Response(null, { - status: 200, - headers: await getSessionHeaders("public", response.recipeUserId), + headers: response.headers, }); }); diff --git a/modules/identity/routes/login/code/spec.ts b/modules/identity/routes/login/code/spec.ts index 78ed314..fdbafc4 100644 --- a/modules/identity/routes/login/code/spec.ts +++ b/modules/identity/routes/login/code/spec.ts @@ -5,9 +5,8 @@ export default route .post("/api/v1/identity/login/code") .body( z.strictObject({ - deviceId: z.string(), - preAuthSessionId: z.string(), - userInputCode: z.string(), + email: z.string(), + otp: z.string(), }), ) .query({ diff --git a/modules/identity/routes/login/email/handle.ts b/modules/identity/routes/login/email/handle.ts index d50ba65..2af0aa3 100644 --- a/modules/identity/routes/login/email/handle.ts +++ b/modules/identity/routes/login/email/handle.ts @@ -1,23 +1,14 @@ -import { logger } from "@platform/logger"; -import Passwordless from "supertokens-node/recipe/passwordless"; - +import { auth } from "../../../services/auth.ts"; +import { logger } from "../../../services/logger.ts"; import route from "./spec.ts"; export default route.access("public").handle(async ({ body: { email } }) => { - const response = await Passwordless.createCode({ tenantId: "public", email }); - if (response.status !== "OK") { - return logger.info({ + const response = await auth.api.sendVerificationOTP({ body: { email, type: "sign-in" } }); + if (response.success === false) { + logger.info({ type: "auth:passwordless", - message: "Create code failed.", + message: "OTP Email verification failed.", received: email, }); } - logger.info({ - type: "auth:passwordless", - data: { - deviceId: response.deviceId, - preAuthSessionId: response.preAuthSessionId, - userInputCode: response.userInputCode, - }, - }); }); diff --git a/modules/identity/routes/login/sudo/handle.ts b/modules/identity/routes/login/sudo/handle.ts index f276087..d1019f2 100644 --- a/modules/identity/routes/login/sudo/handle.ts +++ b/modules/identity/routes/login/sudo/handle.ts @@ -1,48 +1,39 @@ -import { logger } from "@platform/logger"; -import { NotFoundError } from "@platform/relay"; -import { getSessionHeaders } from "@platform/supertoken/session.ts"; -import Passwordless from "supertokens-node/recipe/passwordless"; - import route from "./spec.ts"; export default route.access("public").handle(async ({ body: { email } }) => { - const code = await Passwordless.createCode({ tenantId: "public", email }); - if (code.status !== "OK") { - return logger.info({ - type: "auth:passwordless", - message: "Create code failed.", - received: email, - }); - } - - logger.info({ - type: "auth:passwordless", - data: { - deviceId: code.deviceId, - preAuthSessionId: code.preAuthSessionId, - userInputCode: code.userInputCode, - }, - }); - - const response = await Passwordless.consumeCode({ - tenantId: "public", - preAuthSessionId: code.preAuthSessionId, - deviceId: code.deviceId, - userInputCode: code.userInputCode, - }); - if (response.status !== "OK") { - return new NotFoundError(); - } - - logger.info({ - type: "code:claimed", - session: true, - message: "Identity resolved", - user: response.user.toJson(), - }); - - return new Response(null, { - status: 200, - headers: await getSessionHeaders("public", response.recipeUserId), - }); + // const code = await Passwordless.createCode({ tenantId: "public", email }); + // if (code.status !== "OK") { + // return logger.info({ + // type: "auth:passwordless", + // message: "Create code failed.", + // received: email, + // }); + // } + // logger.info({ + // type: "auth:passwordless", + // data: { + // deviceId: code.deviceId, + // preAuthSessionId: code.preAuthSessionId, + // userInputCode: code.userInputCode, + // }, + // }); + // const response = await Passwordless.consumeCode({ + // tenantId: "public", + // preAuthSessionId: code.preAuthSessionId, + // deviceId: code.deviceId, + // userInputCode: code.userInputCode, + // }); + // if (response.status !== "OK") { + // return new NotFoundError(); + // } + // logger.info({ + // type: "code:claimed", + // session: true, + // message: "Identity resolved", + // user: response.user.toJson(), + // }); + // return new Response(null, { + // status: 200, + // headers: await getSessionHeaders("public", response.recipeUserId), + // }); }); diff --git a/modules/identity/routes/roles/handle.ts b/modules/identity/routes/roles/handle.ts index 000fa28..5505fb7 100644 --- a/modules/identity/routes/roles/handle.ts +++ b/modules/identity/routes/roles/handle.ts @@ -1,15 +1,19 @@ import { ForbiddenError } from "@platform/relay"; -import { getPrincipalRoles } from "@platform/supertoken/principal.ts"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { NotFoundError } from "@platform/relay"; +import { getPrincipalById, setPrincipalRolesById } from "../../services/database.ts"; import route from "./spec.ts"; export default route.access("session").handle(async ({ params: { id }, body: ops }, { access }) => { - const decision = await access.isAllowed({ kind: "role", id, attr: {} }, "manage"); + const principal = await getPrincipalById(id); + if (principal === undefined) { + return new NotFoundError(); + } + const decision = await access.isAllowed({ kind: "role", id: principal.id, attr: principal.attr }, "manage"); if (decision === false) { return new ForbiddenError("You do not have permission to modify roles for this identity."); } - const roles: Set = new Set(await getPrincipalRoles(id)); + const roles: Set = new Set(principal.roles); for (const op of ops) { switch (op.type) { case "add": { @@ -26,5 +30,5 @@ export default route.access("session").handle(async ({ params: { id }, body: ops } } } - await UserMetadata.updateUserMetadata(id, { roles: Array.from(roles) }); + await setPrincipalRolesById(id, Array.from(roles)); }); diff --git a/modules/identity/routes/session/resolve/handle.ts b/modules/identity/routes/session/resolve/handle.ts new file mode 100644 index 0000000..d431062 --- /dev/null +++ b/modules/identity/routes/session/resolve/handle.ts @@ -0,0 +1,17 @@ +import { NotFoundError } from "@platform/relay"; + +import { config } from "../../../config.ts"; +import { getPrincipalByUserId } from "../../../services/database.ts"; +import { getSessionByRequestHeader } from "../../../services/session.ts"; +import route from "./spec.ts"; + +export default route.access(["internal:public", config.internal.privateKey]).handle(async ({ request }) => { + const session = await getSessionByRequestHeader(request.headers); + if (session === undefined) { + return new NotFoundError(); + } + return { + session, + principal: await getPrincipalByUserId(session.userId), + }; +}); diff --git a/modules/identity/routes/session/resolve/spec.ts b/modules/identity/routes/session/resolve/spec.ts new file mode 100644 index 0000000..01a748f --- /dev/null +++ b/modules/identity/routes/session/resolve/spec.ts @@ -0,0 +1,12 @@ +import { route } from "@platform/relay"; +import z from "zod"; + +import { PrincipalSchema } from "../../../models/principal.ts"; +import { SessionSchema } from "../../../models/session.ts"; + +export default route.get("/api/v1/identity/session").response( + z.object({ + session: SessionSchema, + principal: PrincipalSchema, + }), +); diff --git a/modules/identity/server.ts b/modules/identity/server.ts index da1e43e..1572784 100644 --- a/modules/identity/server.ts +++ b/modules/identity/server.ts @@ -1,3 +1,43 @@ +import { HttpAdapter, makeClient } from "@platform/relay"; + +import { config } from "./config.ts"; +import resolve from "./routes/session/resolve/spec.ts"; + +/* + |-------------------------------------------------------------------------------- + | Internal Session Resolver + |-------------------------------------------------------------------------------- + */ + +export const identity = makeClient( + { + adapter: new HttpAdapter({ + url: config.url, + }), + }, + { + resolve: resolve.crypto({ + publicKey: config.internal.publicKey, + }), + }, +); + +/* + |-------------------------------------------------------------------------------- + | Server Exports + |-------------------------------------------------------------------------------- + */ + +export * from "./services/access.ts"; +export * from "./services/session.ts"; +export * from "./types.ts"; + +/* + |-------------------------------------------------------------------------------- + | Module Server + |-------------------------------------------------------------------------------- + */ + export default { routes: [ (await import("./routes/identities/get/handle.ts")).default, @@ -8,5 +48,6 @@ export default { (await import("./routes/login/sudo/handle.ts")).default, (await import("./routes/me/handle.ts")).default, (await import("./routes/roles/handle.ts")).default, + (await import("./routes/session/resolve/handle.ts")).default, ], }; diff --git a/platform/supertoken/access.ts b/modules/identity/services/access.ts similarity index 95% rename from platform/supertoken/access.ts rename to modules/identity/services/access.ts index 7323142..34307b3 100644 --- a/platform/supertoken/access.ts +++ b/modules/identity/services/access.ts @@ -1,5 +1,6 @@ -import { cerbos } from "./cerbos.ts"; -import type { Principal } from "./principal.ts"; +import { cerbos } from "@platform/cerbos"; + +import { Principal } from "../models/principal.ts"; export function getAccessControlMethods(principal: Principal) { return { diff --git a/modules/identity/services/auth.ts b/modules/identity/services/auth.ts new file mode 100644 index 0000000..4f243c0 --- /dev/null +++ b/modules/identity/services/auth.ts @@ -0,0 +1,29 @@ +import { logger } from "@platform/logger"; +import { betterAuth } from "better-auth"; +import { mongodbAdapter } from "better-auth/adapters/mongodb"; +import { emailOTP } from "better-auth/plugins"; + +import { db } from "./database.ts"; + +export const auth = betterAuth({ + database: mongodbAdapter(db.db), + session: { + cookieCache: { + enabled: true, + maxAge: 5 * 60, // Cache duration in seconds + }, + }, + plugins: [ + emailOTP({ + async sendVerificationOTP({ email, otp, type }) { + if (type === "sign-in") { + logger.info({ email, otp, type }); + } else if (type === "email-verification") { + // Send the OTP for email verification + } else { + // Send the OTP for password reset + } + }, + }), + ], +}); diff --git a/modules/identity/services/database.ts b/modules/identity/services/database.ts new file mode 100644 index 0000000..679ffcf --- /dev/null +++ b/modules/identity/services/database.ts @@ -0,0 +1,61 @@ +import { getDatabaseAccessor } from "@platform/database/accessor.ts"; + +import { + parsePrincipal, + type Principal, + PRINCIPAL_TYPE_NAMES, + PrincipalSchema, + PrincipalTypeId, +} from "../models/principal.ts"; + +export const db = getDatabaseAccessor<{ + principal: Principal; +}>("auth"); + +/* + |-------------------------------------------------------------------------------- + | Methods + |-------------------------------------------------------------------------------- + */ + +export async function getPrincipalById(id: string): Promise { + return db + .collection("principal") + .findOne({ id }) + .then((value) => parsePrincipal(value)); +} + +export async function setPrincipalRolesById(id: string, roles: string[]): Promise { + await db.collection("principal").updateOne({ id }, { $set: { roles } }); +} + +export async function setPrincipalAttributesById(id: string, attr: Record): Promise { + await db.collection("principal").updateOne({ id }, { $set: { attr } }); +} + +/** + * Retrieve a principal for a better-auth user. + * + * @param userId - User id from better-auth user list. + */ +export async function getPrincipalByUserId(userId: string): Promise { + const principal = await db.collection("principal").findOneAndUpdate( + { id: userId }, + { + $setOnInsert: { + id: userId, + type: { + id: PrincipalTypeId.User, + name: PRINCIPAL_TYPE_NAMES[PrincipalTypeId.User], + }, + roles: ["user"], + attr: {}, + }, + }, + { upsert: true, returnDocument: "after" }, + ); + if (principal === null) { + throw new Error("Failed to resolve Principal"); + } + return PrincipalSchema.parse(principal); +} diff --git a/modules/identity/services/logger.ts b/modules/identity/services/logger.ts new file mode 100644 index 0000000..965cd3d --- /dev/null +++ b/modules/identity/services/logger.ts @@ -0,0 +1,3 @@ +import { logger as platformLogger } from "@platform/logger"; + +export const logger = platformLogger.prefix("Modules/Identity"); diff --git a/modules/identity/services/session.ts b/modules/identity/services/session.ts new file mode 100644 index 0000000..7a1e806 --- /dev/null +++ b/modules/identity/services/session.ts @@ -0,0 +1,34 @@ +import cookie from "cookie"; + +import { config } from "../config.ts"; +import { auth } from "./auth.ts"; + +/** + * Get session headers which can be applied on a Response object to apply + * an authenticated session to the respondent. + * + * @param accessToken - Token to apply to the cookie. + * @param maxAge - Max age of the token. + */ +export async function getSessionHeaders(accessToken: string, maxAge: number): Promise { + return new Headers({ + "set-cookie": cookie.serialize( + "better-auth.session_token", + encodeURIComponent(accessToken), // URL-encode the token + config.cookie(maxAge), + ), + }); +} + +/** + * Get session container from request headers. + * + * @param headers - Request headers to extract session from. + */ +export async function getSessionByRequestHeader(headers: Headers) { + const response = await auth.api.getSession({ headers }); + if (response === null) { + return undefined; + } + return response.session; +} diff --git a/platform/supertoken/types.ts b/modules/identity/types.ts similarity index 83% rename from platform/supertoken/types.ts rename to modules/identity/types.ts index 51ee3f5..3bfcc06 100644 --- a/platform/supertoken/types.ts +++ b/modules/identity/types.ts @@ -1,7 +1,7 @@ import "@platform/relay"; import "@platform/storage"; -import type Session from "supertokens-node/recipe/session"; +import type { Session } from "better-auth"; import type { AccessControlMethods } from "./access.ts"; import type { Principal } from "./principal.ts"; @@ -11,7 +11,7 @@ declare module "@platform/storage" { /** * TODO ... */ - session?: Session.SessionContainer; + session?: Session; /** * TODO ... @@ -35,7 +35,7 @@ declare module "@platform/relay" { /** * TODO ... */ - session: Session.SessionContainer; + session: Session; /** * TODO ... diff --git a/modules/workspace/cerbos/policies/workspace.yaml b/modules/workspace/cerbos/policies/workspace.yaml index 1b6ed00..afcbc87 100644 --- a/modules/workspace/cerbos/policies/workspace.yaml +++ b/modules/workspace/cerbos/policies/workspace.yaml @@ -16,18 +16,18 @@ resourcePolicy: roles: ["super", "admin", "user"] condition: match: - expr: R.attr.id in P.attr.workspaceIds + expr: R.attr.workspaceId in P.attr.workspaceIds - actions: ["update"] effect: EFFECT_ALLOW roles: ["super", "admin"] condition: match: - expr: R.attr.id in P.attr.workspaceIds + expr: R.attr.workspaceId in P.attr.workspaceIds - actions: ["delete"] effect: EFFECT_ALLOW roles: ["super"] condition: match: - expr: R.attr.id in P.attr.workspaceIds + expr: R.attr.workspaceId in P.attr.workspaceIds diff --git a/modules/workspace/event-store.ts b/modules/workspace/event-store.ts index bb433ef..5e14f3c 100644 --- a/modules/workspace/event-store.ts +++ b/modules/workspace/event-store.ts @@ -1,4 +1,4 @@ -import { container } from "@platform/database/container.ts"; +import { mongo } from "@platform/database/client.ts"; import { EventFactory, EventStore, Prettify, Projector } from "@valkyr/event-store"; import { MongoAdapter } from "@valkyr/event-store/mongo"; @@ -20,7 +20,7 @@ const eventFactory = new EventFactory([ */ export const eventStore = new EventStore({ - adapter: new MongoAdapter(() => container.get("mongo"), `workspace:event-store`), + adapter: new MongoAdapter(() => mongo, `workspace:event-store`), events: eventFactory, snapshot: "auto", }); diff --git a/platform/supertoken/cerbos.ts b/platform/cerbos/cerbos.ts similarity index 100% rename from platform/supertoken/cerbos.ts rename to platform/cerbos/cerbos.ts diff --git a/platform/cerbos/mod.ts b/platform/cerbos/mod.ts new file mode 100644 index 0000000..79b27de --- /dev/null +++ b/platform/cerbos/mod.ts @@ -0,0 +1 @@ +export * from "./cerbos.ts"; diff --git a/platform/supertoken/package.json b/platform/cerbos/package.json similarity index 51% rename from platform/supertoken/package.json rename to platform/cerbos/package.json index e06c480..7f63369 100644 --- a/platform/supertoken/package.json +++ b/platform/cerbos/package.json @@ -1,13 +1,15 @@ { - "name": "@platform/supertoken", + "name": "@platform/auth", "version": "0.0.0", "private": true, "type": "module", + "main": "./mod.ts", + "exports": { + ".": "./mod.ts" + }, "dependencies": { "@cerbos/http": "0.23.1", - "@platform/config": "workspace:*", - "cookie": "1.0.2", - "supertokens-node": "23.0.1", + "@platform/logger": "workspace:*", "zod": "4.1.11" } } \ No newline at end of file diff --git a/platform/database/accessor.ts b/platform/database/accessor.ts index fe1eb4e..f45b29b 100644 --- a/platform/database/accessor.ts +++ b/platform/database/accessor.ts @@ -1,6 +1,6 @@ import { Collection, type CollectionOptions, type Db, type Document, type MongoClient } from "mongodb"; -import { container } from "./container.ts"; +import { mongo } from "./client.ts"; export function getDatabaseAccessor>( database: string, @@ -14,7 +14,7 @@ export function getDatabaseAccessor>( return instance; }, get client(): MongoClient { - return container.get("mongo"); + return mongo; }, collection( name: TSchema, diff --git a/platform/database/client.ts b/platform/database/client.ts new file mode 100644 index 0000000..d8683f4 --- /dev/null +++ b/platform/database/client.ts @@ -0,0 +1,4 @@ +import { config } from "./config.ts"; +import { getMongoClient } from "./connection.ts"; + +export const mongo = getMongoClient(config.mongo); diff --git a/platform/database/container.ts b/platform/database/container.ts deleted file mode 100644 index 6fa9354..0000000 --- a/platform/database/container.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Container } from "@valkyr/inverse"; -import { MongoClient } from "mongodb"; - -export const container = new Container<{ - mongo: MongoClient; -}>("@platform/database"); diff --git a/platform/database/server.ts b/platform/database/server.ts deleted file mode 100644 index d6cbdb5..0000000 --- a/platform/database/server.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { config } from "./config.ts"; -import { getMongoClient } from "./connection.ts"; -import { container } from "./container.ts"; - -export default { - bootstrap: async (): Promise => { - container.set("mongo", getMongoClient(config.mongo)); - }, -}; diff --git a/platform/relay/libraries/context.ts b/platform/relay/libraries/context.ts index 1129f36..616527a 100644 --- a/platform/relay/libraries/context.ts +++ b/platform/relay/libraries/context.ts @@ -1,5 +1,3 @@ -import "@platform/supertoken/types.ts"; - // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface ServerContext {} diff --git a/platform/server/server.ts b/platform/server/server.ts index f9a44e8..3442045 100644 --- a/platform/server/server.ts +++ b/platform/server/server.ts @@ -1,4 +1,4 @@ -import "./types.d.ts"; +import "./types.ts"; import { context } from "@platform/relay"; import { InternalServerError } from "@platform/relay"; diff --git a/platform/server/types.d.ts b/platform/server/types.ts similarity index 95% rename from platform/server/types.d.ts rename to platform/server/types.ts index 218db8d..1fe0d91 100644 --- a/platform/server/types.d.ts +++ b/platform/server/types.ts @@ -30,7 +30,6 @@ declare module "@platform/storage" { declare module "@platform/relay" { interface ServerContext { - isAuthenticated: boolean; request: { headers: Headers; }; diff --git a/platform/supertoken/config.ts b/platform/supertoken/config.ts deleted file mode 100644 index 7d9a0d1..0000000 --- a/platform/supertoken/config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { getEnvironmentVariable } from "@platform/config/environment.ts"; -import type { SerializeOptions } from "cookie"; -import z from "zod"; - -export const config = { - supertokens: { - connectionURI: getEnvironmentVariable({ - key: "SUPERTOKEN_URI", - type: z.string(), - fallback: "http://localhost:3567", - }), - }, - appInfo: { - appName: getEnvironmentVariable({ - key: "PROJECT_NAME", - type: z.string(), - fallback: "Boilerplate", - }), - apiDomain: getEnvironmentVariable({ - key: "API_DOMAIN", - type: z.string(), - fallback: "http://localhost:8370", - }), - websiteDomain: getEnvironmentVariable({ - key: "APP_DOMAIN", - type: z.string(), - fallback: "http://localhost:3000", - }), - apiBasePath: "/api/v1/identity", - websiteBasePath: "/auth", - }, - cookie: (maxAge: number) => - ({ - httpOnly: true, - secure: getEnvironmentVariable({ - key: "AUTH_COOKIE_SECURE", - type: z.coerce.boolean(), - fallback: "false", - }), // Set to true for HTTPS in production - maxAge, - path: "/", - sameSite: "strict", - }) satisfies SerializeOptions, -}; diff --git a/platform/supertoken/server.ts b/platform/supertoken/server.ts deleted file mode 100644 index 38d04c8..0000000 --- a/platform/supertoken/server.ts +++ /dev/null @@ -1,133 +0,0 @@ -import "./types.ts"; - -import { UnauthorizedError } from "@platform/relay"; -import { context } from "@platform/relay"; -import { storage } from "@platform/storage"; -import cookie from "cookie"; -import supertokens from "supertokens-node"; -import Passwordless from "supertokens-node/recipe/passwordless"; -import Session from "supertokens-node/recipe/session"; - -import { getAccessControlMethods } from "./access.ts"; -import { config } from "./config.ts"; -import { getPrincipalAttributes, getPrincipalRoles, Principal } from "./principal.ts"; -import { getSessionByAccessToken } from "./session.ts"; - -/* - |-------------------------------------------------------------------------------- - | Server Module - |-------------------------------------------------------------------------------- - */ - -export default { - bootsrap: async () => { - bootstrapSuperTokens(); - bootstrapStorageContext(); - }, - resolve: async (request: Request): Promise => { - await resolveSession(request); - }, -}; - -/* - |-------------------------------------------------------------------------------- - | Bootstrap Methods - |-------------------------------------------------------------------------------- - */ - -function bootstrapSuperTokens() { - supertokens.init({ - framework: "custom", - supertokens: config.supertokens, - appInfo: config.appInfo, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "EMAIL", - }), - Session.init({ - getTokenTransferMethod: () => "cookie", - }), - ], - }); -} - -function bootstrapStorageContext() { - Object.defineProperties(context, { - /** - * TODO ... - */ - isAuthenticated: { - get() { - return storage.getStore()?.principal !== undefined; - }, - }, - - /** - * TODO ... - */ - session: { - get() { - const session = storage.getStore()?.session; - if (session === undefined) { - throw new UnauthorizedError(); - } - return session; - }, - }, - - /** - * TODO ... - */ - principal: { - get() { - const principal = storage.getStore()?.principal; - if (principal === undefined) { - throw new UnauthorizedError(); - } - return principal; - }, - }, - - /** - * TODO ... - */ - access: { - get() { - const access = storage.getStore()?.access; - if (access === undefined) { - throw new UnauthorizedError(); - } - return access; - }, - }, - }); -} - -/* - |-------------------------------------------------------------------------------- - | Request Middleware - |-------------------------------------------------------------------------------- - */ - -async function resolveSession(request: Request): Promise { - const accessToken = cookie.parse(request.headers.get("cookie") ?? "").sAccessToken; - if (accessToken !== undefined) { - const session = await getSessionByAccessToken(accessToken); - - const store = storage.getStore(); - if (store === undefined) { - return; - } - - const principal: Principal = { - id: session.getUserId(), - roles: await getPrincipalRoles(session.getUserId()), - attr: await getPrincipalAttributes(session.getUserId()), - }; - - store.session = session; - store.principal = principal; - store.access = getAccessControlMethods(principal); - } -} diff --git a/platform/supertoken/session.ts b/platform/supertoken/session.ts deleted file mode 100644 index 306c488..0000000 --- a/platform/supertoken/session.ts +++ /dev/null @@ -1,37 +0,0 @@ -import cookie from "cookie"; -import type { RecipeUserId } from "supertokens-node/index.js"; -import Session from "supertokens-node/recipe/session"; - -import { config } from "./config.ts"; - -/** - * Get session headers which can be applied on a Response object to apply - * an authenticted session to the respondant. - * - * @param tenantId - Tenant scope the session belongs to. - * @param recipeUserId - User recipe to apply to the session. - */ -export async function getSessionHeaders(tenantId: string, recipeUserId: RecipeUserId): Promise { - const session = await Session.createNewSessionWithoutRequestResponse(tenantId, recipeUserId); - const tokens = session.getAllSessionTokensDangerously(); - const options = config.cookie(await session.getExpiry()); - - const headers = new Headers({ "set-cookie": cookie.serialize("sAccessToken", tokens.accessToken, options) }); - if (tokens.refreshToken !== undefined) { - headers.append("set-cookie", cookie.serialize("sRefreshToken", tokens.refreshToken, options)); - } - return headers; -} - -/** - * Get session container from access token. - * - * @param accessToken - Access token to resolve session from. - * @param antiCsrfToken - Optional CSRF token. - */ -export async function getSessionByAccessToken( - accessToken: string, - antiCsrfToken?: string, -): Promise { - return Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken); -} diff --git a/platform/supertoken/users.ts b/platform/supertoken/users.ts deleted file mode 100644 index 2f8251e..0000000 --- a/platform/supertoken/users.ts +++ /dev/null @@ -1,10 +0,0 @@ -import supertokens, { type User } from "supertokens-node"; - -/** - * Get a user by provided user id. - * - * @param userId - User id to retrieve. - */ -export async function getUserById(userId: string): Promise { - return supertokens.getUser(userId); -}