Template
1
0

feat: encapsulate identity with better-auth

This commit is contained in:
2025-09-25 13:24:32 +02:00
parent 99111b69eb
commit f2ba21a7e3
48 changed files with 718 additions and 766 deletions

View File

@@ -12,9 +12,8 @@ post {
body:json {
{
"deviceId": "",
"preAuthSessionId": "",
"userInputCode": ""
"email": "john.doe@fixture.none",
"otp": ""
}
}

View File

@@ -12,7 +12,6 @@ post {
body:json {
{
"base": "http://localhost:5170",
"email": "john.doe@fixture.none"
}
}

View File

@@ -12,7 +12,7 @@ post {
body:json {
{
"name": "valkyr"
"name": ""
}
}

View File

@@ -4,6 +4,8 @@
"start": "deno --allow-all --watch-hmr=routes/ server.ts"
},
"dependencies": {
"@modules/identity": "workspace:*",
"@module/workspace": "workspace:*",
"zod": "4.1.11"
}
}

View File

@@ -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.

114
api/services/session.ts Normal file
View File

@@ -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);
}
}

View File

@@ -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": {

492
deno.lock generated
View File

@@ -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": [

View File

@@ -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.

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -11,4 +11,4 @@ resourcePolicy:
- actions: ["manage"]
effect: EFFECT_ALLOW
roles: ["admin"]
roles: ["super"]

View File

@@ -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,
};

View File

@@ -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<string[]> {
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<Record<string, any>> {
return (await UserMetadata.getUserMetadata(userId)).metadata?.attr ?? {};
}
export const parsePrincipal = makeDocumentParser(PrincipalSchema);
/*
|--------------------------------------------------------------------------------

View File

@@ -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<typeof SessionSchema>;

View File

@@ -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"
}
}

View File

@@ -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),
// };
});

View File

@@ -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);
});

View File

@@ -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,
});
});

View File

@@ -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({

View File

@@ -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,
},
});
});

View File

@@ -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),
// });
});

View File

@@ -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<string> = new Set(await getPrincipalRoles(id));
const roles: Set<string> = 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));
});

View File

@@ -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),
};
});

View File

@@ -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,
}),
);

View File

@@ -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,
],
};

View File

@@ -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 {

View File

@@ -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
}
},
}),
],
});

View File

@@ -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<Principal | undefined> {
return db
.collection("principal")
.findOne({ id })
.then((value) => parsePrincipal(value));
}
export async function setPrincipalRolesById(id: string, roles: string[]): Promise<void> {
await db.collection("principal").updateOne({ id }, { $set: { roles } });
}
export async function setPrincipalAttributesById(id: string, attr: Record<string, any>): Promise<void> {
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<Principal> {
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);
}

View File

@@ -0,0 +1,3 @@
import { logger as platformLogger } from "@platform/logger";
export const logger = platformLogger.prefix("Modules/Identity");

View File

@@ -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<Headers> {
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;
}

View File

@@ -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 ...

View File

@@ -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

View File

@@ -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",
});

1
platform/cerbos/mod.ts Normal file
View File

@@ -0,0 +1 @@
export * from "./cerbos.ts";

View File

@@ -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"
}
}

View File

@@ -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<TSchemas extends Record<string, Document>>(
database: string,
@@ -14,7 +14,7 @@ export function getDatabaseAccessor<TSchemas extends Record<string, Document>>(
return instance;
},
get client(): MongoClient {
return container.get("mongo");
return mongo;
},
collection<TSchema extends keyof TSchemas>(
name: TSchema,

View File

@@ -0,0 +1,4 @@
import { config } from "./config.ts";
import { getMongoClient } from "./connection.ts";
export const mongo = getMongoClient(config.mongo);

View File

@@ -1,6 +0,0 @@
import { Container } from "@valkyr/inverse";
import { MongoClient } from "mongodb";
export const container = new Container<{
mongo: MongoClient;
}>("@platform/database");

View File

@@ -1,9 +0,0 @@
import { config } from "./config.ts";
import { getMongoClient } from "./connection.ts";
import { container } from "./container.ts";
export default {
bootstrap: async (): Promise<void> => {
container.set("mongo", getMongoClient(config.mongo));
},
};

View File

@@ -1,5 +1,3 @@
import "@platform/supertoken/types.ts";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ServerContext {}

View File

@@ -1,4 +1,4 @@
import "./types.d.ts";
import "./types.ts";
import { context } from "@platform/relay";
import { InternalServerError } from "@platform/relay";

View File

@@ -30,7 +30,6 @@ declare module "@platform/storage" {
declare module "@platform/relay" {
interface ServerContext {
isAuthenticated: boolean;
request: {
headers: Headers;
};

View File

@@ -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,
};

View File

@@ -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<void> => {
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<void> {
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);
}
}

View File

@@ -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<Headers> {
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<Session.SessionContainer> {
return Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken);
}

View File

@@ -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<User | undefined> {
return supertokens.getUser(userId);
}