feat: add supertokens
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
meta {
|
||||
name: Get By ID
|
||||
name: Get
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/identities/:id
|
||||
url: {{url}}/identity/:id
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
params:path {
|
||||
id: 16b88034-ca82-4a8e-9fe5-13bd0dd29b75
|
||||
id:
|
||||
}
|
||||
|
||||
settings {
|
||||
32
.bruno/identity/Roles.bru
Normal file
32
.bruno/identity/Roles.bru
Normal 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
|
||||
}
|
||||
43
.bruno/identity/Update.bru
Normal file
43
.bruno/identity/Update.bru
Normal 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
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
meta {
|
||||
name: Code
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/identities/login/code/:identityId/code/:codeId/:value
|
||||
body: none
|
||||
post {
|
||||
url: {{url}}/identity/login/code
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
params:path {
|
||||
identityId: efefa471-905d-4702-bd0a-863d8cf70424
|
||||
codeId: 7055b769-0814-47b8-836e-cfef2d8c2e68
|
||||
value: 00597
|
||||
body:json {
|
||||
{
|
||||
"deviceId": "",
|
||||
"preAuthSessionId": "",
|
||||
"userInputCode": ""
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
meta {
|
||||
name: Email
|
||||
type: http
|
||||
seq: 1
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{url}}/identities/login/email
|
||||
url: {{url}}/identity/login/email
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
meta {
|
||||
name: Login
|
||||
seq: 2
|
||||
seq: 3
|
||||
}
|
||||
|
||||
auth {
|
||||
|
||||
21
.bruno/identity/login/sudo.bru
Normal file
21
.bruno/identity/login/sudo.bru
Normal 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
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
meta {
|
||||
name: Me
|
||||
type: http
|
||||
seq: 3
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{url}}/identities/me
|
||||
url: {{url}}/identity/me
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
21
.bruno/workspace/create.bru
Normal file
21
.bruno/workspace/create.bru
Normal 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
|
||||
}
|
||||
8
.bruno/workspace/folder.bru
Normal file
8
.bruno/workspace/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Workspace
|
||||
seq: 2
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -27,6 +27,4 @@
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true
|
||||
},
|
||||
"vue.format.style.initialIndent": true,
|
||||
"vue.format.script.initialIndent": true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import identity from "@modules/identity/server.ts";
|
||||
import workspace from "@modules/workspace/server.ts";
|
||||
import database from "@platform/database/server.ts";
|
||||
import { logger } from "@platform/logger";
|
||||
import { context } from "@platform/relay";
|
||||
@@ -6,6 +7,7 @@ import { Api } from "@platform/server/api.ts";
|
||||
import server from "@platform/server/server.ts";
|
||||
import socket from "@platform/socket/server.ts";
|
||||
import { storage } from "@platform/storage";
|
||||
import supertokens from "@platform/supertoken/server.ts";
|
||||
|
||||
import { config } from "./config.ts";
|
||||
|
||||
@@ -22,10 +24,11 @@ const log = logger.prefix("Server");
|
||||
await database.bootstrap();
|
||||
await server.bootstrap();
|
||||
await socket.bootstrap();
|
||||
await supertokens.bootsrap();
|
||||
|
||||
// ### 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 socket.resolve();
|
||||
|
||||
await identity.resolve(request);
|
||||
await supertokens.resolve(request);
|
||||
|
||||
// ### Fetch
|
||||
// Execute fetch against the api instance.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"api",
|
||||
"apps/react",
|
||||
"modules/identity",
|
||||
"platform/cerbos",
|
||||
"modules/workspace",
|
||||
"platform/config",
|
||||
"platform/database",
|
||||
"platform/logger",
|
||||
@@ -14,12 +14,14 @@
|
||||
"platform/socket",
|
||||
"platform/spec",
|
||||
"platform/storage",
|
||||
"platform/supertoken",
|
||||
"platform/vault"
|
||||
],
|
||||
"imports": {
|
||||
"@modules/identity/client.ts": "./modules/identity/client.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/database/": "./platform/database/",
|
||||
"@platform/logger": "./platform/logger/mod.ts",
|
||||
@@ -28,6 +30,7 @@
|
||||
"@platform/socket/": "./platform/socket/",
|
||||
"@platform/spec/": "./platform/spec/",
|
||||
"@platform/storage": "./platform/storage/storage.ts",
|
||||
"@platform/supertoken/": "./platform/supertoken/",
|
||||
"@platform/vault": "./platform/vault/vault.ts"
|
||||
},
|
||||
"tasks": {
|
||||
|
||||
411
deno.lock
generated
411
deno.lock
generated
@@ -3,24 +3,21 @@
|
||||
"specifiers": {
|
||||
"npm:@cerbos/http@0.23.1": "0.23.1",
|
||||
"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__dotenv@0.225.5": "0.225.5",
|
||||
"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__event-emitter@1.0.1": "1.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__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-router-devtools@1.131.47": "1.131.47_@tanstack+react-router@1.131.47__react@19.1.1__react-dom@19.1.1___react@19.1.1_react@19.1.1_react-dom@19.1.1__react@19.1.1",
|
||||
"npm:@tanstack/react-router@1.131.47": "1.131.47_react@19.1.1_react-dom@19.1.1__react@19.1.1",
|
||||
"npm:@types/node@*": "24.2.0",
|
||||
"npm:@types/react-dom@19.1.9": "19.1.9_@types+react@19.1.13",
|
||||
"npm:@types/react@19.1.13": "19.1.13",
|
||||
"npm:@vitejs/plugin-react@4.7.0": "4.7.0_vite@7.1.6__picomatch@4.0.3_@babel+core@7.28.4_@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: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",
|
||||
@@ -35,10 +32,11 @@
|
||||
"npm:prettier@3.6.2": "3.6.2",
|
||||
"npm:react-dom@19.1.1": "19.1.1_react@19.1.1",
|
||||
"npm:react@19.1.1": "19.1.1",
|
||||
"npm:supertokens-node@23.0.1": "23.0.1",
|
||||
"npm:tailwindcss@4.1.13": "4.1.13",
|
||||
"npm:typescript-eslint@8.44.0": "8.44.0_eslint@9.35.0_typescript@5.9.2_@typescript-eslint+parser@8.44.0__eslint@9.35.0__typescript@5.9.2",
|
||||
"npm:typescript@5.9.2": "5.9.2",
|
||||
"npm:vite@7.1.6": "7.1.6_picomatch@4.0.3_@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": {
|
||||
@@ -425,23 +423,6 @@
|
||||
"@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": {
|
||||
"integrity": "sha512-BcjBimpxuy7mXjWo7sZ3TtPitx91w3UqssyY92RmJIuoMGYywZRGxaxqK9/oybljbZbZpPOSrkgQI9wKpgZ9vQ==",
|
||||
"dependencies": [
|
||||
@@ -464,14 +445,6 @@
|
||||
"integrity": "sha512-qrBt3wfQgvXbjo+Up6lyzBGxk0IPhDqW9Jx7CJQUQpsxqhoqnBmD8gn0Mt8i+RHHI9uZFCO+FP122ClAC8yljg==",
|
||||
"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": {
|
||||
"integrity": "sha512-TEjyE8g+46jPlu7dJHLrwc8NMGl8zfG+JjWxyNQyDbxP0RtqZ4JmYZfR9vy4RWYWJQbLpw6Kbt2n+K/2zAO/JA==",
|
||||
"dependencies": [
|
||||
@@ -507,14 +480,6 @@
|
||||
],
|
||||
"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": {
|
||||
"integrity": "sha512-0gIauba+vQW6ssqMACLO1Z/METlhzoX+y4t9Sawh/IafQ986Rgvp6gCI+WArp7vbsO5hpItixrqjkxnnNC+h5g==",
|
||||
"dependencies": [
|
||||
@@ -800,16 +765,7 @@
|
||||
"@tailwindcss/node",
|
||||
"@tailwindcss/oxide",
|
||||
"tailwindcss",
|
||||
"vite@7.1.6_picomatch@4.0.3"
|
||||
]
|
||||
},
|
||||
"@tailwindcss/vite@4.1.13_vite@7.1.6__picomatch@4.0.3_@types+node@24.2.0": {
|
||||
"integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==",
|
||||
"dependencies": [
|
||||
"@tailwindcss/node",
|
||||
"@tailwindcss/oxide",
|
||||
"tailwindcss",
|
||||
"vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0"
|
||||
"vite"
|
||||
]
|
||||
},
|
||||
"@tanstack/history@1.131.2": {
|
||||
@@ -916,12 +872,6 @@
|
||||
"@types/json-schema@7.0.15": {
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
|
||||
},
|
||||
"@types/node@24.2.0": {
|
||||
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
||||
"dependencies": [
|
||||
"undici-types"
|
||||
]
|
||||
},
|
||||
"@types/react-dom@19.1.9_@types+react@19.1.13": {
|
||||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||
"dependencies": [
|
||||
@@ -1052,19 +1002,7 @@
|
||||
"@rolldown/pluginutils",
|
||||
"@types/babel__core",
|
||||
"react-refresh",
|
||||
"vite@7.1.6_picomatch@4.0.3"
|
||||
]
|
||||
},
|
||||
"@vitejs/plugin-react@4.7.0_vite@7.1.6__picomatch@4.0.3_@babel+core@7.28.4_@types+node@24.2.0": {
|
||||
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
||||
"dependencies": [
|
||||
"@babel/core",
|
||||
"@babel/plugin-transform-react-jsx-self",
|
||||
"@babel/plugin-transform-react-jsx-source",
|
||||
"@rolldown/pluginutils",
|
||||
"@types/babel__core",
|
||||
"react-refresh",
|
||||
"vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0"
|
||||
"vite"
|
||||
]
|
||||
},
|
||||
"acorn-jsx@5.3.2_acorn@8.15.0": {
|
||||
@@ -1077,6 +1015,12 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"bin": true
|
||||
},
|
||||
"agent-base@6.0.2": {
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dependencies": [
|
||||
"debug"
|
||||
]
|
||||
},
|
||||
"ajv@6.12.6": {
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dependencies": [
|
||||
@@ -1095,9 +1039,23 @@
|
||||
"argparse@2.0.1": {
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"asynckit@0.4.0": {
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios@1.11.0": {
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"dependencies": [
|
||||
"follow-redirects",
|
||||
"form-data",
|
||||
"proxy-from-env"
|
||||
]
|
||||
},
|
||||
"balanced-match@1.0.2": {
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"base64-js@1.5.1": {
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"baseline-browser-mapping@2.8.6": {
|
||||
"integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
|
||||
"bin": true
|
||||
@@ -1135,6 +1093,16 @@
|
||||
"bson@6.10.4": {
|
||||
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="
|
||||
},
|
||||
"buffer-equal-constant-time@1.0.1": {
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||
},
|
||||
"buffer@6.0.3": {
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"dependencies": [
|
||||
"base64-js",
|
||||
"ieee754"
|
||||
]
|
||||
},
|
||||
"call-bind-apply-helpers@1.0.2": {
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dependencies": [
|
||||
@@ -1177,18 +1145,36 @@
|
||||
"color-name@1.1.4": {
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"combined-stream@1.0.8": {
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": [
|
||||
"delayed-stream"
|
||||
]
|
||||
},
|
||||
"concat-map@0.0.1": {
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"content-type@1.0.5": {
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
|
||||
},
|
||||
"convert-source-map@2.0.0": {
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"cookie-es@1.2.2": {
|
||||
"integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="
|
||||
},
|
||||
"cookie@0.7.2": {
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
|
||||
},
|
||||
"cookie@1.0.2": {
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="
|
||||
},
|
||||
"cross-fetch@3.2.0": {
|
||||
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
|
||||
"dependencies": [
|
||||
"node-fetch"
|
||||
]
|
||||
},
|
||||
"cross-spawn@7.0.6": {
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dependencies": [
|
||||
@@ -1197,9 +1183,15 @@
|
||||
"which"
|
||||
]
|
||||
},
|
||||
"crypto-js@4.2.0": {
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"csstype@3.1.3": {
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"dayjs@1.11.18": {
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
|
||||
},
|
||||
"debug@4.4.3": {
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dependencies": [
|
||||
@@ -1209,6 +1201,9 @@
|
||||
"deep-is@0.1.4": {
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
|
||||
},
|
||||
"delayed-stream@1.0.0": {
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"detect-libc@2.1.0": {
|
||||
"integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg=="
|
||||
},
|
||||
@@ -1220,6 +1215,12 @@
|
||||
"gopd"
|
||||
]
|
||||
},
|
||||
"ecdsa-sig-formatter@1.0.11": {
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"dependencies": [
|
||||
"safe-buffer"
|
||||
]
|
||||
},
|
||||
"electron-to-chromium@1.5.222": {
|
||||
"integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w=="
|
||||
},
|
||||
@@ -1242,6 +1243,15 @@
|
||||
"es-errors"
|
||||
]
|
||||
},
|
||||
"es-set-tostringtag@2.1.0": {
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dependencies": [
|
||||
"es-errors",
|
||||
"get-intrinsic",
|
||||
"has-tostringtag",
|
||||
"hasown"
|
||||
]
|
||||
},
|
||||
"esbuild@0.25.10": {
|
||||
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
|
||||
"optionalDependencies": [
|
||||
@@ -1448,6 +1458,19 @@
|
||||
"flatted@3.3.3": {
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="
|
||||
},
|
||||
"follow-redirects@1.15.11": {
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
|
||||
},
|
||||
"form-data@4.0.4": {
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dependencies": [
|
||||
"asynckit",
|
||||
"combined-stream",
|
||||
"es-set-tostringtag",
|
||||
"hasown",
|
||||
"mime-types"
|
||||
]
|
||||
},
|
||||
"fsevents@2.3.3": {
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"os": ["darwin"],
|
||||
@@ -1520,15 +1543,31 @@
|
||||
"has-symbols@1.1.0": {
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
|
||||
},
|
||||
"has-tostringtag@1.0.2": {
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dependencies": [
|
||||
"has-symbols"
|
||||
]
|
||||
},
|
||||
"hasown@2.0.2": {
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dependencies": [
|
||||
"function-bind"
|
||||
]
|
||||
},
|
||||
"https-proxy-agent@5.0.1": {
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dependencies": [
|
||||
"agent-base",
|
||||
"debug"
|
||||
]
|
||||
},
|
||||
"idb@8.0.3": {
|
||||
"integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="
|
||||
},
|
||||
"ieee754@1.2.1": {
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"ignore@5.3.2": {
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="
|
||||
},
|
||||
@@ -1567,6 +1606,9 @@
|
||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"bin": true
|
||||
},
|
||||
"jose@4.15.9": {
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="
|
||||
},
|
||||
"jose@6.1.0": {
|
||||
"integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="
|
||||
},
|
||||
@@ -1597,6 +1639,36 @@
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"bin": true
|
||||
},
|
||||
"jsonwebtoken@9.0.2": {
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"dependencies": [
|
||||
"jws",
|
||||
"lodash.includes",
|
||||
"lodash.isboolean",
|
||||
"lodash.isinteger",
|
||||
"lodash.isnumber",
|
||||
"lodash.isplainobject",
|
||||
"lodash.isstring",
|
||||
"lodash.once",
|
||||
"ms",
|
||||
"semver@7.7.2"
|
||||
]
|
||||
},
|
||||
"jwa@1.4.2": {
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"dependencies": [
|
||||
"buffer-equal-constant-time",
|
||||
"ecdsa-sig-formatter",
|
||||
"safe-buffer"
|
||||
]
|
||||
},
|
||||
"jws@3.2.2": {
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"dependencies": [
|
||||
"jwa",
|
||||
"safe-buffer"
|
||||
]
|
||||
},
|
||||
"keyv@4.5.4": {
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dependencies": [
|
||||
@@ -1610,6 +1682,9 @@
|
||||
"type-check"
|
||||
]
|
||||
},
|
||||
"libphonenumber-js@1.12.21": {
|
||||
"integrity": "sha512-z/o0jBYS3d8js1QBksrHxZUARjmM0S6uvpINkyJ9IcPkXIoUh5l4S3rTbGAlq9ThbCExdSV0wWp8gw7729A/ww=="
|
||||
},
|
||||
"lightningcss-darwin-arm64@1.30.1": {
|
||||
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
||||
"os": ["darwin"],
|
||||
@@ -1684,9 +1759,30 @@
|
||||
"p-locate"
|
||||
]
|
||||
},
|
||||
"lodash.includes@4.3.0": {
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"lodash.isboolean@3.0.3": {
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||
},
|
||||
"lodash.isinteger@4.0.4": {
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
},
|
||||
"lodash.isnumber@3.0.3": {
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||
},
|
||||
"lodash.isplainobject@4.0.6": {
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||
},
|
||||
"lodash.isstring@4.0.1": {
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||
},
|
||||
"lodash.merge@4.6.2": {
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.once@4.1.1": {
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
},
|
||||
"lru-cache@5.1.1": {
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dependencies": [
|
||||
@@ -1715,6 +1811,15 @@
|
||||
"picomatch@2.3.1"
|
||||
]
|
||||
},
|
||||
"mime-db@1.52.0": {
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types@2.1.35": {
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": [
|
||||
"mime-db"
|
||||
]
|
||||
},
|
||||
"mingo@6.6.1": {
|
||||
"integrity": "sha512-KC6b1ODYoSdYu5fBm+SzQb7fa4ARmGwfa3Cf9F7U+2mnfD4Zhf89qQgO1cPTtaJ68w3ntIT5dVujgF52HvN7+g=="
|
||||
},
|
||||
@@ -1733,21 +1838,17 @@
|
||||
"minipass@7.1.2": {
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
|
||||
},
|
||||
"minizlib@3.0.2": {
|
||||
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
||||
"minizlib@3.1.0": {
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"dependencies": [
|
||||
"minipass"
|
||||
]
|
||||
},
|
||||
"mkdirp@3.0.1": {
|
||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||
"bin": true
|
||||
},
|
||||
"mongodb-connection-string-url@3.0.2": {
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"dependencies": [
|
||||
"@types/whatwg-url",
|
||||
"whatwg-url"
|
||||
"whatwg-url@14.2.0"
|
||||
]
|
||||
},
|
||||
"mongodb@6.20.0": {
|
||||
@@ -1772,9 +1873,18 @@
|
||||
"natural-compare@1.4.0": {
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
|
||||
},
|
||||
"node-fetch@2.7.0": {
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": [
|
||||
"whatwg-url@5.0.0"
|
||||
]
|
||||
},
|
||||
"node-releases@2.0.21": {
|
||||
"integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw=="
|
||||
},
|
||||
"nodemailer@6.10.1": {
|
||||
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA=="
|
||||
},
|
||||
"object-inspect@1.13.4": {
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
|
||||
},
|
||||
@@ -1801,6 +1911,9 @@
|
||||
"p-limit"
|
||||
]
|
||||
},
|
||||
"pako@2.1.0": {
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||
},
|
||||
"parent-module@1.0.1": {
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"dependencies": [
|
||||
@@ -1825,6 +1938,12 @@
|
||||
"picomatch@4.0.3": {
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
|
||||
},
|
||||
"pkce-challenge@3.1.0": {
|
||||
"integrity": "sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==",
|
||||
"dependencies": [
|
||||
"crypto-js"
|
||||
]
|
||||
},
|
||||
"postcss@8.5.6": {
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dependencies": [
|
||||
@@ -1843,6 +1962,12 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"bin": true
|
||||
},
|
||||
"process@0.11.10": {
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
|
||||
},
|
||||
"proxy-from-env@1.1.0": {
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"punycode@2.3.1": {
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
||||
},
|
||||
@@ -1852,6 +1977,9 @@
|
||||
"side-channel"
|
||||
]
|
||||
},
|
||||
"querystringify@2.2.0": {
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"queue-microtask@1.2.3": {
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
|
||||
},
|
||||
@@ -1868,6 +1996,9 @@
|
||||
"react@19.1.1": {
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="
|
||||
},
|
||||
"requires-port@1.0.0": {
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
},
|
||||
"resolve-from@4.0.0": {
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
|
||||
},
|
||||
@@ -1918,9 +2049,15 @@
|
||||
"tslib"
|
||||
]
|
||||
},
|
||||
"safe-buffer@5.2.1": {
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"scheduler@0.26.0": {
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
|
||||
},
|
||||
"scmp@2.1.0": {
|
||||
"integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="
|
||||
},
|
||||
"semver@6.3.1": {
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": true
|
||||
@@ -1938,6 +2075,9 @@
|
||||
"seroval@1.3.2": {
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="
|
||||
},
|
||||
"set-cookie-parser@2.7.1": {
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||
},
|
||||
"shebang-command@2.0.0": {
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dependencies": [
|
||||
@@ -2003,6 +2143,29 @@
|
||||
"strip-json-comments@3.1.1": {
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
|
||||
},
|
||||
"supertokens-js-override@0.0.4": {
|
||||
"integrity": "sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg=="
|
||||
},
|
||||
"supertokens-node@23.0.1": {
|
||||
"integrity": "sha512-cCuY9Y5Mj93Pg1ktbqilouWgAoQWniQauftB4Ef6rfOchogx13XTo1pNP14zezn2rSf7WIPb9iaZb5zif6TKtQ==",
|
||||
"dependencies": [
|
||||
"buffer",
|
||||
"content-type",
|
||||
"cookie@0.7.2",
|
||||
"cross-fetch",
|
||||
"debug",
|
||||
"jose@4.15.9",
|
||||
"libphonenumber-js",
|
||||
"nodemailer",
|
||||
"pako",
|
||||
"pkce-challenge",
|
||||
"process",
|
||||
"set-cookie-parser",
|
||||
"supertokens-js-override",
|
||||
"tldts",
|
||||
"twilio"
|
||||
]
|
||||
},
|
||||
"supports-color@7.2.0": {
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dependencies": [
|
||||
@@ -2015,14 +2178,13 @@
|
||||
"tapable@2.2.3": {
|
||||
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="
|
||||
},
|
||||
"tar@7.4.3": {
|
||||
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||
"tar@7.4.4": {
|
||||
"integrity": "sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==",
|
||||
"dependencies": [
|
||||
"@isaacs/fs-minipass",
|
||||
"chownr",
|
||||
"minipass",
|
||||
"minizlib",
|
||||
"mkdirp",
|
||||
"yallist@5.0.0"
|
||||
]
|
||||
},
|
||||
@@ -2039,12 +2201,25 @@
|
||||
"picomatch@4.0.3"
|
||||
]
|
||||
},
|
||||
"tldts-core@6.1.86": {
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="
|
||||
},
|
||||
"tldts@6.1.86": {
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dependencies": [
|
||||
"tldts-core"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"to-regex-range@5.0.1": {
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dependencies": [
|
||||
"is-number"
|
||||
]
|
||||
},
|
||||
"tr46@0.0.3": {
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"tr46@5.1.1": {
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dependencies": [
|
||||
@@ -2060,6 +2235,19 @@
|
||||
"tslib@2.8.1": {
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"twilio@4.23.0": {
|
||||
"integrity": "sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==",
|
||||
"dependencies": [
|
||||
"axios",
|
||||
"dayjs",
|
||||
"https-proxy-agent",
|
||||
"jsonwebtoken",
|
||||
"qs",
|
||||
"scmp",
|
||||
"url-parse",
|
||||
"xmlbuilder"
|
||||
]
|
||||
},
|
||||
"type-check@0.4.0": {
|
||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||
"dependencies": [
|
||||
@@ -2081,9 +2269,6 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"bin": true
|
||||
},
|
||||
"undici-types@7.10.0": {
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
|
||||
},
|
||||
"update-browserslist-db@1.1.3_browserslist@4.26.2": {
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dependencies": [
|
||||
@@ -2099,6 +2284,13 @@
|
||||
"punycode"
|
||||
]
|
||||
},
|
||||
"url-parse@1.5.10": {
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dependencies": [
|
||||
"querystringify",
|
||||
"requires-port"
|
||||
]
|
||||
},
|
||||
"use-sync-external-store@1.5.0_react@19.1.1": {
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"dependencies": [
|
||||
@@ -2124,24 +2316,8 @@
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"vite@7.1.6_picomatch@4.0.3_@types+node@24.2.0": {
|
||||
"integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==",
|
||||
"dependencies": [
|
||||
"@types/node",
|
||||
"esbuild",
|
||||
"fdir",
|
||||
"picomatch@4.0.3",
|
||||
"postcss",
|
||||
"rollup",
|
||||
"tinyglobby"
|
||||
],
|
||||
"optionalDependencies": [
|
||||
"fsevents"
|
||||
],
|
||||
"optionalPeers": [
|
||||
"@types/node"
|
||||
],
|
||||
"bin": true
|
||||
"webidl-conversions@3.0.1": {
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"webidl-conversions@7.0.0": {
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||
@@ -2149,8 +2325,15 @@
|
||||
"whatwg-url@14.2.0": {
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dependencies": [
|
||||
"tr46",
|
||||
"webidl-conversions"
|
||||
"tr46@5.1.1",
|
||||
"webidl-conversions@7.0.0"
|
||||
]
|
||||
},
|
||||
"whatwg-url@5.0.0": {
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": [
|
||||
"tr46@0.0.3",
|
||||
"webidl-conversions@3.0.1"
|
||||
]
|
||||
},
|
||||
"which@2.0.2": {
|
||||
@@ -2163,6 +2346,9 @@
|
||||
"word-wrap@1.2.5": {
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="
|
||||
},
|
||||
"xmlbuilder@13.0.2": {
|
||||
"integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ=="
|
||||
},
|
||||
"yallist@3.1.1": {
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
},
|
||||
@@ -2226,20 +2412,16 @@
|
||||
"modules/identity": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@cerbos/http@0.23.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:supertokens-node@23.0.1",
|
||||
"npm:zod@4.1.11"
|
||||
]
|
||||
}
|
||||
},
|
||||
"platform/cerbos": {
|
||||
"modules/workspace": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@cerbos/http@0.23.1",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -2272,7 +2454,6 @@
|
||||
"platform/relay": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@jsr/valkyr__auth@2.1.4",
|
||||
"npm:path-to-regexp@8",
|
||||
"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": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
services:
|
||||
cerbos:
|
||||
container_name: cerbos
|
||||
image: ghcr.io/cerbos/cerbos:latest
|
||||
command: ["server", "--config=/config.yaml"] # <--- ensure config is used
|
||||
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
|
||||
|
||||
# MongoDB
|
||||
# --------------------------------------------------------------------------------
|
||||
# Used by event store and read store for managing and reading application data.
|
||||
|
||||
mongo:
|
||||
image: mongo:8
|
||||
@@ -26,6 +17,68 @@ services:
|
||||
networks:
|
||||
- localdev
|
||||
|
||||
# Super Tokens Database
|
||||
# --------------------------------------------------------------------------------
|
||||
# Used by supertokens instance to store and manage principals.
|
||||
|
||||
stdb:
|
||||
image: 'postgres:latest'
|
||||
environment:
|
||||
POSTGRES_USER: supertokens_user
|
||||
POSTGRES_PASSWORD: somePassword
|
||||
POSTGRES_DB: supertokens
|
||||
ports:
|
||||
- 5432:5432
|
||||
networks:
|
||||
- localdev
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'supertokens_user', '-d', 'supertokens']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Super Tokens
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
supertokens:
|
||||
image: registry.supertokens.io/supertokens/supertokens-postgresql:latest
|
||||
depends_on:
|
||||
stdb:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- 3567:3567
|
||||
environment:
|
||||
POSTGRESQL_CONNECTION_URI: "postgresql://supertokens_user:somePassword@stdb:5432/supertokens"
|
||||
networks:
|
||||
- localdev
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: >
|
||||
bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"'
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Cerbos
|
||||
# --------------------------------------------------------------------------------
|
||||
# Policy engine for application access control.
|
||||
|
||||
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:
|
||||
localdev:
|
||||
driver: bridge
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 } } });
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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;
|
||||
23
modules/identity/cerbos/policies/identity.yaml
Normal file
23
modules/identity/cerbos/policies/identity.yaml
Normal 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
|
||||
14
modules/identity/cerbos/policies/role.yaml
Normal file
14
modules/identity/cerbos/policies/role.yaml
Normal 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"]
|
||||
@@ -1,14 +1,11 @@
|
||||
import { ResourceRegistry } from "@valkyr/auth";
|
||||
|
||||
/*
|
||||
export const resources = new ResourceRegistry([
|
||||
{
|
||||
kind: "identity",
|
||||
attr: {},
|
||||
},
|
||||
{
|
||||
kind: "workspace",
|
||||
actions: ["read", "update", "delete"],
|
||||
attr: {},
|
||||
},
|
||||
] as const);
|
||||
|
||||
export type Resource = typeof resources.$resource;
|
||||
*/
|
||||
@@ -2,11 +2,10 @@ import { HttpAdapter, makeClient } from "@platform/relay";
|
||||
|
||||
import { config } from "./config.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 loginByEmail from "./routes/login/email/spec.ts";
|
||||
import loginByCode from "./routes/login/password/spec.ts";
|
||||
import me from "./routes/me/spec.ts";
|
||||
|
||||
export const identity = makeClient(
|
||||
{
|
||||
@@ -15,11 +14,6 @@ export const identity = makeClient(
|
||||
}),
|
||||
},
|
||||
{
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
register,
|
||||
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { getEnvironmentVariable } from "@platform/config/environment.ts";
|
||||
import type { SerializeOptions } from "cookie";
|
||||
import z from "zod";
|
||||
|
||||
export const config = {
|
||||
@@ -11,49 +7,4 @@ export const config = {
|
||||
type: z.url(),
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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),
|
||||
];
|
||||
@@ -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>;
|
||||
@@ -7,22 +7,11 @@
|
||||
"./client.ts": "./client.ts",
|
||||
"./server.ts": "./server.ts"
|
||||
},
|
||||
"types": "types.d.ts",
|
||||
"dependencies": {
|
||||
"@cerbos/http": "0.23.1",
|
||||
"@felix/bcrypt": "npm:@jsr/felix__bcrypt@1.0.5",
|
||||
"@platform/cerbos": "workspace:*",
|
||||
"@platform/config": "workspace:*",
|
||||
"@platform/database": "workspace:*",
|
||||
"@platform/logger": "workspace:*",
|
||||
"@platform/relay": "workspace:*",
|
||||
"@platform/server": "workspace:*",
|
||||
"@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",
|
||||
"supertokens-node": "23.0.1",
|
||||
"zod": "4.1.11"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
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";
|
||||
|
||||
export default route.access("session").handle(async ({ params: { id } }, { access }) => {
|
||||
const identity = await getIdentityById(id);
|
||||
if (identity === undefined) {
|
||||
const user = await getUserById(id);
|
||||
if (user === undefined) {
|
||||
return new NotFoundError("Identity does not exist, or has been removed.");
|
||||
}
|
||||
const decision = await access.isAllowed({ kind: "identity", id: identity.id, attr: {} }, "read");
|
||||
const decision = await access.isAllowed({ kind: "identity", id: user.id, attr: {} }, "read");
|
||||
if (decision === false) {
|
||||
return new ForbiddenError("You do not have permission to view this identity.");
|
||||
}
|
||||
return identity;
|
||||
return {
|
||||
id: user.id,
|
||||
roles: await getPrincipalRoles(id),
|
||||
attr: await getPrincipalAttributes(id),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ForbiddenError, NotFoundError, route, UnauthorizedError } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
import { IdentitySchema } from "../../../models/identity.ts";
|
||||
|
||||
export default route
|
||||
.get("/api/v1/identities/:id")
|
||||
.get("/api/v1/identity/:id")
|
||||
.params({
|
||||
id: z.string(),
|
||||
})
|
||||
.errors([UnauthorizedError, ForbiddenError, NotFoundError])
|
||||
.response(IdentitySchema);
|
||||
.response(z.any());
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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]);
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { importVault } from "@platform/vault";
|
||||
|
||||
import { config } from "../../../config.ts";
|
||||
|
||||
export const vault = importVault(config.internal);
|
||||
@@ -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]);
|
||||
40
modules/identity/routes/identities/update/handle.ts
Normal file
40
modules/identity/routes/identities/update/handle.ts
Normal 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 });
|
||||
});
|
||||
29
modules/identity/routes/identities/update/spec.ts
Normal file
29
modules/identity/routes/identities/update/spec.ts
Normal 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]);
|
||||
@@ -1,85 +1,25 @@
|
||||
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";
|
||||
|
||||
export default route.access("public").handle(async ({ params: { identityId, codeId, value }, query: { next } }) => {
|
||||
const code = await eventStore.aggregate.getByStream(Code, codeId);
|
||||
export default route.access("public").handle(async ({ body: { preAuthSessionId, deviceId, userInputCode } }) => {
|
||||
const response = await Passwordless.consumeCode({ tenantId: "public", preAuthSessionId, deviceId, userInputCode });
|
||||
if (response.status !== "OK") {
|
||||
return new NotFoundError();
|
||||
}
|
||||
|
||||
if (code === undefined) {
|
||||
return logger.info({
|
||||
logger.info({
|
||||
type: "code:claimed",
|
||||
session: false,
|
||||
message: "Invalid Code ID",
|
||||
received: codeId,
|
||||
session: true,
|
||||
message: "Identity resolved",
|
||||
user: response.user.toJson(),
|
||||
});
|
||||
}
|
||||
|
||||
if (code.claimedAt !== undefined) {
|
||||
return logger.info({
|
||||
type: "code:claimed",
|
||||
session: false,
|
||||
message: "Code Already Claimed",
|
||||
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, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"set-cookie": cookie.serialize("token", await auth.generate({ id: account.id }, "1 week"), options),
|
||||
},
|
||||
headers: await getSessionHeaders("public", response.recipeUserId),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,12 +2,14 @@ import { route } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
export default route
|
||||
.get("/api/v1/identities/login/code/:identityId/code/:codeId/:value")
|
||||
.params({
|
||||
identityId: z.string(),
|
||||
codeId: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
.post("/api/v1/identity/login/code")
|
||||
.body(
|
||||
z.strictObject({
|
||||
deviceId: z.string(),
|
||||
preAuthSessionId: z.string(),
|
||||
userInputCode: z.string(),
|
||||
}),
|
||||
)
|
||||
.query({
|
||||
next: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
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";
|
||||
|
||||
export default route.access("public").handle(async ({ body: { base, email } }) => {
|
||||
const identity = await eventStore.aggregate.getByRelation(Identity, getIdentityEmailRelation(email));
|
||||
if (identity === undefined) {
|
||||
export default route.access("public").handle(async ({ body: { email } }) => {
|
||||
const response = await Passwordless.createCode({ tenantId: "public", email });
|
||||
if (response.status !== "OK") {
|
||||
return logger.info({
|
||||
type: "auth:email",
|
||||
code: false,
|
||||
message: "Identity Not Found",
|
||||
type: "auth:passwordless",
|
||||
message: "Create code failed.",
|
||||
received: email,
|
||||
});
|
||||
}
|
||||
const code = await eventStore.aggregate.from(Code).create({ id: identity.id }).save();
|
||||
logger.info({
|
||||
type: "auth:email",
|
||||
type: "auth:passwordless",
|
||||
data: {
|
||||
code: code.id,
|
||||
identityId: identity.id,
|
||||
deviceId: response.deviceId,
|
||||
preAuthSessionId: response.preAuthSessionId,
|
||||
userInputCode: response.userInputCode,
|
||||
},
|
||||
link: `${base}/api/v1/admin/auth/${identity.id}/code/${code.id}/${code.value}?next=${base}/admin`,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { route } from "@platform/relay";
|
||||
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({
|
||||
base: z.url(),
|
||||
email: z.email(),
|
||||
}),
|
||||
);
|
||||
|
||||
48
modules/identity/routes/login/sudo/handle.ts
Normal file
48
modules/identity/routes/login/sudo/handle.ts
Normal 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),
|
||||
});
|
||||
});
|
||||
8
modules/identity/routes/login/sudo/spec.ts
Normal file
8
modules/identity/routes/login/sudo/spec.ts
Normal 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(),
|
||||
}),
|
||||
);
|
||||
5
modules/identity/routes/me/handle.ts
Normal file
5
modules/identity/routes/me/handle.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import route from "./spec.ts";
|
||||
|
||||
export default route.access("session").handle(async ({ principal }) => {
|
||||
return principal;
|
||||
});
|
||||
4
modules/identity/routes/me/spec.ts
Normal file
4
modules/identity/routes/me/spec.ts
Normal 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());
|
||||
30
modules/identity/routes/roles/handle.ts
Normal file
30
modules/identity/routes/roles/handle.ts
Normal 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) });
|
||||
});
|
||||
19
modules/identity/routes/roles/spec.ts
Normal file
19
modules/identity/routes/roles/spec.ts
Normal 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]);
|
||||
@@ -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>;
|
||||
@@ -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>;
|
||||
@@ -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>;
|
||||
@@ -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 {
|
||||
routes: [
|
||||
(await import("./routes/identities/get/handle.ts")).default,
|
||||
(await import("./routes/identities/register/handle.ts")).default,
|
||||
(await import("./routes/identities/me/handle.ts")).default,
|
||||
(await import("./routes/identities/resolve/handle.ts")).default,
|
||||
(await import("./routes/identities/update/handle.ts")).default,
|
||||
(await import("./routes/login/code/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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
66
modules/workspace/aggregates/workspace-user.ts
Normal file
66
modules/workspace/aggregates/workspace-user.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
120
modules/workspace/aggregates/workspace.ts
Normal file
120
modules/workspace/aggregates/workspace.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
33
modules/workspace/cerbos/policies/workspace.yaml
Normal file
33
modules/workspace/cerbos/policies/workspace.yaml
Normal 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
|
||||
@@ -3,13 +3,47 @@
|
||||
|
||||
apiVersion: api.cerbos.dev/v1
|
||||
resourcePolicy:
|
||||
resource: workspace
|
||||
resource: workspace_user
|
||||
version: default
|
||||
rules:
|
||||
|
||||
### Read
|
||||
# Admins can invite new members into their own workspace
|
||||
|
||||
- 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
|
||||
effect: EFFECT_ALLOW
|
||||
roles:
|
||||
@@ -17,26 +51,4 @@ resourcePolicy:
|
||||
- user
|
||||
condition:
|
||||
match:
|
||||
expr: request.principal.workspaceIds.includes(request.resource.id)
|
||||
|
||||
### 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)
|
||||
expr: request.principal.workspaceIds.includes(request.resource.workspaceId)
|
||||
22
modules/workspace/cerbos/resources.ts
Normal file
22
modules/workspace/cerbos/resources.ts
Normal 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;
|
||||
*/
|
||||
0
modules/workspace/client.ts
Normal file
0
modules/workspace/client.ts
Normal file
27
modules/workspace/database.ts
Normal file
27
modules/workspace/database.ts
Normal 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));
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import { MongoAdapter } from "@valkyr/event-store/mongo";
|
||||
*/
|
||||
|
||||
const eventFactory = new EventFactory([
|
||||
...(await import("./events/code.ts")).default,
|
||||
...(await import("./events/identity.ts")).default,
|
||||
...(await import("./events/workspace.ts")).default,
|
||||
...(await import("./events/workspace-user.ts")).default,
|
||||
]);
|
||||
|
||||
/*
|
||||
@@ -20,7 +20,7 @@ const eventFactory = new EventFactory([
|
||||
*/
|
||||
|
||||
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,
|
||||
snapshot: "auto",
|
||||
});
|
||||
23
modules/workspace/events/workspace-user.ts
Normal file
23
modules/workspace/events/workspace-user.ts
Normal 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),
|
||||
];
|
||||
19
modules/workspace/events/workspace.ts
Normal file
19
modules/workspace/events/workspace.ts
Normal 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),
|
||||
];
|
||||
38
modules/workspace/models/workspace-user.ts
Normal file
38
modules/workspace/models/workspace-user.ts
Normal 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>;
|
||||
32
modules/workspace/models/workspace.ts
Normal file
32
modules/workspace/models/workspace.ts
Normal 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>;
|
||||
19
modules/workspace/package.json
Normal file
19
modules/workspace/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
20
modules/workspace/routes/workspaces/create/handle.ts
Normal file
20
modules/workspace/routes/workspaces/create/handle.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
14
modules/workspace/routes/workspaces/create/spec.ts
Normal file
14
modules/workspace/routes/workspaces/create/spec.ts
Normal 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);
|
||||
30
modules/workspace/server.ts
Normal file
30
modules/workspace/server.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
13
modules/workspace/value-objects/contact.ts
Normal file
13
modules/workspace/value-objects/contact.ts
Normal 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>;
|
||||
@@ -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
|
||||
@@ -1,3 +1,5 @@
|
||||
import "@platform/supertoken/types.ts";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ServerContext {}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
@@ -298,7 +298,7 @@ export class ValidationError extends ServerError<ValidationErrorData> {
|
||||
* @param message - Optional message to send with the error. Default: "Validation Failed".
|
||||
* @param data - Data with validation failure details.
|
||||
*/
|
||||
constructor(message = "Validation Failed", data: ValidationErrorData) {
|
||||
constructor(message = "Validation Failed", data: TData) {
|
||||
super(message, 422, data);
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ export class ValidationError extends ServerError<ValidationErrorData> {
|
||||
message: issue.message,
|
||||
};
|
||||
}),
|
||||
});
|
||||
} satisfies ValidationErrorData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"dependencies": {
|
||||
"@platform/auth": "workspace:*",
|
||||
"@platform/socket": "workspace:*",
|
||||
"@platform/supertokens": "workspace:*",
|
||||
"@platform/vault": "workspace:*",
|
||||
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4",
|
||||
"path-to-regexp": "8",
|
||||
"zod": "4.1.11"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { cerbos } from "@platform/cerbos/client.ts";
|
||||
import { Resource } from "@platform/cerbos/resources.ts";
|
||||
|
||||
import { cerbos } from "./cerbos.ts";
|
||||
import type { Principal } from "./principal.ts";
|
||||
|
||||
export function access(principal: Principal) {
|
||||
export function getAccessControlMethods(principal: Principal) {
|
||||
return {
|
||||
/**
|
||||
* Check if a principal is allowed to perform an action on a resource.
|
||||
@@ -22,7 +20,7 @@ export function access(principal: Principal) {
|
||||
* "view"
|
||||
* ); // => true
|
||||
*/
|
||||
isAllowed(resource: Resource, action: string) {
|
||||
isAllowed(resource: any, action: string) {
|
||||
return cerbos.isAllowed({ principal, resource, action });
|
||||
},
|
||||
|
||||
@@ -45,7 +43,7 @@ export function access(principal: Principal) {
|
||||
*
|
||||
* decision.isAllowed("view"); // => true
|
||||
*/
|
||||
checkResource(resource: Resource, actions: string[]) {
|
||||
checkResource(resource: any, actions: string[]) {
|
||||
return cerbos.checkResource({ principal, resource, actions });
|
||||
},
|
||||
|
||||
@@ -80,10 +78,10 @@ export function access(principal: Principal) {
|
||||
* action: "view",
|
||||
* }); // => true
|
||||
*/
|
||||
checkResources(resources: { resource: Resource; actions: string[] }[]) {
|
||||
checkResources(resources: { resource: any; actions: string[] }[]) {
|
||||
return cerbos.checkResources({ principal, resources });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type Access = ReturnType<typeof access>;
|
||||
export type AccessControlMethods = ReturnType<typeof getAccessControlMethods>;
|
||||
44
platform/supertoken/config.ts
Normal file
44
platform/supertoken/config.ts
Normal 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,
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "@platform/cerbos",
|
||||
"name": "@platform/supertoken",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@cerbos/http": "0.23.1",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
46
platform/supertoken/principal.ts
Normal file
46
platform/supertoken/principal.ts
Normal 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>;
|
||||
133
platform/supertoken/server.ts
Normal file
133
platform/supertoken/server.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
37
platform/supertoken/session.ts
Normal file
37
platform/supertoken/session.ts
Normal 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);
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
import "@platform/relay";
|
||||
import "@platform/storage";
|
||||
|
||||
import type { Access } from "./auth/access.ts";
|
||||
import type { Principal } from "./auth/principal.ts";
|
||||
import type Session from "supertokens-node/recipe/session";
|
||||
|
||||
import type { AccessControlMethods } from "./access.ts";
|
||||
import type { Principal } from "./principal.ts";
|
||||
|
||||
declare module "@platform/storage" {
|
||||
interface StorageContext {
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
session?: Session.SessionContainer;
|
||||
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
@@ -14,7 +21,7 @@ declare module "@platform/storage" {
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
access?: Access;
|
||||
access?: AccessControlMethods;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +32,11 @@ declare module "@platform/relay" {
|
||||
*/
|
||||
isAuthenticated: boolean;
|
||||
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
session: Session.SessionContainer;
|
||||
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
@@ -33,6 +45,6 @@ declare module "@platform/relay" {
|
||||
/**
|
||||
* TODO ...
|
||||
*/
|
||||
access: Access;
|
||||
access: AccessControlMethods;
|
||||
}
|
||||
}
|
||||
10
platform/supertoken/users.ts
Normal file
10
platform/supertoken/users.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user