Template
1
0

feat: add supertokens

This commit is contained in:
2025-09-24 01:20:09 +02:00
parent 0d70749670
commit 99111b69eb
92 changed files with 1613 additions and 1141 deletions

View File

@@ -1,17 +1,17 @@
meta { meta {
name: Get By ID name: Get
type: http type: http
seq: 2 seq: 2
} }
get { get {
url: {{url}}/identities/:id url: {{url}}/identity/:id
body: none body: none
auth: inherit auth: inherit
} }
params:path { params:path {
id: 16b88034-ca82-4a8e-9fe5-13bd0dd29b75 id:
} }
settings { settings {

32
.bruno/identity/Roles.bru Normal file
View File

@@ -0,0 +1,32 @@
meta {
name: Roles
type: http
seq: 4
}
put {
url: {{url}}/identity/:id/roles
body: json
auth: inherit
}
params:path {
id:
}
body:json {
[
{
"type": "add",
"roles": []
},
{
"type": "remove",
"roles": []
}
]
}
settings {
encodeUrl: true
}

View File

@@ -0,0 +1,43 @@
meta {
name: Update
type: http
seq: 4
}
put {
url: {{url}}/identity/:id
body: json
auth: inherit
}
params:path {
id:
}
body:json {
[
{
"type": "add",
"key": "",
"value": ""
},
{
"type": "push",
"key": "",
"values": ""
},
{
"type": "pop",
"key": "",
"values": ""
},
{
"type": "remove",
"key": ""
}
]
}
settings {
encodeUrl: true
}

View File

@@ -1,19 +1,21 @@
meta { meta {
name: Code name: Code
type: http type: http
seq: 2 seq: 3
} }
get { post {
url: {{url}}/identities/login/code/:identityId/code/:codeId/:value url: {{url}}/identity/login/code
body: none body: json
auth: inherit auth: inherit
} }
params:path { body:json {
identityId: efefa471-905d-4702-bd0a-863d8cf70424 {
codeId: 7055b769-0814-47b8-836e-cfef2d8c2e68 "deviceId": "",
value: 00597 "preAuthSessionId": "",
"userInputCode": ""
}
} }
script:post-response { script:post-response {

View File

@@ -1,11 +1,11 @@
meta { meta {
name: Email name: Email
type: http type: http
seq: 1 seq: 2
} }
post { post {
url: {{url}}/identities/login/email url: {{url}}/identity/login/email
body: json body: json
auth: inherit auth: inherit
} }

View File

@@ -1,6 +1,6 @@
meta { meta {
name: Login name: Login
seq: 2 seq: 3
} }
auth { auth {

View File

@@ -0,0 +1,21 @@
meta {
name: Sudo
type: http
seq: 1
}
post {
url: {{url}}/identities/login/sudo
body: json
auth: inherit
}
body:json {
{
"email": "john.doe@fixture.none"
}
}
settings {
encodeUrl: true
}

View File

@@ -1,11 +1,11 @@
meta { meta {
name: Me name: Me
type: http type: http
seq: 3 seq: 1
} }
get { get {
url: {{url}}/identities/me url: {{url}}/identity/me
body: none body: none
auth: inherit auth: inherit
} }

View File

@@ -1,25 +0,0 @@
meta {
name: Register
type: http
seq: 1
}
post {
url: {{url}}/identities
body: json
auth: inherit
}
body:json {
{
"name": {
"given": "Jane",
"family": "Doe"
},
"email": "jane.doe@fixture.none"
}
}
settings {
encodeUrl: true
}

View File

@@ -0,0 +1,21 @@
meta {
name: Create
type: http
seq: 1
}
post {
url: {{url}}/workspace
body: json
auth: inherit
}
body:json {
{
"name": "valkyr"
}
}
settings {
encodeUrl: true
}

View File

@@ -0,0 +1,8 @@
meta {
name: Workspace
seq: 2
}
auth {
mode: inherit
}

View File

@@ -27,6 +27,4 @@
"**/.DS_Store": true, "**/.DS_Store": true,
"**/Thumbs.db": true "**/Thumbs.db": true
}, },
"vue.format.style.initialIndent": true,
"vue.format.script.initialIndent": true
} }

View File

@@ -1,4 +1,5 @@
import identity from "@modules/identity/server.ts"; import identity from "@modules/identity/server.ts";
import workspace from "@modules/workspace/server.ts";
import database from "@platform/database/server.ts"; import database from "@platform/database/server.ts";
import { logger } from "@platform/logger"; import { logger } from "@platform/logger";
import { context } from "@platform/relay"; import { context } from "@platform/relay";
@@ -6,6 +7,7 @@ import { Api } from "@platform/server/api.ts";
import server from "@platform/server/server.ts"; import server from "@platform/server/server.ts";
import socket from "@platform/socket/server.ts"; import socket from "@platform/socket/server.ts";
import { storage } from "@platform/storage"; import { storage } from "@platform/storage";
import supertokens from "@platform/supertoken/server.ts";
import { config } from "./config.ts"; import { config } from "./config.ts";
@@ -22,10 +24,11 @@ const log = logger.prefix("Server");
await database.bootstrap(); await database.bootstrap();
await server.bootstrap(); await server.bootstrap();
await socket.bootstrap(); await socket.bootstrap();
await supertokens.bootsrap();
// ### Modules // ### Modules
await identity.bootstrap(); await workspace.bootstrap();
/* /*
|-------------------------------------------------------------------------------- |--------------------------------------------------------------------------------
@@ -33,7 +36,7 @@ await identity.bootstrap();
|-------------------------------------------------------------------------------- |--------------------------------------------------------------------------------
*/ */
const api = new Api([...identity.routes]); const api = new Api([...identity.routes, ...workspace.routes]);
/* /*
|-------------------------------------------------------------------------------- |--------------------------------------------------------------------------------
@@ -58,8 +61,7 @@ Deno.serve(
await server.resolve(request); await server.resolve(request);
await socket.resolve(); await socket.resolve();
await supertokens.resolve(request);
await identity.resolve(request);
// ### Fetch // ### Fetch
// Execute fetch against the api instance. // Execute fetch against the api instance.

View File

@@ -5,7 +5,7 @@
"api", "api",
"apps/react", "apps/react",
"modules/identity", "modules/identity",
"platform/cerbos", "modules/workspace",
"platform/config", "platform/config",
"platform/database", "platform/database",
"platform/logger", "platform/logger",
@@ -14,12 +14,14 @@
"platform/socket", "platform/socket",
"platform/spec", "platform/spec",
"platform/storage", "platform/storage",
"platform/supertoken",
"platform/vault" "platform/vault"
], ],
"imports": { "imports": {
"@modules/identity/client.ts": "./modules/identity/client.ts", "@modules/identity/client.ts": "./modules/identity/client.ts",
"@modules/identity/server.ts": "./modules/identity/server.ts", "@modules/identity/server.ts": "./modules/identity/server.ts",
"@platform/cerbos/": "./platform/cerbos/", "@modules/workspace/client.ts": "./modules/workspace/client.ts",
"@modules/workspace/server.ts": "./modules/workspace/server.ts",
"@platform/config/": "./platform/config/", "@platform/config/": "./platform/config/",
"@platform/database/": "./platform/database/", "@platform/database/": "./platform/database/",
"@platform/logger": "./platform/logger/mod.ts", "@platform/logger": "./platform/logger/mod.ts",
@@ -28,6 +30,7 @@
"@platform/socket/": "./platform/socket/", "@platform/socket/": "./platform/socket/",
"@platform/spec/": "./platform/spec/", "@platform/spec/": "./platform/spec/",
"@platform/storage": "./platform/storage/storage.ts", "@platform/storage": "./platform/storage/storage.ts",
"@platform/supertoken/": "./platform/supertoken/",
"@platform/vault": "./platform/vault/vault.ts" "@platform/vault": "./platform/vault/vault.ts"
}, },
"tasks": { "tasks": {

411
deno.lock generated
View File

@@ -3,24 +3,21 @@
"specifiers": { "specifiers": {
"npm:@cerbos/http@0.23.1": "0.23.1", "npm:@cerbos/http@0.23.1": "0.23.1",
"npm:@eslint/js@9.35.0": "9.35.0", "npm:@eslint/js@9.35.0": "9.35.0",
"npm:@jsr/felix__bcrypt@1.0.5": "1.0.5",
"npm:@jsr/std__assert@1.0.14": "1.0.14", "npm:@jsr/std__assert@1.0.14": "1.0.14",
"npm:@jsr/std__dotenv@0.225.5": "0.225.5", "npm:@jsr/std__dotenv@0.225.5": "0.225.5",
"npm:@jsr/std__testing@1.0.15": "1.0.15", "npm:@jsr/std__testing@1.0.15": "1.0.15",
"npm:@jsr/valkyr__auth@2.1.4": "2.1.4",
"npm:@jsr/valkyr__db@2.0.0": "2.0.0", "npm:@jsr/valkyr__db@2.0.0": "2.0.0",
"npm:@jsr/valkyr__event-emitter@1.0.1": "1.0.1", "npm:@jsr/valkyr__event-emitter@1.0.1": "1.0.1",
"npm:@jsr/valkyr__event-store@2.0.1": "2.0.1", "npm:@jsr/valkyr__event-store@2.0.1": "2.0.1",
"npm:@jsr/valkyr__inverse@1.0.1": "1.0.1", "npm:@jsr/valkyr__inverse@1.0.1": "1.0.1",
"npm:@jsr/valkyr__json-rpc@1.1.0": "1.1.0", "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_@types+node@24.2.0", "npm:@tailwindcss/vite@4.1.13": "4.1.13_vite@7.1.6__picomatch@4.0.3",
"npm:@tanstack/react-query@5.89.0": "5.89.0_react@19.1.1", "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-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:@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-dom@19.1.9": "19.1.9_@types+react@19.1.13",
"npm:@types/react@19.1.13": "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_@types+node@24.2.0", "npm:@vitejs/plugin-react@4.7.0": "4.7.0_vite@7.1.6__picomatch@4.0.3_@babel+core@7.28.4",
"npm:cookie@1.0.2": "1.0.2", "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-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", "npm:eslint-plugin-react-refresh@0.4.20": "0.4.20_eslint@9.35.0",
@@ -35,10 +32,11 @@
"npm:prettier@3.6.2": "3.6.2", "npm:prettier@3.6.2": "3.6.2",
"npm:react-dom@19.1.1": "19.1.1_react@19.1.1", "npm:react-dom@19.1.1": "19.1.1_react@19.1.1",
"npm:react@19.1.1": "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: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-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:typescript@5.9.2": "5.9.2",
"npm:vite@7.1.6": "7.1.6_picomatch@4.0.3_@types+node@24.2.0", "npm:vite@7.1.6": "7.1.6_picomatch@4.0.3",
"npm:zod@4.1.11": "4.1.11" "npm:zod@4.1.11": "4.1.11"
}, },
"npm": { "npm": {
@@ -425,23 +423,6 @@
"@jridgewell/sourcemap-codec" "@jridgewell/sourcemap-codec"
] ]
}, },
"@jsr/denosaurs__plug@1.1.0": {
"integrity": "sha512-GNRMr8XcYWbv8C1B5OjDa5u8q3p2lz7YVWQLhH5HAy0pkpb0+Y3npSxzjM49v5ajTFIzUCwIKv1gQukPm9q7qw==",
"dependencies": [
"@jsr/std__encoding",
"@jsr/std__fmt",
"@jsr/std__fs",
"@jsr/std__path"
],
"tarball": "https://npm.jsr.io/~/11/@jsr/denosaurs__plug/1.1.0.tgz"
},
"@jsr/felix__bcrypt@1.0.5": {
"integrity": "sha512-XJAQ+NIs23r5YNUgFMtMbl6lhzn/Ms2x1fDO5qJdcVwHKcTFebc5ZH/EQlJss/YfVYdYC6Ng6QQQLzjhrLD/aw==",
"dependencies": [
"@jsr/denosaurs__plug"
],
"tarball": "https://npm.jsr.io/~/11/@jsr/felix__bcrypt/1.0.5.tgz"
},
"@jsr/std__assert@1.0.14": { "@jsr/std__assert@1.0.14": {
"integrity": "sha512-BcjBimpxuy7mXjWo7sZ3TtPitx91w3UqssyY92RmJIuoMGYywZRGxaxqK9/oybljbZbZpPOSrkgQI9wKpgZ9vQ==", "integrity": "sha512-BcjBimpxuy7mXjWo7sZ3TtPitx91w3UqssyY92RmJIuoMGYywZRGxaxqK9/oybljbZbZpPOSrkgQI9wKpgZ9vQ==",
"dependencies": [ "dependencies": [
@@ -464,14 +445,6 @@
"integrity": "sha512-qrBt3wfQgvXbjo+Up6lyzBGxk0IPhDqW9Jx7CJQUQpsxqhoqnBmD8gn0Mt8i+RHHI9uZFCO+FP122ClAC8yljg==", "integrity": "sha512-qrBt3wfQgvXbjo+Up6lyzBGxk0IPhDqW9Jx7CJQUQpsxqhoqnBmD8gn0Mt8i+RHHI9uZFCO+FP122ClAC8yljg==",
"tarball": "https://npm.jsr.io/~/11/@jsr/std__dotenv/0.225.5.tgz" "tarball": "https://npm.jsr.io/~/11/@jsr/std__dotenv/0.225.5.tgz"
}, },
"@jsr/std__encoding@1.0.10": {
"integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==",
"tarball": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz"
},
"@jsr/std__fmt@1.0.8": {
"integrity": "sha512-miZHzj9OgjuajrcMKzpqNVwFb9O71UHZzV/FHVq0E0Uwmv/1JqXgmXAoBNPrn+MP0fHT3mMgaZ6XvQO7dam67Q==",
"tarball": "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.8.tgz"
},
"@jsr/std__fs@1.0.19": { "@jsr/std__fs@1.0.19": {
"integrity": "sha512-TEjyE8g+46jPlu7dJHLrwc8NMGl8zfG+JjWxyNQyDbxP0RtqZ4JmYZfR9vy4RWYWJQbLpw6Kbt2n+K/2zAO/JA==", "integrity": "sha512-TEjyE8g+46jPlu7dJHLrwc8NMGl8zfG+JjWxyNQyDbxP0RtqZ4JmYZfR9vy4RWYWJQbLpw6Kbt2n+K/2zAO/JA==",
"dependencies": [ "dependencies": [
@@ -507,14 +480,6 @@
], ],
"tarball": "https://npm.jsr.io/~/11/@jsr/std__testing/1.0.15.tgz" "tarball": "https://npm.jsr.io/~/11/@jsr/std__testing/1.0.15.tgz"
}, },
"@jsr/valkyr__auth@2.1.4": {
"integrity": "sha512-z4/OfPJ+7KZKdILrCWMO9nDQgOsLFw0A6HHgV3h7N1DpH1Ok0uRHUNAUA2IbI5KZif8yUPw+wsKztM6pOEMjbg==",
"dependencies": [
"jose",
"zod"
],
"tarball": "https://npm.jsr.io/~/11/@jsr/valkyr__auth/2.1.4.tgz"
},
"@jsr/valkyr__db@2.0.0": { "@jsr/valkyr__db@2.0.0": {
"integrity": "sha512-0gIauba+vQW6ssqMACLO1Z/METlhzoX+y4t9Sawh/IafQ986Rgvp6gCI+WArp7vbsO5hpItixrqjkxnnNC+h5g==", "integrity": "sha512-0gIauba+vQW6ssqMACLO1Z/METlhzoX+y4t9Sawh/IafQ986Rgvp6gCI+WArp7vbsO5hpItixrqjkxnnNC+h5g==",
"dependencies": [ "dependencies": [
@@ -800,16 +765,7 @@
"@tailwindcss/node", "@tailwindcss/node",
"@tailwindcss/oxide", "@tailwindcss/oxide",
"tailwindcss", "tailwindcss",
"vite@7.1.6_picomatch@4.0.3" "vite"
]
},
"@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": { "@tanstack/history@1.131.2": {
@@ -916,12 +872,6 @@
"@types/json-schema@7.0.15": { "@types/json-schema@7.0.15": {
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" "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": { "@types/react-dom@19.1.9_@types+react@19.1.13": {
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
"dependencies": [ "dependencies": [
@@ -1052,19 +1002,7 @@
"@rolldown/pluginutils", "@rolldown/pluginutils",
"@types/babel__core", "@types/babel__core",
"react-refresh", "react-refresh",
"vite@7.1.6_picomatch@4.0.3" "vite"
]
},
"@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": { "acorn-jsx@5.3.2_acorn@8.15.0": {
@@ -1077,6 +1015,12 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"bin": true "bin": true
}, },
"agent-base@6.0.2": {
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": [
"debug"
]
},
"ajv@6.12.6": { "ajv@6.12.6": {
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": [ "dependencies": [
@@ -1095,9 +1039,23 @@
"argparse@2.0.1": { "argparse@2.0.1": {
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" "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==",
"dependencies": [
"follow-redirects",
"form-data",
"proxy-from-env"
]
},
"balanced-match@1.0.2": { "balanced-match@1.0.2": {
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"base64-js@1.5.1": {
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"baseline-browser-mapping@2.8.6": { "baseline-browser-mapping@2.8.6": {
"integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
"bin": true "bin": true
@@ -1135,6 +1093,16 @@
"bson@6.10.4": { "bson@6.10.4": {
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==" "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": { "call-bind-apply-helpers@1.0.2": {
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": [ "dependencies": [
@@ -1177,18 +1145,36 @@
"color-name@1.1.4": { "color-name@1.1.4": {
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"combined-stream@1.0.8": {
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": [
"delayed-stream"
]
},
"concat-map@0.0.1": { "concat-map@0.0.1": {
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
}, },
"content-type@1.0.5": {
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
},
"convert-source-map@2.0.0": { "convert-source-map@2.0.0": {
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
}, },
"cookie-es@1.2.2": { "cookie-es@1.2.2": {
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="
}, },
"cookie@0.7.2": {
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
},
"cookie@1.0.2": { "cookie@1.0.2": {
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" "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": { "cross-spawn@7.0.6": {
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dependencies": [ "dependencies": [
@@ -1197,9 +1183,15 @@
"which" "which"
] ]
}, },
"crypto-js@4.2.0": {
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"csstype@3.1.3": { "csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
}, },
"dayjs@1.11.18": {
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
},
"debug@4.4.3": { "debug@4.4.3": {
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": [ "dependencies": [
@@ -1209,6 +1201,9 @@
"deep-is@0.1.4": { "deep-is@0.1.4": {
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
}, },
"delayed-stream@1.0.0": {
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"detect-libc@2.1.0": { "detect-libc@2.1.0": {
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==" "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="
}, },
@@ -1220,6 +1215,12 @@
"gopd" "gopd"
] ]
}, },
"ecdsa-sig-formatter@1.0.11": {
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": [
"safe-buffer"
]
},
"electron-to-chromium@1.5.222": { "electron-to-chromium@1.5.222": {
"integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==" "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w=="
}, },
@@ -1242,6 +1243,15 @@
"es-errors" "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": { "esbuild@0.25.10": {
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"optionalDependencies": [ "optionalDependencies": [
@@ -1448,6 +1458,19 @@
"flatted@3.3.3": { "flatted@3.3.3": {
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" "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": { "fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"os": ["darwin"], "os": ["darwin"],
@@ -1520,15 +1543,31 @@
"has-symbols@1.1.0": { "has-symbols@1.1.0": {
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
}, },
"has-tostringtag@1.0.2": {
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": [
"has-symbols"
]
},
"hasown@2.0.2": { "hasown@2.0.2": {
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": [ "dependencies": [
"function-bind" "function-bind"
] ]
}, },
"https-proxy-agent@5.0.1": {
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": [
"agent-base",
"debug"
]
},
"idb@8.0.3": { "idb@8.0.3": {
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==" "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
}, },
"ieee754@1.2.1": {
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore@5.3.2": { "ignore@5.3.2": {
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
}, },
@@ -1567,6 +1606,9 @@
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"bin": true "bin": true
}, },
"jose@4.15.9": {
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="
},
"jose@6.1.0": { "jose@6.1.0": {
"integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==" "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="
}, },
@@ -1597,6 +1639,36 @@
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": true "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": { "keyv@4.5.4": {
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dependencies": [ "dependencies": [
@@ -1610,6 +1682,9 @@
"type-check" "type-check"
] ]
}, },
"libphonenumber-js@1.12.21": {
"integrity": "sha512-z/o0jBYS3d8js1QBksrHxZUARjmM0S6uvpINkyJ9IcPkXIoUh5l4S3rTbGAlq9ThbCExdSV0wWp8gw7729A/ww=="
},
"lightningcss-darwin-arm64@1.30.1": { "lightningcss-darwin-arm64@1.30.1": {
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
"os": ["darwin"], "os": ["darwin"],
@@ -1684,9 +1759,30 @@
"p-locate" "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": { "lodash.merge@4.6.2": {
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"lodash.once@4.1.1": {
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"lru-cache@5.1.1": { "lru-cache@5.1.1": {
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dependencies": [ "dependencies": [
@@ -1715,6 +1811,15 @@
"picomatch@2.3.1" "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": { "mingo@6.6.1": {
"integrity": "sha512-KC6b1ODYoSdYu5fBm+SzQb7fa4ARmGwfa3Cf9F7U+2mnfD4Zhf89qQgO1cPTtaJ68w3ntIT5dVujgF52HvN7+g==" "integrity": "sha512-KC6b1ODYoSdYu5fBm+SzQb7fa4ARmGwfa3Cf9F7U+2mnfD4Zhf89qQgO1cPTtaJ68w3ntIT5dVujgF52HvN7+g=="
}, },
@@ -1733,21 +1838,17 @@
"minipass@7.1.2": { "minipass@7.1.2": {
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
}, },
"minizlib@3.0.2": { "minizlib@3.1.0": {
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
"dependencies": [ "dependencies": [
"minipass" "minipass"
] ]
}, },
"mkdirp@3.0.1": {
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"bin": true
},
"mongodb-connection-string-url@3.0.2": { "mongodb-connection-string-url@3.0.2": {
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"dependencies": [ "dependencies": [
"@types/whatwg-url", "@types/whatwg-url",
"whatwg-url" "whatwg-url@14.2.0"
] ]
}, },
"mongodb@6.20.0": { "mongodb@6.20.0": {
@@ -1772,9 +1873,18 @@
"natural-compare@1.4.0": { "natural-compare@1.4.0": {
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" "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": { "node-releases@2.0.21": {
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==" "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="
}, },
"nodemailer@6.10.1": {
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="
},
"object-inspect@1.13.4": { "object-inspect@1.13.4": {
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
}, },
@@ -1801,6 +1911,9 @@
"p-limit" "p-limit"
] ]
}, },
"pako@2.1.0": {
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"parent-module@1.0.1": { "parent-module@1.0.1": {
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dependencies": [ "dependencies": [
@@ -1825,6 +1938,12 @@
"picomatch@4.0.3": { "picomatch@4.0.3": {
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" "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": { "postcss@8.5.6": {
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dependencies": [ "dependencies": [
@@ -1843,6 +1962,12 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"bin": true "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": { "punycode@2.3.1": {
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
}, },
@@ -1852,6 +1977,9 @@
"side-channel" "side-channel"
] ]
}, },
"querystringify@2.2.0": {
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
},
"queue-microtask@1.2.3": { "queue-microtask@1.2.3": {
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
}, },
@@ -1868,6 +1996,9 @@
"react@19.1.1": { "react@19.1.1": {
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==" "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="
}, },
"requires-port@1.0.0": {
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"resolve-from@4.0.0": { "resolve-from@4.0.0": {
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
}, },
@@ -1918,9 +2049,15 @@
"tslib" "tslib"
] ]
}, },
"safe-buffer@5.2.1": {
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"scheduler@0.26.0": { "scheduler@0.26.0": {
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
}, },
"scmp@2.1.0": {
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="
},
"semver@6.3.1": { "semver@6.3.1": {
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"bin": true "bin": true
@@ -1938,6 +2075,9 @@
"seroval@1.3.2": { "seroval@1.3.2": {
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==" "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="
}, },
"set-cookie-parser@2.7.1": {
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
},
"shebang-command@2.0.0": { "shebang-command@2.0.0": {
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": [ "dependencies": [
@@ -2003,6 +2143,29 @@
"strip-json-comments@3.1.1": { "strip-json-comments@3.1.1": {
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" "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": { "supports-color@7.2.0": {
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": [ "dependencies": [
@@ -2015,14 +2178,13 @@
"tapable@2.2.3": { "tapable@2.2.3": {
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==" "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="
}, },
"tar@7.4.3": { "tar@7.4.4": {
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "integrity": "sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==",
"dependencies": [ "dependencies": [
"@isaacs/fs-minipass", "@isaacs/fs-minipass",
"chownr", "chownr",
"minipass", "minipass",
"minizlib", "minizlib",
"mkdirp",
"yallist@5.0.0" "yallist@5.0.0"
] ]
}, },
@@ -2039,12 +2201,25 @@
"picomatch@4.0.3" "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": { "to-regex-range@5.0.1": {
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": [ "dependencies": [
"is-number" "is-number"
] ]
}, },
"tr46@0.0.3": {
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"tr46@5.1.1": { "tr46@5.1.1": {
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"dependencies": [ "dependencies": [
@@ -2060,6 +2235,19 @@
"tslib@2.8.1": { "tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" "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": { "type-check@0.4.0": {
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dependencies": [ "dependencies": [
@@ -2081,9 +2269,6 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"bin": true "bin": true
}, },
"undici-types@7.10.0": {
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
},
"update-browserslist-db@1.1.3_browserslist@4.26.2": { "update-browserslist-db@1.1.3_browserslist@4.26.2": {
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dependencies": [ "dependencies": [
@@ -2099,6 +2284,13 @@
"punycode" "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": { "use-sync-external-store@1.5.0_react@19.1.1": {
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"dependencies": [ "dependencies": [
@@ -2124,24 +2316,8 @@
], ],
"bin": true "bin": true
}, },
"vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0": { "webidl-conversions@3.0.1": {
"integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"dependencies": [
"@types/node",
"esbuild",
"fdir",
"picomatch@4.0.3",
"postcss",
"rollup",
"tinyglobby"
],
"optionalDependencies": [
"fsevents"
],
"optionalPeers": [
"@types/node"
],
"bin": true
}, },
"webidl-conversions@7.0.0": { "webidl-conversions@7.0.0": {
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
@@ -2149,8 +2325,15 @@
"whatwg-url@14.2.0": { "whatwg-url@14.2.0": {
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"dependencies": [ "dependencies": [
"tr46", "tr46@5.1.1",
"webidl-conversions" "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"
] ]
}, },
"which@2.0.2": { "which@2.0.2": {
@@ -2163,6 +2346,9 @@
"word-wrap@1.2.5": { "word-wrap@1.2.5": {
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
}, },
"xmlbuilder@13.0.2": {
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="
},
"yallist@3.1.1": { "yallist@3.1.1": {
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
}, },
@@ -2226,20 +2412,16 @@
"modules/identity": { "modules/identity": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@cerbos/http@0.23.1", "npm:supertokens-node@23.0.1",
"npm:@jsr/felix__bcrypt@1.0.5",
"npm:@jsr/valkyr__auth@2.1.4",
"npm:@jsr/valkyr__event-store@2.0.1",
"npm:cookie@1.0.2",
"npm:zod@4.1.11" "npm:zod@4.1.11"
] ]
} }
}, },
"platform/cerbos": { "modules/workspace": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@cerbos/http@0.23.1", "npm:@jsr/valkyr__event-store@2.0.1",
"npm:@jsr/valkyr__auth@2.1.4", "npm:cookie@1.0.2",
"npm:zod@4.1.11" "npm:zod@4.1.11"
] ]
} }
@@ -2272,7 +2454,6 @@
"platform/relay": { "platform/relay": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [
"npm:@jsr/valkyr__auth@2.1.4",
"npm:path-to-regexp@8", "npm:path-to-regexp@8",
"npm:zod@4.1.11" "npm:zod@4.1.11"
] ]
@@ -2300,6 +2481,16 @@
] ]
} }
}, },
"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": { "platform/vault": {
"packageJson": { "packageJson": {
"dependencies": [ "dependencies": [

View File

@@ -1,17 +1,8 @@
services: services:
cerbos:
container_name: cerbos # MongoDB
image: ghcr.io/cerbos/cerbos:latest # --------------------------------------------------------------------------------
command: ["server", "--config=/config.yaml"] # <--- ensure config is used # Used by event store and read store for managing and reading application data.
ports:
- "3592:3592"
- "3593:3593"
- "3594:3594"
volumes:
- ./platform/cerbos/config.yaml:/config.yaml # <--- mount config
- ./platform/cerbos/policies:/data/policies # <--- mount policies
networks:
- localdev
mongo: mongo:
image: mongo:8 image: mongo:8
@@ -26,6 +17,68 @@ services:
networks: networks:
- localdev - 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.
cerbos:
container_name: cerbos
image: ghcr.io/cerbos/cerbos:latest
command: ["server", "--config=/config.yaml"]
ports:
- "3592:3592"
- "3593:3593"
- "3594:3594"
volumes:
- ./cerbos.yaml:/config.yaml
- ./modules/identity/cerbos/policies:/data/policies/identity
- ./modules/workspace/cerbos/policies:/data/policies/workspace
networks:
- localdev
networks: networks:
localdev: localdev:
driver: bridge driver: bridge

View File

@@ -1,66 +0,0 @@
import { AggregateRoot, getDate } from "@valkyr/event-store";
import { EventRecord, EventStoreFactory } from "../event-store.ts";
import { CodeIdentity } from "../events/code.ts";
export class Code extends AggregateRoot<EventStoreFactory> {
static override readonly name = "code";
identity!: CodeIdentity;
value!: string;
createdAt!: Date;
claimedAt?: Date;
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
get isClaimed(): boolean {
return this.claimedAt !== undefined;
}
// -------------------------------------------------------------------------
// Folder
// -------------------------------------------------------------------------
with(event: EventRecord): void {
switch (event.type) {
case "code:created": {
this.value = event.data.value;
this.identity = event.data.identity;
this.createdAt = getDate(event.created);
break;
}
case "code:claimed": {
this.claimedAt = getDate(event.created);
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
create(identity: CodeIdentity): this {
return this.push({
type: "code:created",
stream: this.id,
data: {
identity,
value: crypto
.getRandomValues(new Uint8Array(5))
.map((v) => v % 10)
.join(""),
},
});
}
claim(): this {
return this.push({
type: "code:claimed",
stream: this.id,
});
}
}

View File

@@ -1,211 +0,0 @@
import { AuditActor, auditors } from "@platform/spec/audit/actor.ts";
import { AggregateRoot, getDate } from "@valkyr/event-store";
import { db } from "../database.ts";
import { type EventRecord, eventStore, type EventStoreFactory, projector } from "../event-store.ts";
import type { Avatar } from "../schemas/avatar.ts";
import type { Contact } from "../schemas/contact.ts";
import type { Email } from "../schemas/email.ts";
import type { Name } from "../schemas/name.ts";
import type { Role } from "../schemas/role.ts";
import type { Strategy } from "../schemas/strategies.ts";
export class Identity extends AggregateRoot<EventStoreFactory> {
static override readonly name = "identity";
avatar?: Avatar;
name?: Name;
contact: Contact = {
emails: [],
};
strategies: Strategy[] = [];
roles: Role[] = [];
createdAt!: Date;
updatedAt!: Date;
// -------------------------------------------------------------------------
// Reducer
// -------------------------------------------------------------------------
with(event: EventRecord): void {
switch (event.type) {
case "identity:created": {
this.id = event.stream;
this.createdAt = getDate(event.created);
break;
}
case "identity:avatar:added": {
this.avatar = { url: event.data };
this.updatedAt = getDate(event.created);
break;
}
case "identity:name:added": {
this.name = event.data;
this.updatedAt = getDate(event.created);
break;
}
case "identity:email:added": {
this.contact.emails.push(event.data);
this.updatedAt = getDate(event.created);
break;
}
case "identity:role:added": {
this.roles.push(event.data);
this.updatedAt = getDate(event.created);
break;
}
case "identity:strategy:email:added": {
this.strategies.push({ type: "email", value: event.data });
this.updatedAt = getDate(event.created);
break;
}
case "identity:strategy:password:added": {
this.strategies.push({ type: "password", ...event.data });
this.updatedAt = getDate(event.created);
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
create(meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "identity:created",
meta,
});
}
addAvatar(url: string, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:avatar:added",
data: url,
meta,
});
}
addName(name: Name, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:name:added",
data: name,
meta,
});
}
addEmail(email: Email, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:email:added",
data: email,
meta,
});
}
addRole(role: Role, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:role:added",
data: role,
meta,
});
}
addEmailStrategy(email: string, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:strategy:email:added",
data: email,
meta,
});
}
addPasswordStrategy(alias: string, password: string, meta: AuditActor = auditors.system): this {
return this.push({
stream: this.id,
type: "identity:strategy:password:added",
data: { alias, password },
meta,
});
}
}
/*
|--------------------------------------------------------------------------------
| Utilities
|--------------------------------------------------------------------------------
*/
export async function isEmailClaimed(email: string): Promise<boolean> {
const relations = await eventStore.relations.getByKey(getIdentityEmailRelation(email));
if (relations.length > 0) {
return true;
}
return false;
}
/*
|--------------------------------------------------------------------------------
| Relations
|--------------------------------------------------------------------------------
*/
export function getIdentityEmailRelation(email: string): string {
return `/identities/emails/${email}`;
}
export function getIdentityAliasRelation(alias: string): string {
return `/identities/aliases/${alias}`;
}
/*
|--------------------------------------------------------------------------------
| Projectors
|--------------------------------------------------------------------------------
*/
projector.on("identity:created", async ({ stream: id }) => {
await db.collection("identities").insertOne({
id,
name: {
given: null,
family: null,
},
contact: {
emails: [],
},
strategies: [],
roles: [],
});
});
projector.on("identity:avatar:added", async ({ stream: id, data: url }) => {
await db.collection("identities").updateOne({ id }, { $set: { avatar: { url } } });
});
projector.on("identity:name:added", async ({ stream: id, data: name }) => {
await db.collection("identities").updateOne({ id }, { $set: { name } });
});
projector.on("identity:email:added", async ({ stream: id, data: email }) => {
await db.collection("identities").updateOne({ id }, { $push: { "contact.emails": email } });
});
projector.on("identity:role:added", async ({ stream: id, data: role }) => {
await db.collection("identities").updateOne({ id }, { $push: { roles: role } });
});
projector.on("identity:strategy:email:added", async ({ stream: id, data: email }) => {
await eventStore.relations.insert(getIdentityEmailRelation(email), id);
await db.collection("identities").updateOne({ id }, { $push: { strategies: { type: "email", value: email } } });
});
projector.on("identity:strategy:password:added", async ({ stream: id, data: strategy }) => {
await eventStore.relations.insert(getIdentityAliasRelation(strategy.alias), id);
await db.collection("identities").updateOne({ id }, { $push: { strategies: { type: "password", ...strategy } } });
});

View File

@@ -1,15 +0,0 @@
import { resources } from "@platform/cerbos/resources.ts";
import { Auth } from "@valkyr/auth";
import { access } from "./auth/access.ts";
import { jwt } from "./auth/jwt.ts";
import { principal } from "./auth/principal.ts";
export const auth = new Auth({
principal,
resources,
access,
jwt,
});
export type Session = typeof auth.$session;

View File

@@ -1,9 +0,0 @@
import { config } from "../config.ts";
export const jwt = {
algorithm: "RS256",
privateKey: config.auth.privateKey,
publicKey: config.auth.publicKey,
issuer: "http://localhost",
audience: "http://localhost",
};

View File

@@ -1,39 +0,0 @@
import { HttpAdapter, makeClient } from "@platform/relay";
import { PrincipalProvider } from "@valkyr/auth";
import z from "zod";
import { config } from "../config.ts";
import resolve from "../routes/identities/resolve/spec.ts";
import { RoleSchema } from "../schemas/role.ts";
export const identity = makeClient(
{
adapter: new HttpAdapter({
url: config.url,
}),
},
{
resolve: resolve.crypto({
publicKey: config.internal.publicKey,
}),
},
);
export const principal = new PrincipalProvider(
RoleSchema,
{
workspaceIds: z.array(z.string()).optional().default([]),
},
async function (id: string) {
const response = await identity.resolve({ params: { id } });
if ("data" in response) {
return {
id,
roles: response.data.roles,
attributes: this.attributes.parse(response.data.attributes),
};
}
},
);
export type Principal = typeof principal.$principal;

View File

@@ -0,0 +1,23 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: identity
version: default
rules:
# Admins can read any identity with limited fields
- actions: ["read", "update"]
effect: EFFECT_ALLOW
roles: ["admin"]
# Users can fully read, update, or delete their own identity
- actions: ["read", "update", "delete"]
effect: EFFECT_ALLOW
roles: ["user"]
condition:
match:
expr: request.resource.id == request.principal.id

View File

@@ -0,0 +1,14 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: role
version: default
rules:
# Admin can manage roles
- actions: ["manage"]
effect: EFFECT_ALLOW
roles: ["admin"]

View File

@@ -1,14 +1,11 @@
import { ResourceRegistry } from "@valkyr/auth"; /*
export const resources = new ResourceRegistry([ export const resources = new ResourceRegistry([
{ {
kind: "identity", kind: "identity",
attr: {}, actions: ["read", "update", "delete"],
},
{
kind: "workspace",
attr: {}, attr: {},
}, },
] as const); ] as const);
export type Resource = typeof resources.$resource; export type Resource = typeof resources.$resource;
*/

View File

@@ -2,11 +2,10 @@ import { HttpAdapter, makeClient } from "@platform/relay";
import { config } from "./config.ts"; import { config } from "./config.ts";
import getById from "./routes/identities/get/spec.ts"; import getById from "./routes/identities/get/spec.ts";
import me from "./routes/identities/me/spec.ts";
import register from "./routes/identities/register/spec.ts";
import loginByPassword from "./routes/login/code/spec.ts"; import loginByPassword from "./routes/login/code/spec.ts";
import loginByEmail from "./routes/login/email/spec.ts"; import loginByEmail from "./routes/login/email/spec.ts";
import loginByCode from "./routes/login/password/spec.ts"; import loginByCode from "./routes/login/password/spec.ts";
import me from "./routes/me/spec.ts";
export const identity = makeClient( export const identity = makeClient(
{ {
@@ -15,11 +14,6 @@ export const identity = makeClient(
}), }),
}, },
{ {
/**
* TODO ...
*/
register,
/** /**
* TODO ... * TODO ...
*/ */

View File

@@ -1,8 +1,4 @@
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { getEnvironmentVariable } from "@platform/config/environment.ts"; import { getEnvironmentVariable } from "@platform/config/environment.ts";
import type { SerializeOptions } from "cookie";
import z from "zod"; import z from "zod";
export const config = { export const config = {
@@ -11,49 +7,4 @@ export const config = {
type: z.url(), type: z.url(),
fallback: "http://localhost:8370", fallback: "http://localhost:8370",
}), }),
auth: {
privateKey: getEnvironmentVariable({
key: "AUTH_PRIVATE_KEY",
type: z.string(),
fallback: await readFile(resolve(import.meta.dirname!, ".keys", "private"), "utf-8"),
}),
publicKey: getEnvironmentVariable({
key: "AUTH_PUBLIC_KEY",
type: z.string(),
fallback: await readFile(resolve(import.meta.dirname!, ".keys", "public"), "utf-8"),
}),
},
internal: {
privateKey: getEnvironmentVariable({
key: "INTERNAL_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: "INTERNAL_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,11 +0,0 @@
import * as bcrypt from "@felix/bcrypt";
export const password = { hash, verify };
async function hash(password: string): Promise<string> {
return bcrypt.hash(password);
}
async function verify(password: string, hash: string): Promise<boolean> {
return bcrypt.verify(password, hash);
}

View File

@@ -1,49 +0,0 @@
import { getDatabaseAccessor } from "@platform/database/accessor.ts";
import { type Identity, parseIdentity } from "./models/identity.ts";
import type { PasswordStrategy } from "./schemas/strategies.ts";
export const db = getDatabaseAccessor<{
identities: Identity;
}>(`identity:read-store`);
/*
|--------------------------------------------------------------------------------
| Identity
|--------------------------------------------------------------------------------
*/
/**
* Retrieve a single account by its primary identifier.
*
* @param id - Unique identity.
*/
export async function getIdentityById(id: string): Promise<Identity | undefined> {
return db
.collection("identities")
.findOne({ id })
.then((document) => parseIdentity(document));
}
/**
* Get strategy details for the given password strategy alias.
*
* @param alias - Alias to get strategy for.
*/
export async function getPasswordStrategyByAlias(
alias: string,
): Promise<({ accountId: string } & PasswordStrategy) | undefined> {
const account = await db.collection("identities").findOne({
strategies: {
$elemMatch: { type: "password", alias },
},
});
if (account === null) {
return undefined;
}
const strategy = account.strategies.find((strategy) => strategy.type === "password" && strategy.alias === alias);
if (strategy === undefined) {
return undefined;
}
return { accountId: account.id, ...strategy } as { accountId: string } & PasswordStrategy;
}

View File

@@ -1,7 +0,0 @@
import { ConflictError } from "@platform/relay";
export class IdentityEmailClaimedError extends ConflictError {
constructor(email: string) {
super(`Email '${email}' is already claimed by another identity.`);
}
}

View File

@@ -1,18 +0,0 @@
import { event } from "@valkyr/event-store";
import z from "zod";
const CodeIdentitySchema = z.object({
id: z.string(),
});
export default [
event.type("code:created").data(
z.object({
identity: CodeIdentitySchema,
value: z.string(),
}),
),
event.type("code:claimed"),
];
export type CodeIdentity = z.infer<typeof CodeIdentitySchema>;

View File

@@ -1,21 +0,0 @@
import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
import { event } from "@valkyr/event-store";
import z from "zod";
import { EmailSchema } from "../schemas/email.ts";
import { NameSchema } from "../schemas/name.ts";
import { RoleSchema } from "../schemas/role.ts";
export default [
event.type("identity:created").meta(AuditActorSchema),
event.type("identity:avatar:added").data(z.string()).meta(AuditActorSchema),
event.type("identity:name:added").data(NameSchema).meta(AuditActorSchema),
event.type("identity:email:added").data(EmailSchema).meta(AuditActorSchema),
event.type("identity:role:added").data(RoleSchema).meta(AuditActorSchema),
event.type("identity:strategy:email:added").data(z.string()).meta(AuditActorSchema),
event.type("identity:strategy:passkey:added").meta(AuditActorSchema),
event
.type("identity:strategy:password:added")
.data(z.object({ alias: z.string(), password: z.string() }))
.meta(AuditActorSchema),
];

View File

@@ -1,36 +0,0 @@
import { makeDocumentParser } from "@platform/database/utilities.ts";
import { z } from "zod";
import { AvatarSchema } from "../schemas/avatar.ts";
import { ContactSchema } from "../schemas/contact.ts";
import { NameSchema } from "../schemas/name.ts";
import { RoleSchema } from "../schemas/role.ts";
import { StrategySchema } from "../schemas/strategies.ts";
export const IdentitySchema = z.object({
id: z.uuid(),
avatar: AvatarSchema.optional(),
name: NameSchema.optional(),
contact: ContactSchema.default({
emails: [],
}),
strategies: z.array(StrategySchema).default([]),
roles: z.array(RoleSchema).default([]),
attributes: z.record(z.string(), z.any()),
});
/*
|--------------------------------------------------------------------------------
| Parsers
|--------------------------------------------------------------------------------
*/
export const parseIdentity = makeDocumentParser(IdentitySchema);
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type Identity = z.infer<typeof IdentitySchema>;

View File

@@ -7,22 +7,11 @@
"./client.ts": "./client.ts", "./client.ts": "./client.ts",
"./server.ts": "./server.ts" "./server.ts": "./server.ts"
}, },
"types": "types.d.ts",
"dependencies": { "dependencies": {
"@cerbos/http": "0.23.1",
"@felix/bcrypt": "npm:@jsr/felix__bcrypt@1.0.5",
"@platform/cerbos": "workspace:*",
"@platform/config": "workspace:*", "@platform/config": "workspace:*",
"@platform/database": "workspace:*",
"@platform/logger": "workspace:*", "@platform/logger": "workspace:*",
"@platform/relay": "workspace:*", "@platform/relay": "workspace:*",
"@platform/server": "workspace:*", "supertokens-node": "23.0.1",
"@platform/spec": "workspace:*",
"@platform/storage": "workspace:*",
"@platform/vault": "workspace:*",
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4",
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
"cookie": "1.0.2",
"zod": "4.1.11" "zod": "4.1.11"
} }
} }

View File

@@ -1,16 +1,21 @@
import { ForbiddenError, NotFoundError } from "@platform/relay"; import { ForbiddenError, NotFoundError } from "@platform/relay";
import { getPrincipalAttributes, getPrincipalRoles } from "@platform/supertoken/principal.ts";
import { getUserById } from "@platform/supertoken/users.ts";
import { getIdentityById } from "../../../database.ts";
import route from "./spec.ts"; import route from "./spec.ts";
export default route.access("session").handle(async ({ params: { id } }, { access }) => { export default route.access("session").handle(async ({ params: { id } }, { access }) => {
const identity = await getIdentityById(id); const user = await getUserById(id);
if (identity === undefined) { if (user === undefined) {
return new NotFoundError("Identity does not exist, or has been removed."); return new NotFoundError("Identity does not exist, or has been removed.");
} }
const decision = await access.isAllowed({ kind: "identity", id: identity.id, attr: {} }, "read"); const decision = await access.isAllowed({ kind: "identity", id: user.id, attr: {} }, "read");
if (decision === false) { if (decision === false) {
return new ForbiddenError("You do not have permission to view this identity."); return new ForbiddenError("You do not have permission to view this identity.");
} }
return identity; return {
id: user.id,
roles: await getPrincipalRoles(id),
attr: await getPrincipalAttributes(id),
};
}); });

View File

@@ -1,12 +1,10 @@
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay"; import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod"; import z from "zod";
import { IdentitySchema } from "../../../models/identity.ts";
export default route export default route
.get("/api/v1/identities/:id") .get("/api/v1/identity/:id")
.params({ .params({
id: z.string(), id: z.string(),
}) })
.errors([UnauthorizedError, ForbiddenError, NotFoundError]) .errors([UnauthorizedError, ForbiddenError, NotFoundError])
.response(IdentitySchema); .response(z.any());

View File

@@ -1,12 +0,0 @@
import { UnauthorizedError } from "@platform/relay";
import { getIdentityById } from "../../../database.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ principal }) => {
const identity = await getIdentityById(principal.id);
if (identity === undefined) {
return new UnauthorizedError("You must be signed in to view your session.");
}
return identity;
});

View File

@@ -1,5 +0,0 @@
import { NotFoundError, route, UnauthorizedError } from "@platform/relay";
import { IdentitySchema } from "../../../models/identity.ts";
export default route.get("/api/v1/identities/me").response(IdentitySchema).errors([UnauthorizedError, NotFoundError]);

View File

@@ -1,11 +0,0 @@
import { Identity, isEmailClaimed } from "../../../aggregates/identity.ts";
import { IdentityEmailClaimedError } from "../../../errors.ts";
import { eventStore } from "../../../event-store.ts";
import route from "./spec.ts";
export default route.access("public").handle(async ({ body: { name, email } }) => {
if ((await isEmailClaimed(email)) === true) {
return new IdentityEmailClaimedError(email);
}
return eventStore.aggregate.from(Identity).create().addName(name).addEmailStrategy(email).addRole("user").save();
});

View File

@@ -1,17 +0,0 @@
import { route } from "@platform/relay";
import z from "zod";
import { IdentityEmailClaimedError } from "../../../errors.ts";
import { IdentitySchema } from "../../../models/identity.ts";
import { NameSchema } from "../../../schemas/name.ts";
export default route
.post("/api/v1/identities")
.body(
z.object({
name: NameSchema,
email: z.email(),
}),
)
.errors([IdentityEmailClaimedError])
.response(IdentitySchema);

View File

@@ -1,13 +0,0 @@
import { NotFoundError } from "@platform/relay";
import { config } from "../../../config.ts";
import { getIdentityById } from "../../../database.ts";
import route from "./spec.ts";
export default route.access(["internal:public", config.internal.privateKey]).handle(async ({ params: { id } }) => {
const identity = await getIdentityById(id);
if (identity === undefined) {
return new NotFoundError();
}
return identity;
});

View File

@@ -1,5 +0,0 @@
import { importVault } from "@platform/vault";
import { config } from "../../../config.ts";
export const vault = importVault(config.internal);

View File

@@ -1,12 +0,0 @@
import { NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
import { IdentitySchema } from "../../../models/identity.ts";
export default route
.get("/api/v1/identities/:id/resolve")
.params({
id: z.string(),
})
.response(IdentitySchema)
.errors([UnauthorizedError, NotFoundError]);

View File

@@ -0,0 +1,40 @@
import { ForbiddenError } from "@platform/relay";
import { getPrincipalAttributes } from "@platform/supertoken/principal.ts";
import UserMetadata from "supertokens-node/recipe/usermetadata";
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");
if (decision === false) {
return new ForbiddenError("You do not have permission to update this identity.");
}
const attr = await getPrincipalAttributes(id);
for (const op of ops) {
switch (op.type) {
case "add": {
attr[op.key] = op.value;
break;
}
case "push": {
if (attr[op.key] === undefined) {
attr[op.key] = op.values;
} else {
attr[op.key] = [...attr[op.key], ...op.values];
}
break;
}
case "pop": {
if (Array.isArray(attr[op.key])) {
attr[op.key] = attr[op.key].filter((value: any) => op.values.includes(value) === false);
}
break;
}
case "remove": {
delete attr[op.key];
break;
}
}
}
await UserMetadata.updateUserMetadata(id, { attr });
});

View File

@@ -0,0 +1,29 @@
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route
.put("/api/v1/identity/:id")
.params({
id: z.string(),
})
.body(
z.array(
z.union([
z.strictObject({
type: z.union([z.literal("add")]),
key: z.string(),
value: z.any(),
}),
z.strictObject({
type: z.union([z.literal("push"), z.literal("pop")]),
key: z.string(),
values: z.array(z.any()),
}),
z.strictObject({
type: z.union([z.literal("remove")]),
key: z.string(),
}),
]),
),
)
.errors([UnauthorizedError, ForbiddenError, NotFoundError]);

View File

@@ -1,85 +1,25 @@
import { logger } from "@platform/logger"; import { logger } from "@platform/logger";
import cookie from "cookie"; import { NotFoundError } from "@platform/relay";
import { getSessionHeaders } from "@platform/supertoken/session.ts";
import Passwordless from "supertokens-node/recipe/passwordless";
import { Code } from "../../../aggregates/code.ts";
import { Identity } from "../../../aggregates/identity.ts";
import { auth } from "../../../auth.ts";
import { config } from "../../../config.ts";
import { eventStore } from "../../../event-store.ts";
import route from "./spec.ts"; import route from "./spec.ts";
export default route.access("public").handle(async ({ params: { identityId, codeId, value }, query: { next } }) => { export default route.access("public").handle(async ({ body: { preAuthSessionId, deviceId, userInputCode } }) => {
const code = await eventStore.aggregate.getByStream(Code, codeId); const response = await Passwordless.consumeCode({ tenantId: "public", preAuthSessionId, deviceId, userInputCode });
if (response.status !== "OK") {
if (code === undefined) { return new NotFoundError();
return logger.info({
type: "code:claimed",
session: false,
message: "Invalid Code ID",
received: codeId,
});
} }
if (code.claimedAt !== undefined) { logger.info({
return logger.info({ type: "code:claimed",
type: "code:claimed", session: true,
session: false, message: "Identity resolved",
message: "Code Already Claimed", user: response.user.toJson(),
received: codeId, });
});
}
await code.claim().save();
if (code.value !== value) {
return logger.info({
type: "code:claimed",
session: false,
message: "Invalid Value",
expected: code.value,
received: value,
});
}
if (code.identity.id !== identityId) {
return logger.info({
type: "code:claimed",
session: false,
message: "Invalid Identity ID",
expected: code.identity.id,
received: identityId,
});
}
const account = await eventStore.aggregate.getByStream(Identity, identityId);
if (account === undefined) {
return logger.info({
type: "code:claimed",
session: false,
message: "Account Not Found",
expected: code.identity.id,
received: undefined,
});
}
logger.info({ type: "code:claimed", session: true });
const options = config.cookie(1000 * 60 * 60 * 24 * 7);
if (next !== undefined) {
return new Response(null, {
status: 302,
headers: {
location: next,
"set-cookie": cookie.serialize("token", await auth.generate({ id: account.id }, "1 week"), options),
},
});
}
return new Response(null, { return new Response(null, {
status: 200, status: 200,
headers: { headers: await getSessionHeaders("public", response.recipeUserId),
"set-cookie": cookie.serialize("token", await auth.generate({ id: account.id }, "1 week"), options),
},
}); });
}); });

View File

@@ -2,12 +2,14 @@ import { route } from "@platform/relay";
import z from "zod"; import z from "zod";
export default route export default route
.get("/api/v1/identities/login/code/:identityId/code/:codeId/:value") .post("/api/v1/identity/login/code")
.params({ .body(
identityId: z.string(), z.strictObject({
codeId: z.string(), deviceId: z.string(),
value: z.string(), preAuthSessionId: z.string(),
}) userInputCode: z.string(),
}),
)
.query({ .query({
next: z.string().optional(), next: z.string().optional(),
}); });

View File

@@ -1,27 +1,23 @@
import { logger } from "@platform/logger"; import { logger } from "@platform/logger";
import Passwordless from "supertokens-node/recipe/passwordless";
import { Code } from "../../../aggregates/code.ts";
import { getIdentityEmailRelation, Identity } from "../../../aggregates/identity.ts";
import { eventStore } from "../../../event-store.ts";
import route from "./spec.ts"; import route from "./spec.ts";
export default route.access("public").handle(async ({ body: { base, email } }) => { export default route.access("public").handle(async ({ body: { email } }) => {
const identity = await eventStore.aggregate.getByRelation(Identity, getIdentityEmailRelation(email)); const response = await Passwordless.createCode({ tenantId: "public", email });
if (identity === undefined) { if (response.status !== "OK") {
return logger.info({ return logger.info({
type: "auth:email", type: "auth:passwordless",
code: false, message: "Create code failed.",
message: "Identity Not Found",
received: email, received: email,
}); });
} }
const code = await eventStore.aggregate.from(Code).create({ id: identity.id }).save();
logger.info({ logger.info({
type: "auth:email", type: "auth:passwordless",
data: { data: {
code: code.id, deviceId: response.deviceId,
identityId: identity.id, preAuthSessionId: response.preAuthSessionId,
userInputCode: response.userInputCode,
}, },
link: `${base}/api/v1/admin/auth/${identity.id}/code/${code.id}/${code.value}?next=${base}/admin`,
}); });
}); });

View File

@@ -1,9 +1,8 @@
import { route } from "@platform/relay"; import { route } from "@platform/relay";
import z from "zod"; import z from "zod";
export default route.post("/api/v1/identities/login/email").body( export default route.post("/api/v1/identity/login/email").body(
z.object({ z.object({
base: z.url(),
email: z.email(), email: z.email(),
}), }),
); );

View File

@@ -0,0 +1,48 @@
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),
});
});

View File

@@ -0,0 +1,8 @@
import { route } from "@platform/relay";
import z from "zod";
export default route.post("/api/v1/identities/login/sudo").body(
z.object({
email: z.email(),
}),
);

View File

@@ -0,0 +1,5 @@
import route from "./spec.ts";
export default route.access("session").handle(async ({ principal }) => {
return principal;
});

View File

@@ -0,0 +1,4 @@
import { NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route.get("/api/v1/identity/me").errors([UnauthorizedError, NotFoundError]).response(z.any());

View File

@@ -0,0 +1,30 @@
import { ForbiddenError } from "@platform/relay";
import { getPrincipalRoles } from "@platform/supertoken/principal.ts";
import UserMetadata from "supertokens-node/recipe/usermetadata";
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");
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));
for (const op of ops) {
switch (op.type) {
case "add": {
for (const role of op.roles) {
roles.add(role);
}
break;
}
case "remove": {
for (const role of op.roles) {
roles.delete(role);
}
break;
}
}
}
await UserMetadata.updateUserMetadata(id, { roles: Array.from(roles) });
});

View File

@@ -0,0 +1,19 @@
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
import z from "zod";
export default route
.put("/api/v1/identity/:id/roles")
.params({
id: z.string(),
})
.body(
z.array(
z.union([
z.strictObject({
type: z.union([z.literal("add"), z.literal("remove")]),
roles: z.array(z.any()),
}),
]),
),
)
.errors([UnauthorizedError, ForbiddenError, NotFoundError]);

View File

@@ -1,9 +0,0 @@
import z from "zod";
import { EmailSchema } from "./email.ts";
export const ContactSchema = z.object({
emails: z.array(EmailSchema).default([]).describe("A list of email addresses associated with the contact."),
});
export type Contact = z.infer<typeof ContactSchema>;

View File

@@ -1,5 +0,0 @@
import z from "zod";
export const RoleSchema = z.union([z.literal("user"), z.literal("admin")]);
export type Role = z.infer<typeof RoleSchema>;

View File

@@ -1,37 +0,0 @@
import z from "zod";
const EmailStrategySchema = z.object({
type: z.literal("email"),
value: z.string(),
});
const PasswordStrategySchema = z.object({
type: z.literal("password"),
alias: z.string(),
password: z.string(),
});
const PasskeyStrategySchema = z.object({
type: z.literal("passkey"),
credId: z.string(),
credPublicKey: z.string(),
webauthnUserId: z.string(),
counter: z.number(),
backupEligible: z.boolean(),
backupStatus: z.boolean(),
transports: z.string(),
createdAt: z.date(),
lastUsed: z.date(),
});
export const StrategySchema = z.discriminatedUnion("type", [
EmailStrategySchema,
PasswordStrategySchema,
PasskeyStrategySchema,
]);
export type EmailStrategy = z.infer<typeof EmailStrategySchema>;
export type PasswordStrategy = z.infer<typeof PasswordStrategySchema>;
export type PasskeyStrategy = z.infer<typeof PasskeyStrategySchema>;
export type Strategy = z.infer<typeof StrategySchema>;

View File

@@ -1,96 +1,12 @@
import "./types.d.ts";
import { idIndex } from "@platform/database/id.ts";
import { register as registerReadStore } from "@platform/database/registrar.ts";
import { UnauthorizedError } from "@platform/relay";
import { context } from "@platform/relay";
import { storage } from "@platform/storage";
import { register as registerEventStore } from "@valkyr/event-store/mongo";
import cookie from "cookie";
import { auth } from "./auth.ts";
import { db } from "./database.ts";
import { eventStore } from "./event-store.ts";
export default { export default {
routes: [ routes: [
(await import("./routes/identities/get/handle.ts")).default, (await import("./routes/identities/get/handle.ts")).default,
(await import("./routes/identities/register/handle.ts")).default, (await import("./routes/identities/update/handle.ts")).default,
(await import("./routes/identities/me/handle.ts")).default,
(await import("./routes/identities/resolve/handle.ts")).default,
(await import("./routes/login/code/handle.ts")).default, (await import("./routes/login/code/handle.ts")).default,
(await import("./routes/login/email/handle.ts")).default, (await import("./routes/login/email/handle.ts")).default,
(await import("./routes/login/password/handle.ts")).default, // (await import("./routes/login/password/handle.ts")).default,
(await import("./routes/login/sudo/handle.ts")).default,
(await import("./routes/me/handle.ts")).default,
(await import("./routes/roles/handle.ts")).default,
], ],
/**
* TODO ...
*/
bootstrap: async (): Promise<void> => {
await registerReadStore(db.db, [
{
name: "identities",
indexes: [
idIndex,
[{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
[{ "strategies.type": 1, "strategies.value": 1 }, { name: "strategy.email" }],
],
},
]);
await registerEventStore(eventStore.db.db, console.info);
Object.defineProperties(context, {
/**
* TODO ...
*/
isAuthenticated: {
get() {
return storage.getStore()?.principal !== undefined;
},
},
/**
* 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;
},
},
});
},
/**
* TODO ...
*/
resolve: async (request: Request): Promise<void> => {
const token = cookie.parse(request.headers.get("cookie") ?? "").token;
if (token !== undefined) {
const session = await auth.resolve(token);
if (session.valid === true) {
const context = storage.getStore();
if (context === undefined) {
return;
}
context.principal = session.principal;
context.access = session.access;
}
}
},
}; };

View File

@@ -0,0 +1,66 @@
import { AuditActor, auditors } from "@platform/spec/audit/actor.ts";
import { AggregateRoot, getDate } from "@valkyr/event-store";
import { db } from "../database.ts";
import { EventRecord, EventStoreFactory, projector } from "../event-store.ts";
export class WorkspaceUser extends AggregateRoot<EventStoreFactory> {
static override readonly name = "workspace:user";
workspaceId!: string;
identityId!: string;
createdAt!: Date;
updatedAt?: Date;
// -------------------------------------------------------------------------
// Reducer
// -------------------------------------------------------------------------
with(event: EventRecord): void {
switch (event.type) {
case "workspace:user:created": {
this.workspaceId = event.data.workspaceId;
this.identityId = event.data.identityId;
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
create(workspaceId: string, identityId: string, meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "workspace:user:created",
data: {
workspaceId,
identityId,
},
meta,
});
}
}
/*
|--------------------------------------------------------------------------------
| Projectors
|--------------------------------------------------------------------------------
*/
projector.on("workspace:user:created", async ({ stream: id, data: { workspaceId, identityId }, meta, created }) => {
await db.collection("workspace:users").insertOne({
id,
workspaceId,
identityId,
name: {
given: "",
family: "",
},
contacts: [],
createdAt: getDate(created),
createdBy: meta.user.uid ?? "Unknown",
});
});

View File

@@ -0,0 +1,120 @@
import { AuditActor, auditors } from "@platform/spec/audit/actor.ts";
import { AggregateRoot, getDate } from "@valkyr/event-store";
import { db } from "../database.ts";
import { EventRecord, EventStoreFactory, projector } from "../event-store.ts";
export class Workspace extends AggregateRoot<EventStoreFactory> {
static override readonly name = "workspace";
ownerId!: string;
name!: string;
description?: string;
archived = false;
createdAt!: Date;
updatedAt?: Date;
// -------------------------------------------------------------------------
// Reducer
// -------------------------------------------------------------------------
with(event: EventRecord): void {
switch (event.type) {
case "workspace:created": {
this.id = event.stream;
this.ownerId = event.data.ownerId;
this.name = event.data.name;
this.createdAt = getDate(event.created);
break;
}
case "workspace:name:added": {
this.name = event.data;
this.updatedAt = getDate(event.created);
break;
}
case "workspace:description:added": {
this.description = event.data;
this.updatedAt = getDate(event.created);
break;
}
case "workspace:archived": {
this.archived = true;
this.updatedAt = getDate(event.created);
break;
}
case "workspace:restored": {
this.archived = false;
this.updatedAt = getDate(event.created);
break;
}
}
}
// -------------------------------------------------------------------------
// Actions
// -------------------------------------------------------------------------
create(ownerId: string, name: string, meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "workspace:created",
data: {
ownerId,
name,
},
meta,
});
}
setName(name: string, meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "workspace:name:added",
data: name,
meta,
});
}
setDescription(description: string, meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "workspace:description:added",
data: description,
meta,
});
}
archive(meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "workspace:archived",
meta,
});
}
restore(meta: AuditActor = auditors.system) {
return this.push({
stream: this.id,
type: "workspace:restored",
meta,
});
}
}
/*
|--------------------------------------------------------------------------------
| Projectors
|--------------------------------------------------------------------------------
*/
projector.on("workspace:created", async ({ stream: id, data: { ownerId, name }, meta, created }) => {
await db.collection("workspaces").insertOne({
id,
ownerId,
name,
createdAt: getDate(created),
createdBy: meta.user.uid ?? "Unknown",
});
});

View File

@@ -0,0 +1,33 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: workspace
version: default
rules:
- actions: ["create"]
effect: EFFECT_ALLOW
roles: ["super"]
- actions: ["read"]
effect: EFFECT_ALLOW
roles: ["super", "admin", "user"]
condition:
match:
expr: R.attr.id in P.attr.workspaceIds
- actions: ["update"]
effect: EFFECT_ALLOW
roles: ["super", "admin"]
condition:
match:
expr: R.attr.id in P.attr.workspaceIds
- actions: ["delete"]
effect: EFFECT_ALLOW
roles: ["super"]
condition:
match:
expr: R.attr.id in P.attr.workspaceIds

View File

@@ -3,13 +3,47 @@
apiVersion: api.cerbos.dev/v1 apiVersion: api.cerbos.dev/v1
resourcePolicy: resourcePolicy:
resource: workspace resource: workspace_user
version: default version: default
rules: rules:
### Read # Admins can invite new members into their own workspace
- actions: - actions:
- invite
effect: EFFECT_ALLOW
roles:
- admin
condition:
match:
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
# Admins can remove members from their own workspace
- actions:
- remove
effect: EFFECT_ALLOW
roles:
- admin
condition:
match:
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
# Admins can update member roles in their own workspace
- actions:
- update_role
effect: EFFECT_ALLOW
roles:
- admin
condition:
match:
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
# Admins and users can list/read members of their own workspace
- actions:
- list
- read - read
effect: EFFECT_ALLOW effect: EFFECT_ALLOW
roles: roles:
@@ -17,26 +51,4 @@ resourcePolicy:
- user - user
condition: condition:
match: match:
expr: request.principal.workspaceIds.includes(request.resource.id) expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
### Update
- actions:
- update
effect: EFFECT_ALLOW
roles:
- admin
condition:
match:
expr: request.principal.workspaceIds.includes(request.resource.id)
### Delete
- actions:
- delete
effect: EFFECT_ALLOW
roles:
- admin
condition:
match:
expr: request.principal.workspaceIds.includes(request.resource.id)

View File

@@ -0,0 +1,22 @@
import z from "zod";
/*
export const resources = new ResourceRegistry([
{
kind: "workspace",
actions: [],
attr: {
workspaceId: z.string(),
},
},
{
kind: "workspace_user",
actions: [],
attr: {
workspaceId: z.string(),
},
},
] as const);
export type Resource = typeof resources.$resource;
*/

View File

View File

@@ -0,0 +1,27 @@
import { getDatabaseAccessor } from "@platform/database/accessor.ts";
import { parseWorkspace, type Workspace } from "./models/workspace.ts";
import { WorkspaceUser } from "./models/workspace-user.ts";
export const db = getDatabaseAccessor<{
workspaces: Workspace;
"workspace:users": WorkspaceUser;
}>(`workspace:read-store`);
/*
|--------------------------------------------------------------------------------
| Identity
|--------------------------------------------------------------------------------
*/
/**
* Retrieve a single workspace by its primary identifier.
*
* @param id - Unique identity.
*/
export async function getWorkspaceById(id: string): Promise<Workspace | undefined> {
return db
.collection("workspaces")
.findOne({ id })
.then((document) => parseWorkspace(document));
}

View File

@@ -9,8 +9,8 @@ import { MongoAdapter } from "@valkyr/event-store/mongo";
*/ */
const eventFactory = new EventFactory([ const eventFactory = new EventFactory([
...(await import("./events/code.ts")).default, ...(await import("./events/workspace.ts")).default,
...(await import("./events/identity.ts")).default, ...(await import("./events/workspace-user.ts")).default,
]); ]);
/* /*
@@ -20,7 +20,7 @@ const eventFactory = new EventFactory([
*/ */
export const eventStore = new EventStore({ export const eventStore = new EventStore({
adapter: new MongoAdapter(() => container.get("mongo"), `identity:event-store`), adapter: new MongoAdapter(() => container.get("mongo"), `workspace:event-store`),
events: eventFactory, events: eventFactory,
snapshot: "auto", snapshot: "auto",
}); });

View File

@@ -0,0 +1,23 @@
import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
import { event } from "@valkyr/event-store";
import z from "zod";
import { AvatarSchema } from "../value-objects/avatar.ts";
import { ContactSchema } from "../value-objects/contact.ts";
import { NameSchema } from "../value-objects/name.ts";
export default [
event
.type("workspace:user:created")
.data(
z.strictObject({
workspaceId: z.string(),
identityId: z.string(),
}),
)
.meta(AuditActorSchema),
event.type("workspace:user:name-set").data(NameSchema).meta(AuditActorSchema),
event.type("workspace:user:avatar-set").data(AvatarSchema).meta(AuditActorSchema),
event.type("workspace:user:contacts-added").data(z.array(ContactSchema)).meta(AuditActorSchema),
event.type("workspace:user:contacts-removed").data(z.array(z.string())).meta(AuditActorSchema),
];

View File

@@ -0,0 +1,19 @@
import { AuditActorSchema } from "@platform/spec/audit/actor.ts";
import { event } from "@valkyr/event-store";
import z from "zod";
export default [
event
.type("workspace:created")
.data(
z.strictObject({
ownerId: z.uuid(),
name: z.string(),
}),
)
.meta(AuditActorSchema),
event.type("workspace:name:added").data(z.string()).meta(AuditActorSchema),
event.type("workspace:description:added").data(z.string()).meta(AuditActorSchema),
event.type("workspace:archived").meta(AuditActorSchema),
event.type("workspace:restored").meta(AuditActorSchema),
];

View File

@@ -0,0 +1,38 @@
import { makeDocumentParser } from "@platform/database/utilities.ts";
import { z } from "zod";
import { AvatarSchema } from "../value-objects/avatar.ts";
import { ContactSchema } from "../value-objects/contact.ts";
import { NameSchema } from "../value-objects/name.ts";
export const WorkspaceUserSchema = z.object({
id: z.uuid(),
workspaceId: z.uuid(),
identityId: z.string(),
name: NameSchema.optional(),
avatar: AvatarSchema.optional(),
contacts: z.array(ContactSchema).default([]),
createdAt: z.coerce.date(),
createdBy: z.string(),
updatedAt: z.coerce.date().optional(),
updatedBy: z.string().optional(),
});
/*
|--------------------------------------------------------------------------------
| Parsers
|--------------------------------------------------------------------------------
*/
export const parseWorkspaceUser = makeDocumentParser(WorkspaceUserSchema);
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type WorkspaceUser = z.infer<typeof WorkspaceUserSchema>;

View File

@@ -0,0 +1,32 @@
import { makeDocumentParser } from "@platform/database/utilities.ts";
import { z } from "zod";
export const WorkspaceSchema = z.object({
id: z.uuid(),
ownerId: z.uuid(),
name: z.string(),
description: z.string().optional(),
createdAt: z.coerce.date(),
createdBy: z.string(),
updatedAt: z.coerce.date().optional(),
updatedBy: z.string().optional(),
});
/*
|--------------------------------------------------------------------------------
| Parsers
|--------------------------------------------------------------------------------
*/
export const parseWorkspace = makeDocumentParser(WorkspaceSchema);
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type Workspace = z.infer<typeof WorkspaceSchema>;

View File

@@ -0,0 +1,19 @@
{
"name": "@modules/workspace",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
"./client.ts": "./client.ts",
"./server.ts": "./server.ts"
},
"types": "types.d.ts",
"dependencies": {
"@platform/database": "workspace:*",
"@platform/relay": "workspace:*",
"@platform/spec": "workspace:*",
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
"cookie": "1.0.2",
"zod": "4.1.11"
}
}

View File

@@ -0,0 +1,20 @@
import { ForbiddenError } from "@platform/relay";
import { Workspace } from "../../../aggregates/workspace.ts";
import { eventStore } from "../../../event-store.ts";
import route from "./spec.ts";
export default route.access("session").handle(async ({ body: { name } }, { access, principal }) => {
const decision = await access.isAllowed({ kind: "workspace", id: "1", attr: {} }, "create");
if (decision === false) {
return new ForbiddenError("You do not have permission to create workspaces.");
}
const workspace = await eventStore.aggregate.from(Workspace).create(principal.id, name).save();
return {
id: workspace.id,
ownerId: workspace.ownerId,
name: workspace.name,
createdAt: workspace.createdAt,
createdBy: principal.id,
};
});

View File

@@ -0,0 +1,14 @@
import { ForbiddenError, InternalServerError, route, UnauthorizedError, ValidationError } from "@platform/relay";
import z from "zod";
import { WorkspaceSchema } from "../../../models/workspace.ts";
export default route
.post("/api/v1/workspace")
.body(
z.strictObject({
name: z.string(),
}),
)
.errors([UnauthorizedError, ForbiddenError, ValidationError, InternalServerError])
.response(WorkspaceSchema);

View File

@@ -0,0 +1,30 @@
import { idIndex } from "@platform/database/id.ts";
import { register as registerReadStore } from "@platform/database/registrar.ts";
import { register as registerEventStore } from "@valkyr/event-store/mongo";
import { db } from "./database.ts";
import { eventStore } from "./event-store.ts";
export default {
routes: [(await import("./routes/workspaces/create/handle.ts")).default],
bootstrap: async (): Promise<void> => {
await registerReadStore(db.db, [
{
name: "workspaces",
indexes: [
idIndex,
// [{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
],
},
{
name: "workspace:users",
indexes: [
idIndex,
// [{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
],
},
]);
await registerEventStore(eventStore.db.db, console.info);
},
};

View File

@@ -0,0 +1,13 @@
import z from "zod";
import { EmailSchema } from "./email.ts";
export const ContactSchema = z.union([
z.object({
id: z.string(),
type: z.literal("email"),
email: EmailSchema,
}),
]);
export type Contact = z.infer<typeof ContactSchema>;

View File

@@ -1,47 +0,0 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: identity
version: default
rules:
### Read
- actions:
- read
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- read
effect: EFFECT_ALLOW
roles:
- user
condition:
match:
expr: request.resource.id == request.principal.id
### Update
- actions:
- update
effect: EFFECT_ALLOW
roles:
- user
condition:
match:
expr: request.resource.id == request.principal.id
### Delete
- actions:
- delete
effect: EFFECT_ALLOW
roles:
- user
condition:
match:
expr: request.resource.id == request.principal.id

View File

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

View File

@@ -286,7 +286,7 @@ export class UnprocessableContentError<TData = unknown> extends ServerError<TDat
} }
} }
export class ValidationError extends ServerError<ValidationErrorData> { export class ValidationError<TData = unknown> extends ServerError<TData> {
readonly code = "VALIDATION"; readonly code = "VALIDATION";
/** /**
@@ -298,7 +298,7 @@ export class ValidationError extends ServerError<ValidationErrorData> {
* @param message - Optional message to send with the error. Default: "Validation Failed". * @param message - Optional message to send with the error. Default: "Validation Failed".
* @param data - Data with validation failure details. * @param data - Data with validation failure details.
*/ */
constructor(message = "Validation Failed", data: ValidationErrorData) { constructor(message = "Validation Failed", data: TData) {
super(message, 422, data); super(message, 422, data);
} }
@@ -322,7 +322,7 @@ export class ValidationError extends ServerError<ValidationErrorData> {
message: issue.message, message: issue.message,
}; };
}), }),
}); } satisfies ValidationErrorData);
} }
} }

View File

@@ -10,8 +10,8 @@
"dependencies": { "dependencies": {
"@platform/auth": "workspace:*", "@platform/auth": "workspace:*",
"@platform/socket": "workspace:*", "@platform/socket": "workspace:*",
"@platform/supertokens": "workspace:*",
"@platform/vault": "workspace:*", "@platform/vault": "workspace:*",
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4",
"path-to-regexp": "8", "path-to-regexp": "8",
"zod": "4.1.11" "zod": "4.1.11"
} }

View File

@@ -1,9 +1,7 @@
import { cerbos } from "@platform/cerbos/client.ts"; import { cerbos } from "./cerbos.ts";
import { Resource } from "@platform/cerbos/resources.ts";
import type { Principal } from "./principal.ts"; import type { Principal } from "./principal.ts";
export function access(principal: Principal) { export function getAccessControlMethods(principal: Principal) {
return { return {
/** /**
* Check if a principal is allowed to perform an action on a resource. * Check if a principal is allowed to perform an action on a resource.
@@ -22,7 +20,7 @@ export function access(principal: Principal) {
* "view" * "view"
* ); // => true * ); // => true
*/ */
isAllowed(resource: Resource, action: string) { isAllowed(resource: any, action: string) {
return cerbos.isAllowed({ principal, resource, action }); return cerbos.isAllowed({ principal, resource, action });
}, },
@@ -45,7 +43,7 @@ export function access(principal: Principal) {
* *
* decision.isAllowed("view"); // => true * decision.isAllowed("view"); // => true
*/ */
checkResource(resource: Resource, actions: string[]) { checkResource(resource: any, actions: string[]) {
return cerbos.checkResource({ principal, resource, actions }); return cerbos.checkResource({ principal, resource, actions });
}, },
@@ -80,10 +78,10 @@ export function access(principal: Principal) {
* action: "view", * action: "view",
* }); // => true * }); // => true
*/ */
checkResources(resources: { resource: Resource; actions: string[] }[]) { checkResources(resources: { resource: any; actions: string[] }[]) {
return cerbos.checkResources({ principal, resources }); return cerbos.checkResources({ principal, resources });
}, },
}; };
} }
export type Access = ReturnType<typeof access>; export type AccessControlMethods = ReturnType<typeof getAccessControlMethods>;

View File

@@ -0,0 +1,44 @@
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,12 +1,13 @@
{ {
"name": "@platform/cerbos", "name": "@platform/supertoken",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@cerbos/http": "0.23.1", "@cerbos/http": "0.23.1",
"@platform/config": "workspace:*", "@platform/config": "workspace:*",
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4", "cookie": "1.0.2",
"supertokens-node": "23.0.1",
"zod": "4.1.11" "zod": "4.1.11"
} }
} }

View File

@@ -0,0 +1,46 @@
import UserMetadata from "supertokens-node/recipe/usermetadata";
import z from "zod";
/*
|--------------------------------------------------------------------------------
| Schema
|--------------------------------------------------------------------------------
*/
export const PrincipalSchema = z.object({
id: z.string(),
roles: z.array(z.string()),
attr: z.record(z.string(), z.any()),
});
/*
|--------------------------------------------------------------------------------
| Utilities
|--------------------------------------------------------------------------------
*/
/**
* 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 ?? {};
}
/*
|--------------------------------------------------------------------------------
| Types
|--------------------------------------------------------------------------------
*/
export type Principal = z.infer<typeof PrincipalSchema>;

View File

@@ -0,0 +1,133 @@
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

@@ -0,0 +1,37 @@
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,11 +1,18 @@
import "@platform/relay"; import "@platform/relay";
import "@platform/storage"; import "@platform/storage";
import type { Access } from "./auth/access.ts"; import type Session from "supertokens-node/recipe/session";
import type { Principal } from "./auth/principal.ts";
import type { AccessControlMethods } from "./access.ts";
import type { Principal } from "./principal.ts";
declare module "@platform/storage" { declare module "@platform/storage" {
interface StorageContext { interface StorageContext {
/**
* TODO ...
*/
session?: Session.SessionContainer;
/** /**
* TODO ... * TODO ...
*/ */
@@ -14,7 +21,7 @@ declare module "@platform/storage" {
/** /**
* TODO ... * TODO ...
*/ */
access?: Access; access?: AccessControlMethods;
} }
} }
@@ -25,6 +32,11 @@ declare module "@platform/relay" {
*/ */
isAuthenticated: boolean; isAuthenticated: boolean;
/**
* TODO ...
*/
session: Session.SessionContainer;
/** /**
* TODO ... * TODO ...
*/ */
@@ -33,6 +45,6 @@ declare module "@platform/relay" {
/** /**
* TODO ... * TODO ...
*/ */
access: Access; access: AccessControlMethods;
} }
} }

View File

@@ -0,0 +1,10 @@
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);
}