feat: modular domain driven boilerplate
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
meta {
|
meta {
|
||||||
name: account
|
name: Identity
|
||||||
seq: 1
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5,13 +5,13 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{url}}/accounts/:id
|
url: {{url}}/identities/:id
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
params:path {
|
params:path {
|
||||||
id:
|
id: 16b88034-ca82-4a8e-9fe5-13bd0dd29b75
|
||||||
}
|
}
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
@@ -5,15 +5,15 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{url}}/auth/code/:accountId/code/:codeId/:value
|
url: {{url}}/identities/login/code/:identityId/code/:codeId/:value
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
params:path {
|
params:path {
|
||||||
accountId:
|
identityId: efefa471-905d-4702-bd0a-863d8cf70424
|
||||||
codeId:
|
codeId: 7055b769-0814-47b8-836e-cfef2d8c2e68
|
||||||
value:
|
value: 00597
|
||||||
}
|
}
|
||||||
|
|
||||||
script:post-response {
|
script:post-response {
|
||||||
@@ -5,7 +5,7 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{url}}/auth/email
|
url: {{url}}/identities/login/email
|
||||||
body: json
|
body: json
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
meta {
|
meta {
|
||||||
name: auth
|
name: Login
|
||||||
seq: 2
|
seq: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Session
|
name: Me
|
||||||
type: http
|
type: http
|
||||||
seq: 3
|
seq: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{url}}/auth/session
|
url: {{url}}/identities/me
|
||||||
body: none
|
body: none
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Create
|
name: Register
|
||||||
type: http
|
type: http
|
||||||
seq: 1
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
url: {{url}}/accounts
|
url: {{url}}/identities
|
||||||
body: json
|
body: json
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,10 @@ post {
|
|||||||
body:json {
|
body:json {
|
||||||
{
|
{
|
||||||
"name": {
|
"name": {
|
||||||
"given": "John",
|
"given": "Jane",
|
||||||
"family": "Doe"
|
"family": "Doe"
|
||||||
},
|
},
|
||||||
"email": "john.doe@fixture.none"
|
"email": "jane.doe@fixture.none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { resolve } from "node:path";
|
|
||||||
|
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
|
||||||
|
|
||||||
const LIBRARIES_DIR = resolve(import.meta.dirname!, "..", "libraries");
|
|
||||||
const STORES_DIR = resolve(import.meta.dirname!, "..", "stores");
|
|
||||||
|
|
||||||
const log = logger.prefix("Bootstrap");
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Database
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
await import("~libraries/database/.tasks/bootstrap.ts");
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Packages
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
await bootstrap(LIBRARIES_DIR);
|
|
||||||
await bootstrap(STORES_DIR);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Helpers
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traverse path and look for a `bootstrap.ts` file in each folder found under
|
|
||||||
* the given path. If a `boostrap.ts` file is found it is imported so its content
|
|
||||||
* is executed.
|
|
||||||
*
|
|
||||||
* @param path - Path to resolve `bootstrap.ts` files.
|
|
||||||
*/
|
|
||||||
export async function bootstrap(path: string): Promise<void> {
|
|
||||||
const bootstrap: { name: string; path: string }[] = [];
|
|
||||||
for await (const entry of Deno.readDir(path)) {
|
|
||||||
if (entry.isDirectory === true) {
|
|
||||||
const moduleName = path.split("/").pop();
|
|
||||||
if (moduleName === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const filePath = `${path}/${entry.name}/.tasks/bootstrap.ts`;
|
|
||||||
if (await hasFile(filePath)) {
|
|
||||||
bootstrap.push({ name: entry.name, path: filePath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const entry of bootstrap) {
|
|
||||||
log.info(entry.name);
|
|
||||||
await import(entry.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hasFile(filePath: string) {
|
|
||||||
try {
|
|
||||||
await Deno.lstat(filePath);
|
|
||||||
} catch (err) {
|
|
||||||
if (!(err instanceof Deno.errors.NotFound)) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { resolve } from "node:path";
|
|
||||||
import process from "node:process";
|
|
||||||
|
|
||||||
import { exists } from "@std/fs";
|
|
||||||
|
|
||||||
import { config } from "~libraries/database/config.ts";
|
|
||||||
import { getMongoClient } from "~libraries/database/connection.ts";
|
|
||||||
import { container } from "~libraries/database/container.ts";
|
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Dependencies
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
const client = getMongoClient(config.mongo);
|
|
||||||
|
|
||||||
container.set("client", client);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Migrate
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
const db = client.db("api:migrations");
|
|
||||||
const collection = db.collection<MigrationDocument>("migrations");
|
|
||||||
|
|
||||||
const { default: journal } = await import(resolve(import.meta.dirname!, "migrations", "meta", "_journal.json"), {
|
|
||||||
with: { type: "json" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const migrations =
|
|
||||||
(await collection.findOne({ name: journal.name })) ?? ({ name: journal.name, entries: [] } as MigrationDocument);
|
|
||||||
|
|
||||||
for (const entry of journal.entries) {
|
|
||||||
const migrationFileName = `${String(entry.idx).padStart(4, "0")}_${entry.name}.ts`;
|
|
||||||
if (migrations.entries.includes(migrationFileName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const migrationPath = resolve(import.meta.dirname!, "migrations", migrationFileName);
|
|
||||||
if (await exists(migrationPath)) {
|
|
||||||
await import(migrationPath);
|
|
||||||
await collection.updateOne(
|
|
||||||
{
|
|
||||||
name: journal.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$set: { name: journal.name },
|
|
||||||
$push: { entries: migrationFileName }, // Assuming 'entries' is an array
|
|
||||||
},
|
|
||||||
{
|
|
||||||
upsert: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
logger.info(`Migrated ${migrationPath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MigrationDocument = {
|
|
||||||
name: string;
|
|
||||||
entries: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "api",
|
|
||||||
"entries": []
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { config as auth } from "~libraries/auth/config.ts";
|
import { getEnvironmentVariable } from "@platform/config/environment.ts";
|
||||||
import { getEnvironmentVariable, toNumber } from "~libraries/config/mod.ts";
|
import z from "zod";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
name: "valkyr",
|
name: "@valkyr/boilerplate",
|
||||||
host: getEnvironmentVariable("API_HOST", "0.0.0.0"),
|
host: getEnvironmentVariable({ key: "API_HOST", type: z.ipv4(), fallback: "0.0.0.0" }),
|
||||||
port: getEnvironmentVariable("API_PORT", toNumber, "8370"),
|
port: getEnvironmentVariable({
|
||||||
...auth,
|
key: "API_PORT",
|
||||||
|
type: z.coerce.number(),
|
||||||
|
fallback: "8370",
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"imports": {
|
|
||||||
"~libraries/": "./libraries/",
|
|
||||||
"~stores/": "./stores/",
|
|
||||||
"~config": "./config.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Auth } from "@valkyr/auth";
|
|
||||||
|
|
||||||
import { access } from "./access.ts";
|
|
||||||
import { config } from "./config.ts";
|
|
||||||
import { principal } from "./principal.ts";
|
|
||||||
import { resources } from "./resources.ts";
|
|
||||||
|
|
||||||
export const auth = new Auth({
|
|
||||||
principal,
|
|
||||||
resources,
|
|
||||||
access,
|
|
||||||
jwt: {
|
|
||||||
algorithm: "RS256",
|
|
||||||
privateKey: config.privateKey,
|
|
||||||
publicKey: config.publicKey,
|
|
||||||
issuer: "http://localhost",
|
|
||||||
audience: "http://localhost",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Session = typeof auth.$session;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { resolve } from "node:path";
|
|
||||||
|
|
||||||
import type { SerializeOptions } from "cookie";
|
|
||||||
|
|
||||||
import { getEnvironmentVariable, toBoolean } from "~libraries/config/mod.ts";
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
privateKey: getEnvironmentVariable(
|
|
||||||
"AUTH_PRIVATE_KEY",
|
|
||||||
await readFile(resolve(import.meta.dirname!, ".keys", "private"), "utf-8"),
|
|
||||||
),
|
|
||||||
publicKey: getEnvironmentVariable(
|
|
||||||
"AUTH_PUBLIC_KEY",
|
|
||||||
await readFile(resolve(import.meta.dirname!, ".keys", "public"), "utf-8"),
|
|
||||||
),
|
|
||||||
cookie: (maxAge: number) =>
|
|
||||||
({
|
|
||||||
httpOnly: true,
|
|
||||||
secure: getEnvironmentVariable("AUTH_COOKIE_SECURE", toBoolean, "false"), // Set to true for HTTPS in production
|
|
||||||
maxAge,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
}) satisfies SerializeOptions,
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { auth } from "./auth.ts";
|
|
||||||
|
|
||||||
export * from "./auth.ts";
|
|
||||||
export * from "./config.ts";
|
|
||||||
|
|
||||||
export type Auth = typeof auth;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { RoleSchema } from "@platform/spec/account/role.ts";
|
|
||||||
import { PrincipalProvider } from "@valkyr/auth";
|
|
||||||
|
|
||||||
import { db } from "~stores/read-store/database.ts";
|
|
||||||
|
|
||||||
export const principal = new PrincipalProvider(RoleSchema, {}, async function (id: string) {
|
|
||||||
const account = await db.collection("accounts").findOne({ id });
|
|
||||||
if (account === null) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
roles: account.roles,
|
|
||||||
attributes: {},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Principal = typeof principal.$principal;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { parseArgs } from "@std/cli";
|
|
||||||
|
|
||||||
import { Parser, toString } from "./parsers.ts";
|
|
||||||
|
|
||||||
export function getArgsVariable(key: string, fallback?: any): string;
|
|
||||||
export function getArgsVariable<T extends Parser>(key: string, parse: T, fallback?: any): ReturnType<T>;
|
|
||||||
export function getArgsVariable<T extends Parser>(key: string, parse?: T, fallback?: any): ReturnType<T> {
|
|
||||||
if (typeof parse === "string") {
|
|
||||||
fallback = parse;
|
|
||||||
parse = undefined;
|
|
||||||
}
|
|
||||||
const flags = parseArgs(Deno.args);
|
|
||||||
const value = flags[key];
|
|
||||||
if (value === undefined) {
|
|
||||||
if (fallback !== undefined) {
|
|
||||||
return parse ? parse(fallback) : fallback;
|
|
||||||
}
|
|
||||||
throw new Error(`Config Exception: Missing ${key} variable in arguments`);
|
|
||||||
}
|
|
||||||
return parse ? parse(value) : (toString(value) as any);
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { load } from "@std/dotenv";
|
|
||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
import { Env, Parser, toServiceEnv, toString } from "./parsers.ts";
|
|
||||||
|
|
||||||
const env = await load();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an environment variable and parse it to the desired type.
|
|
||||||
*
|
|
||||||
* @param key - Environment key to resolve.
|
|
||||||
* @param parse - Parser function to convert the value to the desired type. Default: `string`.
|
|
||||||
*/
|
|
||||||
export function getEnvironmentVariable(key: string, fallback?: any): string;
|
|
||||||
export function getEnvironmentVariable<T extends Parser>(key: string, parse: T, fallback?: any): ReturnType<T>;
|
|
||||||
export function getEnvironmentVariable<T extends Parser>(key: string, parse?: T, fallback?: any): ReturnType<T> {
|
|
||||||
if (typeof parse === "string") {
|
|
||||||
fallback = parse;
|
|
||||||
parse = undefined;
|
|
||||||
}
|
|
||||||
const value = env[key] ?? Deno.env.get(key);
|
|
||||||
if (value === undefined) {
|
|
||||||
if (fallback !== undefined) {
|
|
||||||
return parse ? parse(fallback) : fallback;
|
|
||||||
}
|
|
||||||
throw new Error(`Config Exception: Missing ${key} variable in configuration`);
|
|
||||||
}
|
|
||||||
return parse ? parse(value) : (toString(value) as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an environment variable, select value based on ENV map and parse it to the desired type. Can be used with simple primitives or objects / arrays
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {{
|
|
||||||
* key: string;
|
|
||||||
* envFallback?: FallbackEnvMap;
|
|
||||||
* fallback: string;
|
|
||||||
* validation: z.ZodTypeAny,
|
|
||||||
* }} options
|
|
||||||
* @param {string} options.key - the name of the env variable
|
|
||||||
* @param {object} options.envFallback - map with env specific fallbacks that will be used if none value provided
|
|
||||||
* @param {string} options.envFallback.local - example "local" SERVICE_ENV target fallback value
|
|
||||||
* @param {string} options.fallback - string fallback that will be used if no env variable found
|
|
||||||
* @param {z.ZodTypeAny} options.validation - Zod validation object or validation primitive
|
|
||||||
* @returns {z.infer<typeof validation>} - Returns the inferred type of the validation provided
|
|
||||||
*/
|
|
||||||
export function validateEnvVariable({
|
|
||||||
key,
|
|
||||||
envFallback,
|
|
||||||
fallback,
|
|
||||||
validation,
|
|
||||||
}: {
|
|
||||||
key: string;
|
|
||||||
validation: z.ZodTypeAny;
|
|
||||||
envFallback?: FallbackEnvMap;
|
|
||||||
fallback?: string;
|
|
||||||
}): z.infer<typeof validation> {
|
|
||||||
const serviceEnv = getEnvironmentVariable("SERVICE_ENV", toServiceEnv, "local");
|
|
||||||
const providedValue = env[key] ?? Deno.env.get(key);
|
|
||||||
const fallbackValue = typeof envFallback === "object" ? (envFallback[serviceEnv] ?? fallback) : fallback;
|
|
||||||
const toBeUsed = providedValue ?? fallbackValue;
|
|
||||||
try {
|
|
||||||
if (typeof toBeUsed === "string" && (toBeUsed.trim().startsWith("{") || toBeUsed.trim().startsWith("["))) {
|
|
||||||
return validation.parse(JSON.parse(toBeUsed));
|
|
||||||
}
|
|
||||||
return validation.parse(toBeUsed);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Deno.errors.InvalidData(`Config Exception: Missing valid ${key} variable in configuration`, { cause: e });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FallbackEnvMap = Partial<Record<Env, string>> & {
|
|
||||||
testing?: string;
|
|
||||||
local?: string;
|
|
||||||
stg?: string;
|
|
||||||
demo?: string;
|
|
||||||
prod?: string;
|
|
||||||
};
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
const SERVICE_ENV = ["testing", "local", "stg", "demo", "prod"] as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an variable to a string.
|
|
||||||
*
|
|
||||||
* @param value - Value to convert.
|
|
||||||
*/
|
|
||||||
export function toString(value: unknown): string {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
throw new Error(`Config Exception: Cannot convert ${value} to string`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an variable to a number.
|
|
||||||
*
|
|
||||||
* @param value - Value to convert.
|
|
||||||
*/
|
|
||||||
export function toNumber(value: unknown): number {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return parseInt(value);
|
|
||||||
}
|
|
||||||
throw new Error(`Config Exception: Cannot convert ${value} to number`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an variable to a boolean.
|
|
||||||
*
|
|
||||||
* @param value - Value to convert.
|
|
||||||
*/
|
|
||||||
export function toBoolean(value: unknown): boolean {
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value === "true" || value === "1";
|
|
||||||
}
|
|
||||||
throw new Error(`Config Exception: Cannot convert ${value} to boolean`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a variable to an array of strings.
|
|
||||||
*
|
|
||||||
* Expects a comma seprated, eg. foo,bar,foobar
|
|
||||||
*
|
|
||||||
* @param value - Value to convert.
|
|
||||||
*/
|
|
||||||
export function toArray(value: unknown): string[] {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
if (value === "") {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return value.split(",");
|
|
||||||
}
|
|
||||||
throw new Error(`Config Exception: Cannot convert ${value} to array`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the given value is a valid SERVICE_ENV variable.
|
|
||||||
*
|
|
||||||
* @param value - Value to validate.
|
|
||||||
*/
|
|
||||||
export function toServiceEnv(value: unknown): Env {
|
|
||||||
assertServiceEnv(value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Assertions
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
function assertServiceEnv(value: unknown): asserts value is Env {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
throw new Error(`Config Exception: Env ${value} is not a string`);
|
|
||||||
}
|
|
||||||
if ((SERVICE_ENV as unknown as string[]).includes(value) === false) {
|
|
||||||
throw new Error(`Config Exception: Invalid env ${value} provided`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Types
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type Parser = (value: unknown) => any;
|
|
||||||
|
|
||||||
export type Env = (typeof SERVICE_ENV)[number];
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./libraries/args.ts";
|
|
||||||
export * from "./libraries/environment.ts";
|
|
||||||
export * from "./libraries/parsers.ts";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./password.ts";
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { config } from "../config.ts";
|
|
||||||
import { getMongoClient } from "../connection.ts";
|
|
||||||
import { container } from "../container.ts";
|
|
||||||
|
|
||||||
container.set("client", getMongoClient(config.mongo));
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { getEnvironmentVariable, toNumber } from "~libraries/config/mod.ts";
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
mongo: {
|
|
||||||
host: getEnvironmentVariable("DB_MONGO_HOST", "localhost"),
|
|
||||||
port: getEnvironmentVariable("DB_MONGO_PORT", toNumber, "27017"),
|
|
||||||
user: getEnvironmentVariable("DB_MONGO_USER", "root"),
|
|
||||||
pass: getEnvironmentVariable("DB_MONGO_PASSWORD", "password"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { getArgsVariable } from "~libraries/config/mod.ts";
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
level: getArgsVariable("LOG_LEVEL", "info"),
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { ServerContext } from "@platform/relay";
|
|
||||||
|
|
||||||
import type { Sockets } from "~libraries/socket/sockets.ts";
|
|
||||||
|
|
||||||
import { Access } from "../auth/access.ts";
|
|
||||||
import { Session } from "../auth/auth.ts";
|
|
||||||
import { Principal } from "../auth/principal.ts";
|
|
||||||
import { req } from "./request.ts";
|
|
||||||
|
|
||||||
declare module "@platform/relay" {
|
|
||||||
interface ServerContext {
|
|
||||||
/**
|
|
||||||
* Current request instance being handled.
|
|
||||||
*/
|
|
||||||
request: Request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is the request authenticated.
|
|
||||||
*/
|
|
||||||
isAuthenticated: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get request session instance.
|
|
||||||
*/
|
|
||||||
session: Session;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get request principal.
|
|
||||||
*/
|
|
||||||
principal: Principal;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get access control session.
|
|
||||||
*/
|
|
||||||
access: Access;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sockets instance attached to the server.
|
|
||||||
*/
|
|
||||||
sockets: Sockets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRequestContext(request: Request): ServerContext {
|
|
||||||
return {
|
|
||||||
request,
|
|
||||||
|
|
||||||
get isAuthenticated(): boolean {
|
|
||||||
return req.isAuthenticated;
|
|
||||||
},
|
|
||||||
|
|
||||||
get session(): Session {
|
|
||||||
return req.session;
|
|
||||||
},
|
|
||||||
|
|
||||||
get principal(): Principal {
|
|
||||||
return req.session.principal;
|
|
||||||
},
|
|
||||||
|
|
||||||
get access(): Access {
|
|
||||||
return req.session.access;
|
|
||||||
},
|
|
||||||
|
|
||||||
get sockets(): Sockets {
|
|
||||||
return req.sockets;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from "./api.ts";
|
|
||||||
export * from "./context.ts";
|
|
||||||
export * from "./modules.ts";
|
|
||||||
export * from "./request.ts";
|
|
||||||
export * from "./storage.ts";
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { InternalServerError, UnauthorizedError } from "@platform/relay";
|
|
||||||
|
|
||||||
import { Session } from "../auth/auth.ts";
|
|
||||||
import { storage } from "./storage.ts";
|
|
||||||
|
|
||||||
export const req = {
|
|
||||||
get store() {
|
|
||||||
const store = storage.getStore();
|
|
||||||
if (store === undefined) {
|
|
||||||
throw new InternalServerError("AsyncLocalStorage not defined.");
|
|
||||||
}
|
|
||||||
return store;
|
|
||||||
},
|
|
||||||
|
|
||||||
get sockets() {
|
|
||||||
if (this.store.sockets === undefined) {
|
|
||||||
throw new InternalServerError("Sockets not defined.");
|
|
||||||
}
|
|
||||||
return this.store.sockets;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the request is authenticated.
|
|
||||||
*/
|
|
||||||
get isAuthenticated(): boolean {
|
|
||||||
return this.session !== undefined;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current session.
|
|
||||||
*/
|
|
||||||
get session(): Session {
|
|
||||||
if (this.store.session === undefined) {
|
|
||||||
throw new UnauthorizedError();
|
|
||||||
}
|
|
||||||
return this.store.session;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the meta information stored in the request.
|
|
||||||
*/
|
|
||||||
get info() {
|
|
||||||
return this.store.info;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current session.
|
|
||||||
*/
|
|
||||||
getSession(): Session | undefined {
|
|
||||||
return this.store.session;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get store that is potentially undefined.
|
|
||||||
* Typically used when utility functions might run in and out of request scope.
|
|
||||||
*/
|
|
||||||
getStore() {
|
|
||||||
return storage.getStore();
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type ReqContext = typeof req;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { AsyncLocalStorage } from "node:async_hooks";
|
|
||||||
|
|
||||||
import type { Session } from "~libraries/auth/mod.ts";
|
|
||||||
import type { Sockets } from "~libraries/socket/sockets.ts";
|
|
||||||
|
|
||||||
export const storage = new AsyncLocalStorage<Storage>();
|
|
||||||
|
|
||||||
export type Storage = {
|
|
||||||
session?: Session;
|
|
||||||
info: {
|
|
||||||
method: string;
|
|
||||||
start: number;
|
|
||||||
end?: number;
|
|
||||||
};
|
|
||||||
sockets?: Sockets;
|
|
||||||
response: {
|
|
||||||
headers: Headers;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { toJsonRpc } from "@valkyr/json-rpc";
|
|
||||||
|
|
||||||
import { Session } from "~libraries/auth/mod.ts";
|
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
|
||||||
import { asyncLocalStorage } from "~libraries/server/storage.ts";
|
|
||||||
|
|
||||||
import { sockets } from "./sockets.ts";
|
|
||||||
|
|
||||||
export function upgrade(request: Request, session?: Session) {
|
|
||||||
const { socket, response } = Deno.upgradeWebSocket(request);
|
|
||||||
|
|
||||||
socket.addEventListener("open", () => {
|
|
||||||
logger.prefix("Socket").info("socket connected", { session });
|
|
||||||
sockets.add(socket);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener("close", () => {
|
|
||||||
logger.prefix("Socket").info("socket disconnected", { session });
|
|
||||||
sockets.del(socket);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener("message", (event) => {
|
|
||||||
if (event.data === "ping") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = toJsonRpc(event.data);
|
|
||||||
|
|
||||||
logger.prefix("Socket").info(message);
|
|
||||||
|
|
||||||
asyncLocalStorage.run(
|
|
||||||
{
|
|
||||||
session,
|
|
||||||
info: {
|
|
||||||
method: message.method!,
|
|
||||||
start: Date.now(),
|
|
||||||
},
|
|
||||||
sockets,
|
|
||||||
response: {
|
|
||||||
headers: new Headers(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
// api
|
|
||||||
// .send(body)
|
|
||||||
// .then((response) => {
|
|
||||||
// if (response !== undefined) {
|
|
||||||
// logger.info({ response });
|
|
||||||
// socket.send(JSON.stringify(response));
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// logger.info({ error });
|
|
||||||
// socket.send(JSON.stringify(error));
|
|
||||||
// });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,9 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "deno --allow-all --watch-hmr=routes/ server.ts",
|
"start": "deno --allow-all --watch-hmr=routes/ server.ts"
|
||||||
"migrate": "deno run --allow-all .tasks/migrate.ts"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cerbos/http": "0.23.1",
|
"zod": "4.1.11"
|
||||||
"@felix/bcrypt": "npm:@jsr/felix__bcrypt@1.0.5",
|
|
||||||
"@platform/models": "workspace:*",
|
|
||||||
"@platform/relay": "workspace:*",
|
|
||||||
"@platform/spec": "workspace:*",
|
|
||||||
"@std/cli": "npm:@jsr/std__cli@1.0.22",
|
|
||||||
"@std/dotenv": "npm:@jsr/std__dotenv@0.225.5",
|
|
||||||
"@std/fs": "npm:@jsr/std__fs@1.0.19",
|
|
||||||
"@std/path": "npm:@jsr/std__path@1.1.2",
|
|
||||||
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4",
|
|
||||||
"@valkyr/event-store": "npm:@jsr/valkyr__event-store@2.0.1",
|
|
||||||
"@valkyr/inverse": "npm:@jsr/valkyr__inverse@1.0.1",
|
|
||||||
"@valkyr/json-rpc": "npm:@jsr/valkyr__json-rpc@1.1.0",
|
|
||||||
"cookie": "1.0.2",
|
|
||||||
"mongodb": "6.20.0",
|
|
||||||
"zod": "4.1.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { AccountEmailClaimedError } from "@platform/spec/account/errors.ts";
|
|
||||||
import { create } from "@platform/spec/account/routes.ts";
|
|
||||||
|
|
||||||
import { Account, isEmailClaimed } from "~stores/event-store/aggregates/account.ts";
|
|
||||||
import { eventStore } from "~stores/event-store/event-store.ts";
|
|
||||||
|
|
||||||
export default create.access("public").handle(async ({ body: { name, email } }) => {
|
|
||||||
if ((await isEmailClaimed(email)) === true) {
|
|
||||||
return new AccountEmailClaimedError(email);
|
|
||||||
}
|
|
||||||
return eventStore.aggregate
|
|
||||||
.from(Account)
|
|
||||||
.create()
|
|
||||||
.addName(name)
|
|
||||||
.addEmailStrategy(email)
|
|
||||||
.addRole("user")
|
|
||||||
.save()
|
|
||||||
.then((account) => account.id);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { ForbiddenError, NotFoundError } from "@platform/relay";
|
|
||||||
import { getById } from "@platform/spec/account/routes.ts";
|
|
||||||
|
|
||||||
import { db } from "~stores/read-store/database.ts";
|
|
||||||
|
|
||||||
export default getById.access("authenticated").handle(async ({ params: { id } }, { access }) => {
|
|
||||||
const account = await db.collection("accounts").findOne({ id });
|
|
||||||
if (account === null) {
|
|
||||||
return new NotFoundError();
|
|
||||||
}
|
|
||||||
const decision = await access.isAllowed({ kind: "account", id: account.id, attributes: {} }, "read");
|
|
||||||
if (decision === false) {
|
|
||||||
return new ForbiddenError();
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
});
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { email } from "@platform/spec/auth/routes.ts";
|
|
||||||
|
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
|
||||||
import { Account, getAccountEmailRelation } from "~stores/event-store/aggregates/account.ts";
|
|
||||||
import { Code } from "~stores/event-store/aggregates/code.ts";
|
|
||||||
import { eventStore } from "~stores/event-store/event-store.ts";
|
|
||||||
|
|
||||||
export default email.access("public").handle(async ({ body: { base, email } }) => {
|
|
||||||
const account = await eventStore.aggregate.getByRelation(Account, getAccountEmailRelation(email));
|
|
||||||
if (account === undefined) {
|
|
||||||
return logger.info({
|
|
||||||
type: "auth:email",
|
|
||||||
code: false,
|
|
||||||
message: "Account Not Found",
|
|
||||||
received: email,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const code = await eventStore.aggregate.from(Code).create({ accountId: account.id }).save();
|
|
||||||
logger.info({
|
|
||||||
type: "auth:email",
|
|
||||||
data: {
|
|
||||||
code: code.id,
|
|
||||||
accountId: account.id,
|
|
||||||
},
|
|
||||||
link: `${base}/api/v1/admin/auth/${account.id}/code/${code.id}/${code.value}?next=${base}/admin`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { UnauthorizedError } from "@platform/relay";
|
|
||||||
import { session } from "@platform/spec/auth/routes.ts";
|
|
||||||
|
|
||||||
import { getAccountById } from "~stores/read-store/methods.ts";
|
|
||||||
|
|
||||||
export default session.access("authenticated").handle(async ({ principal }) => {
|
|
||||||
const account = await getAccountById(principal.id);
|
|
||||||
if (account === undefined) {
|
|
||||||
return new UnauthorizedError();
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { resolve } from "@std/path";
|
import identity from "@modules/identity/server.ts";
|
||||||
import cookie from "cookie";
|
import database from "@platform/database/server.ts";
|
||||||
|
import { logger } from "@platform/logger";
|
||||||
import { auth, type Session } from "~libraries/auth/mod.ts";
|
import { context } from "@platform/relay";
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
import { Api } from "@platform/server/api.ts";
|
||||||
import { type Storage, storage } from "~libraries/server/mod.ts";
|
import server from "@platform/server/server.ts";
|
||||||
import { Api, resolveRoutes } from "~libraries/server/mod.ts";
|
import socket from "@platform/socket/server.ts";
|
||||||
|
import { storage } from "@platform/storage";
|
||||||
|
|
||||||
import { config } from "./config.ts";
|
import { config } from "./config.ts";
|
||||||
|
|
||||||
const ROUTES_DIR = resolve(import.meta.dirname!, "routes");
|
|
||||||
|
|
||||||
const log = logger.prefix("Server");
|
const log = logger.prefix("Server");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -18,7 +17,15 @@ const log = logger.prefix("Server");
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
await import("./.tasks/bootstrap.ts");
|
// ### Platform
|
||||||
|
|
||||||
|
await database.bootstrap();
|
||||||
|
await server.bootstrap();
|
||||||
|
await socket.bootstrap();
|
||||||
|
|
||||||
|
// ### Modules
|
||||||
|
|
||||||
|
await identity.bootstrap();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -26,7 +33,7 @@ await import("./.tasks/bootstrap.ts");
|
|||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const api = new Api(await resolveRoutes(ROUTES_DIR));
|
const api = new Api([...identity.routes]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
@@ -42,42 +49,25 @@ Deno.serve(
|
|||||||
logger.prefix("Server").info(`Listening at http://${hostname}:${port}`);
|
logger.prefix("Server").info(`Listening at http://${hostname}:${port}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request) => {
|
async (request) =>
|
||||||
const url = new URL(request.url);
|
storage.run({}, async () => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
let session: Session | undefined;
|
// ### Storage Context
|
||||||
|
// Resolve storage context for all dependent modules.
|
||||||
|
|
||||||
const token = cookie.parse(request.headers.get("cookie") ?? "").token;
|
await server.resolve(request);
|
||||||
if (token !== undefined) {
|
await socket.resolve();
|
||||||
const resolved = await auth.resolve(token);
|
|
||||||
if (resolved.valid === false) {
|
|
||||||
return new Response(resolved.message, {
|
|
||||||
status: 401,
|
|
||||||
headers: {
|
|
||||||
"set-cookie": cookie.serialize("token", "", config.cookie(0)),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
session = resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = {
|
await identity.resolve(request);
|
||||||
session,
|
|
||||||
info: {
|
// ### Fetch
|
||||||
method: request.url,
|
// Execute fetch against the api instance.
|
||||||
start: Date.now(),
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
headers: new Headers(),
|
|
||||||
},
|
|
||||||
} satisfies Storage;
|
|
||||||
|
|
||||||
return storage.run(context, async () => {
|
|
||||||
return api.fetch(request).finally(() => {
|
return api.fetch(request).finally(() => {
|
||||||
log.info(
|
log.info(
|
||||||
`${request.method} ${url.pathname} [${((Date.now() - context.info.start) / 1000).toLocaleString()} seconds]`,
|
`${request.method} ${url.pathname} [${((Date.now() - context.info.start) / 1000).toLocaleString()} seconds]`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
}),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { register } from "@valkyr/event-store/mongo";
|
|
||||||
|
|
||||||
import { eventStore } from "../event-store.ts";
|
|
||||||
|
|
||||||
await register(eventStore.db.db, console.info);
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
import { toAccountDocument } from "@platform/models/account.ts";
|
|
||||||
import { Avatar } from "@platform/models/value-objects/avatar.ts";
|
|
||||||
import { Contact } from "@platform/models/value-objects/contact.ts";
|
|
||||||
import { Email } from "@platform/models/value-objects/email.ts";
|
|
||||||
import { Name } from "@platform/models/value-objects/name.ts";
|
|
||||||
import { Role } from "@platform/spec/account/role.ts";
|
|
||||||
import { Strategy } from "@platform/spec/account/strategies.ts";
|
|
||||||
import { AggregateRoot, getDate } from "@valkyr/event-store";
|
|
||||||
|
|
||||||
import { db } from "~stores/read-store/database.ts";
|
|
||||||
|
|
||||||
import { eventStore } from "../event-store.ts";
|
|
||||||
import { Auditor, systemAuditor } from "../events/auditor.ts";
|
|
||||||
import { EventRecord, EventStoreFactory } from "../events/mod.ts";
|
|
||||||
import { projector } from "../projector.ts";
|
|
||||||
|
|
||||||
export class Account extends AggregateRoot<EventStoreFactory> {
|
|
||||||
static override readonly name = "account";
|
|
||||||
|
|
||||||
avatar?: Avatar;
|
|
||||||
name?: Name;
|
|
||||||
contact: Contact = {
|
|
||||||
emails: [],
|
|
||||||
};
|
|
||||||
strategies: Strategy[] = [];
|
|
||||||
roles: Role[] = [];
|
|
||||||
|
|
||||||
createdAt!: Date;
|
|
||||||
updatedAt!: Date;
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Reducer
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
with(event: EventRecord): void {
|
|
||||||
switch (event.type) {
|
|
||||||
case "account:created": {
|
|
||||||
this.id = event.stream;
|
|
||||||
this.createdAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "account:avatar:added": {
|
|
||||||
this.avatar = { url: event.data };
|
|
||||||
this.updatedAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "account:name:added": {
|
|
||||||
this.name = event.data;
|
|
||||||
this.updatedAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "account:email:added": {
|
|
||||||
this.contact.emails.push(event.data);
|
|
||||||
this.updatedAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "account:role:added": {
|
|
||||||
this.roles.push(event.data);
|
|
||||||
this.updatedAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "strategy:email:added": {
|
|
||||||
this.strategies.push({ type: "email", value: event.data });
|
|
||||||
this.updatedAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "strategy:password:added": {
|
|
||||||
this.strategies.push({ type: "password", ...event.data });
|
|
||||||
this.updatedAt = getDate(event.created);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Actions
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
create(meta: Auditor = systemAuditor) {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "account:created",
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addAvatar(url: string, meta: Auditor = systemAuditor): this {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "account:avatar:added",
|
|
||||||
data: url,
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addName(name: Name, meta: Auditor = systemAuditor): this {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "account:name:added",
|
|
||||||
data: name,
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addEmail(email: Email, meta: Auditor = systemAuditor): this {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "account:email:added",
|
|
||||||
data: email,
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addRole(role: Role, meta: Auditor = systemAuditor): this {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "account:role:added",
|
|
||||||
data: role,
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addEmailStrategy(email: string, meta: Auditor = systemAuditor): this {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "strategy:email:added",
|
|
||||||
data: email,
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addPasswordStrategy(alias: string, password: string, meta: Auditor = systemAuditor): this {
|
|
||||||
return this.push({
|
|
||||||
stream: this.id,
|
|
||||||
type: "strategy:password:added",
|
|
||||||
data: { alias, password },
|
|
||||||
meta,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Utilities
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function isEmailClaimed(email: string): Promise<boolean> {
|
|
||||||
const relations = await eventStore.relations.getByKey(getAccountEmailRelation(email));
|
|
||||||
if (relations.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Relations
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function getAccountEmailRelation(email: string): string {
|
|
||||||
return `/accounts/emails/${email}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAccountAliasRelation(alias: string): string {
|
|
||||||
return `/accounts/aliases/${alias}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Projectors
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
projector.on("account:created", async ({ stream: id }) => {
|
|
||||||
await db.collection("accounts").insertOne(
|
|
||||||
toAccountDocument({
|
|
||||||
id,
|
|
||||||
name: {
|
|
||||||
given: null,
|
|
||||||
family: null,
|
|
||||||
},
|
|
||||||
contact: {
|
|
||||||
emails: [],
|
|
||||||
},
|
|
||||||
strategies: [],
|
|
||||||
roles: [],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
projector.on("account:avatar:added", async ({ stream: id, data: url }) => {
|
|
||||||
await db.collection("accounts").updateOne({ id }, { $set: { avatar: { url } } });
|
|
||||||
});
|
|
||||||
|
|
||||||
projector.on("account:name:added", async ({ stream: id, data: name }) => {
|
|
||||||
await db.collection("accounts").updateOne({ id }, { $set: { name } });
|
|
||||||
});
|
|
||||||
|
|
||||||
projector.on("account:email:added", async ({ stream: id, data: email }) => {
|
|
||||||
await db.collection("accounts").updateOne({ id }, { $push: { "contact.emails": email } });
|
|
||||||
});
|
|
||||||
|
|
||||||
projector.on("account:role:added", async ({ stream: id, data: role }) => {
|
|
||||||
await db.collection("accounts").updateOne({ id }, { $push: { roles: role } });
|
|
||||||
});
|
|
||||||
|
|
||||||
projector.on("strategy:email:added", async ({ stream: id, data: email }) => {
|
|
||||||
await eventStore.relations.insert(getAccountEmailRelation(email), id);
|
|
||||||
await db.collection("accounts").updateOne({ id }, { $push: { strategies: { type: "email", value: email } } });
|
|
||||||
});
|
|
||||||
|
|
||||||
projector.on("strategy:password:added", async ({ stream: id, data: strategy }) => {
|
|
||||||
await eventStore.relations.insert(getAccountAliasRelation(strategy.alias), id);
|
|
||||||
await db.collection("accounts").updateOne({ id }, { $push: { strategies: { type: "password", ...strategy } } });
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { EventStore } from "@valkyr/event-store";
|
|
||||||
import { MongoAdapter } from "@valkyr/event-store/mongo";
|
|
||||||
|
|
||||||
import { config } from "~config";
|
|
||||||
import { container } from "~libraries/database/container.ts";
|
|
||||||
|
|
||||||
import { events } from "./events/mod.ts";
|
|
||||||
import { projector } from "./projector.ts";
|
|
||||||
|
|
||||||
export const eventStore = new EventStore({
|
|
||||||
adapter: new MongoAdapter(() => container.get("client"), `${config.name}:event-store`),
|
|
||||||
events,
|
|
||||||
snapshot: "auto",
|
|
||||||
});
|
|
||||||
|
|
||||||
eventStore.onEventsInserted(async (records, { batch }) => {
|
|
||||||
if (batch !== undefined) {
|
|
||||||
await projector.pushMany(batch, records);
|
|
||||||
} else {
|
|
||||||
for (const record of records) {
|
|
||||||
await projector.push(record, { hydrated: false, outdated: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { EmailSchema } from "@platform/models/value-objects/email.ts";
|
|
||||||
import { NameSchema } from "@platform/models/value-objects/name.ts";
|
|
||||||
import { RoleSchema } from "@platform/spec/account/role.ts";
|
|
||||||
import { event } from "@valkyr/event-store";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { AuditorSchema } from "./auditor.ts";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
event.type("account:created").meta(AuditorSchema),
|
|
||||||
event.type("account:avatar:added").data(z.string()).meta(AuditorSchema),
|
|
||||||
event.type("account:name:added").data(NameSchema).meta(AuditorSchema),
|
|
||||||
event.type("account:email:added").data(EmailSchema).meta(AuditorSchema),
|
|
||||||
event.type("account:role:added").data(RoleSchema).meta(AuditorSchema),
|
|
||||||
];
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import z from "zod";
|
|
||||||
|
|
||||||
export const AuditorSchema = z.object({
|
|
||||||
auditor: z.union([
|
|
||||||
z.object({
|
|
||||||
type: z.literal("system"),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
type: z.literal("account"),
|
|
||||||
accountId: z.string(),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const systemAuditor: Auditor = {
|
|
||||||
auditor: {
|
|
||||||
type: "system",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Auditor = z.infer<typeof AuditorSchema>;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { EventFactory, Prettify } from "@valkyr/event-store";
|
|
||||||
|
|
||||||
import account from "./account.ts";
|
|
||||||
import code from "./code.ts";
|
|
||||||
import organization from "./organization.ts";
|
|
||||||
import strategy from "./strategy.ts";
|
|
||||||
|
|
||||||
export const events = new EventFactory([...account, ...code, ...organization, ...strategy]);
|
|
||||||
|
|
||||||
export type EventStoreFactory = typeof events;
|
|
||||||
|
|
||||||
export type EventRecord = Prettify<EventStoreFactory["$events"][number]["$record"]>;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { event } from "@valkyr/event-store";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { AuditorSchema } from "./auditor.ts";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
event
|
|
||||||
.type("organization:created")
|
|
||||||
.data(z.object({ name: z.string() }))
|
|
||||||
.meta(AuditorSchema),
|
|
||||||
];
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { event } from "@valkyr/event-store";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { AuditorSchema } from "./auditor.ts";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
event.type("strategy:email:added").data(z.string()).meta(AuditorSchema),
|
|
||||||
event.type("strategy:passkey:added").meta(AuditorSchema),
|
|
||||||
event
|
|
||||||
.type("strategy:password:added")
|
|
||||||
.data(z.object({ alias: z.string(), password: z.string() }))
|
|
||||||
.meta(AuditorSchema),
|
|
||||||
];
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Projector } from "@valkyr/event-store";
|
|
||||||
|
|
||||||
import { EventStoreFactory } from "./events/mod.ts";
|
|
||||||
|
|
||||||
export const projector = new Projector<EventStoreFactory>();
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { idIndex } from "~libraries/database/id.ts";
|
|
||||||
import { register } from "~libraries/database/registrar.ts";
|
|
||||||
|
|
||||||
import { db } from "../database.ts";
|
|
||||||
|
|
||||||
await register(db.db, [
|
|
||||||
{
|
|
||||||
name: "accounts",
|
|
||||||
indexes: [
|
|
||||||
idIndex,
|
|
||||||
[{ "strategies.type": 1, "strategies.alias": 1 }, { name: "strategy.password" }],
|
|
||||||
[{ "strategies.type": 1, "strategies.value": 1 }, { name: "strategy.email" }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "roles",
|
|
||||||
indexes: [idIndex, [{ name: 1 }, { name: "role.name" }]],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { AccountDocument } from "@platform/models/account.ts";
|
|
||||||
|
|
||||||
import { config } from "~config";
|
|
||||||
import { getDatabaseAccessor } from "~libraries/database/accessor.ts";
|
|
||||||
|
|
||||||
export const db = getDatabaseAccessor<{
|
|
||||||
accounts: AccountDocument;
|
|
||||||
}>(`${config.name}:read-store`);
|
|
||||||
|
|
||||||
export function takeOne<TDocument>(documents: TDocument[]): TDocument | undefined {
|
|
||||||
return documents[0];
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"tailwindcss": "4.1.13",
|
"tailwindcss": "4.1.13",
|
||||||
"zod": "4.1.9"
|
"zod": "4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.35.0",
|
"@eslint/js": "9.35.0",
|
||||||
|
|||||||
25
deno.json
25
deno.json
@@ -4,14 +4,31 @@
|
|||||||
"workspace": [
|
"workspace": [
|
||||||
"api",
|
"api",
|
||||||
"apps/react",
|
"apps/react",
|
||||||
"platform/models",
|
"modules/identity",
|
||||||
|
"platform/cerbos",
|
||||||
|
"platform/config",
|
||||||
|
"platform/database",
|
||||||
|
"platform/logger",
|
||||||
"platform/relay",
|
"platform/relay",
|
||||||
"platform/spec"
|
"platform/server",
|
||||||
|
"platform/socket",
|
||||||
|
"platform/spec",
|
||||||
|
"platform/storage",
|
||||||
|
"platform/vault"
|
||||||
],
|
],
|
||||||
"imports": {
|
"imports": {
|
||||||
"@platform/models/": "./platform/models/",
|
"@modules/identity/client.ts": "./modules/identity/client.ts",
|
||||||
|
"@modules/identity/server.ts": "./modules/identity/server.ts",
|
||||||
|
"@platform/cerbos/": "./platform/cerbos/",
|
||||||
|
"@platform/config/": "./platform/config/",
|
||||||
|
"@platform/database/": "./platform/database/",
|
||||||
|
"@platform/logger": "./platform/logger/mod.ts",
|
||||||
"@platform/relay": "./platform/relay/mod.ts",
|
"@platform/relay": "./platform/relay/mod.ts",
|
||||||
"@platform/spec/": "./platform/spec/"
|
"@platform/server/": "./platform/server/",
|
||||||
|
"@platform/socket/": "./platform/socket/",
|
||||||
|
"@platform/spec/": "./platform/spec/",
|
||||||
|
"@platform/storage": "./platform/storage/storage.ts",
|
||||||
|
"@platform/vault": "./platform/vault/vault.ts"
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start:api": {
|
"start:api": {
|
||||||
|
|||||||
210
deno.lock
generated
210
deno.lock
generated
@@ -5,10 +5,7 @@
|
|||||||
"npm:@eslint/js@9.35.0": "9.35.0",
|
"npm:@eslint/js@9.35.0": "9.35.0",
|
||||||
"npm:@jsr/felix__bcrypt@1.0.5": "1.0.5",
|
"npm:@jsr/felix__bcrypt@1.0.5": "1.0.5",
|
||||||
"npm:@jsr/std__assert@1.0.14": "1.0.14",
|
"npm:@jsr/std__assert@1.0.14": "1.0.14",
|
||||||
"npm:@jsr/std__cli@1.0.22": "1.0.22",
|
|
||||||
"npm:@jsr/std__dotenv@0.225.5": "0.225.5",
|
"npm:@jsr/std__dotenv@0.225.5": "0.225.5",
|
||||||
"npm:@jsr/std__fs@1.0.19": "1.0.19",
|
|
||||||
"npm:@jsr/std__path@1.1.2": "1.1.2",
|
|
||||||
"npm:@jsr/std__testing@1.0.15": "1.0.15",
|
"npm:@jsr/std__testing@1.0.15": "1.0.15",
|
||||||
"npm:@jsr/valkyr__auth@2.1.4": "2.1.4",
|
"npm:@jsr/valkyr__auth@2.1.4": "2.1.4",
|
||||||
"npm:@jsr/valkyr__db@2.0.0": "2.0.0",
|
"npm:@jsr/valkyr__db@2.0.0": "2.0.0",
|
||||||
@@ -31,7 +28,9 @@
|
|||||||
"npm:eslint@9.35.0": "9.35.0",
|
"npm:eslint@9.35.0": "9.35.0",
|
||||||
"npm:fast-equals@5.2.2": "5.2.2",
|
"npm:fast-equals@5.2.2": "5.2.2",
|
||||||
"npm:globals@16.4.0": "16.4.0",
|
"npm:globals@16.4.0": "16.4.0",
|
||||||
|
"npm:jose@6.1.0": "6.1.0",
|
||||||
"npm:mongodb@6.20.0": "6.20.0",
|
"npm:mongodb@6.20.0": "6.20.0",
|
||||||
|
"npm:nanoid@5.1.5": "5.1.5",
|
||||||
"npm:path-to-regexp@8": "8.3.0",
|
"npm:path-to-regexp@8": "8.3.0",
|
||||||
"npm:prettier@3.6.2": "3.6.2",
|
"npm:prettier@3.6.2": "3.6.2",
|
||||||
"npm:react-dom@19.1.1": "19.1.1_react@19.1.1",
|
"npm:react-dom@19.1.1": "19.1.1_react@19.1.1",
|
||||||
@@ -40,8 +39,7 @@
|
|||||||
"npm:typescript-eslint@8.44.0": "8.44.0_eslint@9.35.0_typescript@5.9.2_@typescript-eslint+parser@8.44.0__eslint@9.35.0__typescript@5.9.2",
|
"npm:typescript-eslint@8.44.0": "8.44.0_eslint@9.35.0_typescript@5.9.2_@typescript-eslint+parser@8.44.0__eslint@9.35.0__typescript@5.9.2",
|
||||||
"npm:typescript@5.9.2": "5.9.2",
|
"npm:typescript@5.9.2": "5.9.2",
|
||||||
"npm:vite@7.1.6": "7.1.6_picomatch@4.0.3_@types+node@24.2.0",
|
"npm:vite@7.1.6": "7.1.6_picomatch@4.0.3_@types+node@24.2.0",
|
||||||
"npm:zod@4": "4.1.9",
|
"npm:zod@4.1.11": "4.1.11"
|
||||||
"npm:zod@4.1.9": "4.1.9"
|
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"@babel/code-frame@7.27.1": {
|
"@babel/code-frame@7.27.1": {
|
||||||
@@ -455,10 +453,6 @@
|
|||||||
"integrity": "sha512-aIG8W3TOmW+lKdAJA5w56qASu9EiUmBXbhW6eAlSEUBid+KVESGqQygFFg+awt/c8K+qobVM6M/u3SbIy0NyUQ==",
|
"integrity": "sha512-aIG8W3TOmW+lKdAJA5w56qASu9EiUmBXbhW6eAlSEUBid+KVESGqQygFFg+awt/c8K+qobVM6M/u3SbIy0NyUQ==",
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.14.tgz"
|
"tarball": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.14.tgz"
|
||||||
},
|
},
|
||||||
"@jsr/std__cli@1.0.22": {
|
|
||||||
"integrity": "sha512-PQkNPxuo8nOby8RgRxaLrQ9UAem/cCYKZYznV1fISZAzBbxMVBfsIeHA9FxMH0OUuRcu4ReEZ9QudeGg6xLdvw==",
|
|
||||||
"tarball": "https://npm.jsr.io/~/11/@jsr/std__cli/1.0.22.tgz"
|
|
||||||
},
|
|
||||||
"@jsr/std__data-structures@1.0.9": {
|
"@jsr/std__data-structures@1.0.9": {
|
||||||
"integrity": "sha512-+mT4Nll6fx+CPNqrlC+huhIOYNSMS+KUdJ4B8NujiQrh/bq++ds5PXpEsfV5EPR+YuWcuDGG0P1DE+Rednd7Wg==",
|
"integrity": "sha512-+mT4Nll6fx+CPNqrlC+huhIOYNSMS+KUdJ4B8NujiQrh/bq++ds5PXpEsfV5EPR+YuWcuDGG0P1DE+Rednd7Wg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -597,108 +591,113 @@
|
|||||||
"@rolldown/pluginutils@1.0.0-beta.27": {
|
"@rolldown/pluginutils@1.0.0-beta.27": {
|
||||||
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="
|
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="
|
||||||
},
|
},
|
||||||
"@rollup/rollup-android-arm-eabi@4.51.0": {
|
"@rollup/rollup-android-arm-eabi@4.52.0": {
|
||||||
"integrity": "sha512-VyfldO8T/C5vAXBGIobrAnUE+VJNVLw5z9h4NgSDq/AJZWt/fXqdW+0PJbk+M74xz7yMDRiHtlsuDV7ew6K20w==",
|
"integrity": "sha512-VxDYCDqOaR7NXzAtvRx7G1u54d2kEHopb28YH/pKzY6y0qmogP3gG7CSiWsq9WvDFxOQMpNEyjVAHZFXfH3o/A==",
|
||||||
"os": ["android"],
|
"os": ["android"],
|
||||||
"cpu": ["arm"]
|
"cpu": ["arm"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-android-arm64@4.51.0": {
|
"@rollup/rollup-android-arm64@4.52.0": {
|
||||||
"integrity": "sha512-Z3ujzDZgsEVSokgIhmOAReh9SGT2qloJJX2Xo1Q3nPU1EhCXrV0PbpR3r7DWRgozqnjrPZQkLe5cgBPIYp70Vg==",
|
"integrity": "sha512-pqDirm8koABIKvzL59YI9W9DWbRlTX7RWhN+auR8HXJxo89m4mjqbah7nJZjeKNTNYopqL+yGg+0mhCpf3xZtQ==",
|
||||||
"os": ["android"],
|
"os": ["android"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-darwin-arm64@4.51.0": {
|
"@rollup/rollup-darwin-arm64@4.52.0": {
|
||||||
"integrity": "sha512-T3gskHgArUdR6TCN69li5VELVAZK+iQ4iwMoSMNYixoj+56EC9lTj35rcxhXzIJt40YfBkvDy3GS+t5zh7zM6g==",
|
"integrity": "sha512-YCdWlY/8ltN6H78HnMsRHYlPiKvqKagBP1r+D7SSylxX+HnsgXGCmLiV3Y4nSyY9hW8qr8U9LDUx/Lo7M6MfmQ==",
|
||||||
"os": ["darwin"],
|
"os": ["darwin"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-darwin-x64@4.51.0": {
|
"@rollup/rollup-darwin-x64@4.52.0": {
|
||||||
"integrity": "sha512-Hh7n/fh0g5UjH6ATDF56Qdf5bzdLZKIbhp5KftjMYG546Ocjeyg15dxphCpH1FFY2PJ2G6MiOVL4jMq5VLTyrQ==",
|
"integrity": "sha512-z4nw6y1j+OOSGzuVbSWdIp1IUks9qNw4dc7z7lWuWDKojY38VMWBlEN7F9jk5UXOkUcp97vA1N213DF+Lz8BRg==",
|
||||||
"os": ["darwin"],
|
"os": ["darwin"],
|
||||||
"cpu": ["x64"]
|
"cpu": ["x64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-freebsd-arm64@4.51.0": {
|
"@rollup/rollup-freebsd-arm64@4.52.0": {
|
||||||
"integrity": "sha512-0EddADb6FBvfqYoxwVom3hAbAvpSVUbZqmR1wmjk0MSZ06hn/UxxGHKRqEQDMkts7XiZjejVB+TLF28cDTU+gA==",
|
"integrity": "sha512-Q/dv9Yvyr5rKlK8WQJZVrp5g2SOYeZUs9u/t2f9cQ2E0gJjYB/BWoedXfUT0EcDJefi2zzVfhcOj8drWCzTviw==",
|
||||||
"os": ["freebsd"],
|
"os": ["freebsd"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-freebsd-x64@4.51.0": {
|
"@rollup/rollup-freebsd-x64@4.52.0": {
|
||||||
"integrity": "sha512-MpqaEDLo3JuVPF+wWV4mK7V8akL76WCz8ndfz1aVB7RhvXFO3k7yT7eu8OEuog4VTSyNu5ibvN9n6lgjq/qLEQ==",
|
"integrity": "sha512-kdBsLs4Uile/fbjZVvCRcKB4q64R+1mUq0Yd7oU1CMm1Av336ajIFqNFovByipciuUQjBCPMxwJhCgfG2re3rg==",
|
||||||
"os": ["freebsd"],
|
"os": ["freebsd"],
|
||||||
"cpu": ["x64"]
|
"cpu": ["x64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-arm-gnueabihf@4.51.0": {
|
"@rollup/rollup-linux-arm-gnueabihf@4.52.0": {
|
||||||
"integrity": "sha512-WEWAGFNFFpvSWAIT3MYvxTkYHv/cJl9yWKpjhheg7ONfB0hetZt/uwBnM3GZqSHrk5bXCDYTFXg3jQyk/j7eXQ==",
|
"integrity": "sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["arm"]
|
"cpu": ["arm"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-arm-musleabihf@4.51.0": {
|
"@rollup/rollup-linux-arm-musleabihf@4.52.0": {
|
||||||
"integrity": "sha512-9bxtxj8QoAp++LOq5PGDGkEEOpCDk9rOEHUcXadnijedDH8IXrBt6PnBa4Y6NblvGWdoxvXZYghZLaliTCmAng==",
|
"integrity": "sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["arm"]
|
"cpu": ["arm"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-arm64-gnu@4.51.0": {
|
"@rollup/rollup-linux-arm64-gnu@4.52.0": {
|
||||||
"integrity": "sha512-DdqA+fARqIsfqDYkKo2nrWMp0kvu/wPJ2G8lZ4DjYhn+8QhrjVuzmsh7tTkhULwjvHTN59nWVzAixmOi6rqjNA==",
|
"integrity": "sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-arm64-musl@4.51.0": {
|
"@rollup/rollup-linux-arm64-musl@4.52.0": {
|
||||||
"integrity": "sha512-2XVRNzcUJE1UJua8P4a1GXS5jafFWE+pQ6zhUbZzptOu/70p1F6+0FTi6aGPd6jNtnJqGMjtBCXancC2dhYlWw==",
|
"integrity": "sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-loong64-gnu@4.51.0": {
|
"@rollup/rollup-linux-loong64-gnu@4.52.0": {
|
||||||
"integrity": "sha512-R8QhY0kLIPCAVXWi2yftDSpn7Jtejey/WhMoBESSfwGec5SKdFVupjxFlKoQ7clVRuaDpiQf7wNx3EBZf4Ey6g==",
|
"integrity": "sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["loong64"]
|
"cpu": ["loong64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-ppc64-gnu@4.51.0": {
|
"@rollup/rollup-linux-ppc64-gnu@4.52.0": {
|
||||||
"integrity": "sha512-I498RPfxx9cMv1KTHQ9tg2Ku1utuQm+T5B+Xro+WNu3FzAFSKp4awKfgMoZwjoPgNbaFGINaOM25cQW6WuBhiQ==",
|
"integrity": "sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["ppc64"]
|
"cpu": ["ppc64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-riscv64-gnu@4.51.0": {
|
"@rollup/rollup-linux-riscv64-gnu@4.52.0": {
|
||||||
"integrity": "sha512-o8COudsb8lvtdm9ixg9aKjfX5aeoc2x9KGE7WjtrmQFquoCRZ9jtzGlonujE4WhvXFepTraWzT4RcwyDDeHXjA==",
|
"integrity": "sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["riscv64"]
|
"cpu": ["riscv64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-riscv64-musl@4.51.0": {
|
"@rollup/rollup-linux-riscv64-musl@4.52.0": {
|
||||||
"integrity": "sha512-0shJPgSXMdYzOQzpM5BJN2euXY1f8uV8mS6AnrbMcH2KrkNsbpMxWB1wp8UEdiJ1NtyBkCk3U/HfX5mEONBq6w==",
|
"integrity": "sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["riscv64"]
|
"cpu": ["riscv64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-s390x-gnu@4.51.0": {
|
"@rollup/rollup-linux-s390x-gnu@4.52.0": {
|
||||||
"integrity": "sha512-L7pV+ny7865jamSCQwyozBYjFRUKaTsPqDz7ClOtJCDu4paf2uAa0mrcHwSt4XxZP2ogFZS9uuitH3NXdeBEJA==",
|
"integrity": "sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["s390x"]
|
"cpu": ["s390x"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-x64-gnu@4.51.0": {
|
"@rollup/rollup-linux-x64-gnu@4.52.0": {
|
||||||
"integrity": "sha512-4YHhP+Rv3T3+H3TPbUvWOw5tuSwhrVhkHHZhk4hC9VXeAOKR26/IsUAT4FsB4mT+kfIdxxb1BezQDEg/voPO8A==",
|
"integrity": "sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["x64"]
|
"cpu": ["x64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-linux-x64-musl@4.51.0": {
|
"@rollup/rollup-linux-x64-musl@4.52.0": {
|
||||||
"integrity": "sha512-P7U7U03+E5w7WgJtvSseNLOX1UhknVPmEaqgUENFWfNxNBa1OhExT6qYGmyF8gepcxWSaSfJsAV5UwhWrYefdQ==",
|
"integrity": "sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["x64"]
|
"cpu": ["x64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-openharmony-arm64@4.51.0": {
|
"@rollup/rollup-openharmony-arm64@4.52.0": {
|
||||||
"integrity": "sha512-FuD8g3u9W6RPwdO1R45hZFORwa1g9YXEMesAKP/sOi7mDqxjbni8S3zAXJiDcRfGfGBqpRYVuH54Gu3FTuSoEw==",
|
"integrity": "sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==",
|
||||||
"os": ["openharmony"],
|
"os": ["openharmony"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-win32-arm64-msvc@4.51.0": {
|
"@rollup/rollup-win32-arm64-msvc@4.52.0": {
|
||||||
"integrity": "sha512-zST+FdMCX3QAYfmZX3dp/Fy8qLUetfE17QN5ZmmFGPrhl86qvRr+E9u2bk7fzkIXsfQR30Z7ZRS7WMryPPn4rQ==",
|
"integrity": "sha512-YQugafP/rH0eOOHGjmNgDURrpYHrIX0yuojOI8bwCyXwxC9ZdTd3vYkmddPX0oHONLXu9Rb1dDmT0VNpjkzGGw==",
|
||||||
"os": ["win32"],
|
"os": ["win32"],
|
||||||
"cpu": ["arm64"]
|
"cpu": ["arm64"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-win32-ia32-msvc@4.51.0": {
|
"@rollup/rollup-win32-ia32-msvc@4.52.0": {
|
||||||
"integrity": "sha512-U+qhoCVAZmTHCmUKxdQxw1jwAFNFXmOpMME7Npt5GTb1W/7itfgAgNluVOvyeuSeqW+dEQLFuNZF3YZPO8XkMg==",
|
"integrity": "sha512-zYdUYhi3Qe2fndujBqL5FjAFzvNeLxtIqfzNEVKD1I7C37/chv1VxhscWSQHTNfjPCrBFQMnynwA3kpZpZ8w4A==",
|
||||||
"os": ["win32"],
|
"os": ["win32"],
|
||||||
"cpu": ["ia32"]
|
"cpu": ["ia32"]
|
||||||
},
|
},
|
||||||
"@rollup/rollup-win32-x64-msvc@4.51.0": {
|
"@rollup/rollup-win32-x64-gnu@4.52.0": {
|
||||||
"integrity": "sha512-z6UpFzMhXSD8NNUfCi2HO+pbpSzSWIIPgb1TZsEZjmZYtk6RUIC63JYjlFBwbBZS3jt3f1q6IGfkj3g+GnBt2Q==",
|
"integrity": "sha512-fGk03kQylNaCOQ96HDMeT7E2n91EqvCDd3RwvT5k+xNdFCeMGnj5b5hEgTGrQuyidqSsD3zJDQ21QIaxXqTBJw==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@rollup/rollup-win32-x64-msvc@4.52.0": {
|
||||||
|
"integrity": "sha512-6iKDCVSIUQ8jPMoIV0OytRKniaYyy5EbY/RRydmLW8ZR3cEBhxbWl5ro0rkUNe0ef6sScvhbY79HrjRm8i3vDQ==",
|
||||||
"os": ["win32"],
|
"os": ["win32"],
|
||||||
"cpu": ["x64"]
|
"cpu": ["x64"]
|
||||||
},
|
},
|
||||||
@@ -848,8 +847,8 @@
|
|||||||
"tiny-warning"
|
"tiny-warning"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@tanstack/react-store@0.7.5_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
|
"@tanstack/react-store@0.7.7_react@19.1.1_react-dom@19.1.1__react@19.1.1": {
|
||||||
"integrity": "sha512-A+WZtEnHZpvbKXm8qR+xndNKywBLez2KKKKEQc7w0Qs45GvY1LpRI3BTZNmELwEVim8+Apf99iEDH2J+MUIzlQ==",
|
"integrity": "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@tanstack/store",
|
"@tanstack/store",
|
||||||
"react",
|
"react",
|
||||||
@@ -879,8 +878,8 @@
|
|||||||
"tiny-invariant"
|
"tiny-invariant"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@tanstack/store@0.7.5": {
|
"@tanstack/store@0.7.7": {
|
||||||
"integrity": "sha512-qd/OjkjaFRKqKU4Yjipaen/EOB9MyEg6Wr9fW103RBPACf1ZcKhbhcu2S5mj5IgdPib6xFIgCUti/mKVkl+fRw=="
|
"integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="
|
||||||
},
|
},
|
||||||
"@types/babel__core@7.20.5": {
|
"@types/babel__core@7.20.5": {
|
||||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
||||||
@@ -1766,6 +1765,10 @@
|
|||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
|
"nanoid@5.1.5": {
|
||||||
|
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
"natural-compare@1.4.0": {
|
"natural-compare@1.4.0": {
|
||||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
|
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
|
||||||
},
|
},
|
||||||
@@ -1825,7 +1828,7 @@
|
|||||||
"postcss@8.5.6": {
|
"postcss@8.5.6": {
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"nanoid",
|
"nanoid@3.3.11",
|
||||||
"picocolors",
|
"picocolors",
|
||||||
"source-map-js"
|
"source-map-js"
|
||||||
]
|
]
|
||||||
@@ -1871,8 +1874,8 @@
|
|||||||
"reusify@1.1.0": {
|
"reusify@1.1.0": {
|
||||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="
|
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="
|
||||||
},
|
},
|
||||||
"rollup@4.51.0": {
|
"rollup@4.52.0": {
|
||||||
"integrity": "sha512-7cR0XWrdp/UAj2HMY/Y4QQEUjidn3l2AY1wSeZoFjMbD8aOMPoV9wgTFYbrJpPzzvejDEini1h3CiUP8wLzxQA==",
|
"integrity": "sha512-+IuescNkTJQgX7AkIDtITipZdIGcWF0pnVvZTWStiazUmcGA2ag8dfg0urest2XlXUi9kuhfQ+qmdc5Stc3z7g==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/estree"
|
"@types/estree"
|
||||||
],
|
],
|
||||||
@@ -1897,6 +1900,7 @@
|
|||||||
"@rollup/rollup-openharmony-arm64",
|
"@rollup/rollup-openharmony-arm64",
|
||||||
"@rollup/rollup-win32-arm64-msvc",
|
"@rollup/rollup-win32-arm64-msvc",
|
||||||
"@rollup/rollup-win32-ia32-msvc",
|
"@rollup/rollup-win32-ia32-msvc",
|
||||||
|
"@rollup/rollup-win32-x64-gnu",
|
||||||
"@rollup/rollup-win32-x64-msvc",
|
"@rollup/rollup-win32-x64-msvc",
|
||||||
"fsevents"
|
"fsevents"
|
||||||
],
|
],
|
||||||
@@ -2168,8 +2172,8 @@
|
|||||||
"yocto-queue@0.1.0": {
|
"yocto-queue@0.1.0": {
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
||||||
},
|
},
|
||||||
"zod@4.1.9": {
|
"zod@4.1.11": {
|
||||||
"integrity": "sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ=="
|
"integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
@@ -2187,19 +2191,7 @@
|
|||||||
"api": {
|
"api": {
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@cerbos/http@0.23.1",
|
"npm:zod@4.1.11"
|
||||||
"npm:@jsr/felix__bcrypt@1.0.5",
|
|
||||||
"npm:@jsr/std__cli@1.0.22",
|
|
||||||
"npm:@jsr/std__dotenv@0.225.5",
|
|
||||||
"npm:@jsr/std__fs@1.0.19",
|
|
||||||
"npm:@jsr/std__path@1.1.2",
|
|
||||||
"npm:@jsr/valkyr__auth@2.1.4",
|
|
||||||
"npm:@jsr/valkyr__event-store@2.0.1",
|
|
||||||
"npm:@jsr/valkyr__inverse@1.0.1",
|
|
||||||
"npm:@jsr/valkyr__json-rpc@1.1.0",
|
|
||||||
"npm:cookie@1.0.2",
|
|
||||||
"npm:mongodb@6.20.0",
|
|
||||||
"npm:zod@4.1.9"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2227,29 +2219,91 @@
|
|||||||
"npm:typescript-eslint@8.44.0",
|
"npm:typescript-eslint@8.44.0",
|
||||||
"npm:typescript@5.9.2",
|
"npm:typescript@5.9.2",
|
||||||
"npm:vite@7.1.6",
|
"npm:vite@7.1.6",
|
||||||
"npm:zod@4.1.9"
|
"npm:zod@4.1.11"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"platform/models": {
|
"modules/identity": {
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:zod@4"
|
"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:zod@4.1.11"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/cerbos": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@cerbos/http@0.23.1",
|
||||||
|
"npm:@jsr/valkyr__auth@2.1.4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/config": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@jsr/std__dotenv@0.225.5",
|
||||||
|
"npm:zod@4.1.11"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/database": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@jsr/valkyr__inverse@1.0.1",
|
||||||
|
"npm:mongodb@6.20.0",
|
||||||
|
"npm:zod@4.1.11"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/logger": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@jsr/valkyr__event-store@2.0.1",
|
||||||
|
"npm:zod@4.1.11"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"platform/relay": {
|
"platform/relay": {
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"npm:@jsr/valkyr__auth@2.1.4",
|
||||||
"npm:path-to-regexp@8",
|
"npm:path-to-regexp@8",
|
||||||
"npm:zod@4"
|
"npm:zod@4.1.11"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/server": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@jsr/valkyr__json-rpc@1.1.0",
|
||||||
|
"npm:zod@4.1.11"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/socket": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@jsr/valkyr__json-rpc@1.1.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"platform/spec": {
|
"platform/spec": {
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:zod@4"
|
"npm:zod@4.1.11"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"platform/vault": {
|
||||||
|
"packageJson": {
|
||||||
|
"dependencies": [
|
||||||
|
"npm:jose@6.1.0",
|
||||||
|
"npm:nanoid@5.1.5"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ services:
|
|||||||
- "3593:3593"
|
- "3593:3593"
|
||||||
- "3594:3594"
|
- "3594:3594"
|
||||||
volumes:
|
volumes:
|
||||||
- ./cerbos/config.yaml:/config.yaml # <--- mount config
|
- ./platform/cerbos/config.yaml:/config.yaml # <--- mount config
|
||||||
- ./cerbos/policies:/data/policies # <--- mount policies
|
- ./platform/cerbos/policies:/data/policies # <--- mount policies
|
||||||
networks:
|
networks:
|
||||||
- localdev
|
- localdev
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AggregateRoot, getDate } from "@valkyr/event-store";
|
import { AggregateRoot, getDate } from "@valkyr/event-store";
|
||||||
|
|
||||||
|
import { EventRecord, EventStoreFactory } from "../event-store.ts";
|
||||||
import { CodeIdentity } from "../events/code.ts";
|
import { CodeIdentity } from "../events/code.ts";
|
||||||
import { EventRecord, EventStoreFactory } from "../events/mod.ts";
|
|
||||||
|
|
||||||
export class Code extends AggregateRoot<EventStoreFactory> {
|
export class Code extends AggregateRoot<EventStoreFactory> {
|
||||||
static override readonly name = "code";
|
static override readonly name = "code";
|
||||||
211
modules/identity/aggregates/identity.ts
Normal file
211
modules/identity/aggregates/identity.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
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 } } });
|
||||||
|
});
|
||||||
15
modules/identity/auth.ts
Normal file
15
modules/identity/auth.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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,6 +1,7 @@
|
|||||||
import { cerbos } from "./cerbos.ts";
|
import { cerbos } from "@platform/cerbos/client.ts";
|
||||||
|
import { Resource } from "@platform/cerbos/resources.ts";
|
||||||
|
|
||||||
import type { Principal } from "./principal.ts";
|
import type { Principal } from "./principal.ts";
|
||||||
import { Resource } from "./resources.ts";
|
|
||||||
|
|
||||||
export function access(principal: Principal) {
|
export function access(principal: Principal) {
|
||||||
return {
|
return {
|
||||||
9
modules/identity/auth/jwt.ts
Normal file
9
modules/identity/auth/jwt.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { config } from "../config.ts";
|
||||||
|
|
||||||
|
export const jwt = {
|
||||||
|
algorithm: "RS256",
|
||||||
|
privateKey: config.auth.privateKey,
|
||||||
|
publicKey: config.auth.publicKey,
|
||||||
|
issuer: "http://localhost",
|
||||||
|
audience: "http://localhost",
|
||||||
|
};
|
||||||
32
modules/identity/auth/principal.ts
Normal file
32
modules/identity/auth/principal.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { HttpAdapter, makeClient } from "@platform/relay";
|
||||||
|
import { PrincipalProvider } from "@valkyr/auth";
|
||||||
|
|
||||||
|
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, {}, async function (id: string) {
|
||||||
|
const response = await identity.resolve({ params: { id } });
|
||||||
|
if ("data" in response) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
roles: response.data.roles,
|
||||||
|
attributes: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Principal = typeof principal.$principal;
|
||||||
53
modules/identity/client.ts
Normal file
53
modules/identity/client.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const identity = makeClient(
|
||||||
|
{
|
||||||
|
adapter: new HttpAdapter({
|
||||||
|
url: config.url,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
register,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
getById,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
me,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
login: {
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
email: loginByEmail,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
password: loginByPassword,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
code: loginByCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
59
modules/identity/config.ts
Normal file
59
modules/identity/config.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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 = {
|
||||||
|
url: getEnvironmentVariable({
|
||||||
|
key: "IDENTITY_SERVICE_URL",
|
||||||
|
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,46 +1,30 @@
|
|||||||
import { Account, fromAccountDocument } from "@platform/models/account.ts";
|
import { getDatabaseAccessor } from "@platform/database/accessor.ts";
|
||||||
import { PasswordStrategy } from "@platform/spec/auth/strategies.ts";
|
|
||||||
|
|
||||||
import { db, takeOne } from "./database.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`);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
| Accounts
|
| Identity
|
||||||
|--------------------------------------------------------------------------------
|
|--------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a single account by its primary identifier.
|
* Retrieve a single account by its primary identifier.
|
||||||
*
|
*
|
||||||
* @param id - Account identifier.
|
* @param id - Unique identity.
|
||||||
*/
|
*/
|
||||||
export async function getAccountById(id: string): Promise<Account | undefined> {
|
export async function getIdentityById(id: string): Promise<Identity | undefined> {
|
||||||
return db
|
return db
|
||||||
.collection("accounts")
|
.collection("identities")
|
||||||
.aggregate([
|
.findOne({ id })
|
||||||
{
|
.then((document) => parseIdentity(document));
|
||||||
$match: { id },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: "roles",
|
|
||||||
localField: "roles",
|
|
||||||
foreignField: "id",
|
|
||||||
as: "roles",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.toArray()
|
|
||||||
.then(fromAccountDocument)
|
|
||||||
.then(takeOne);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
| Auth
|
|
||||||
|--------------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get strategy details for the given password strategy alias.
|
* Get strategy details for the given password strategy alias.
|
||||||
*
|
*
|
||||||
@@ -49,7 +33,7 @@ export async function getAccountById(id: string): Promise<Account | undefined> {
|
|||||||
export async function getPasswordStrategyByAlias(
|
export async function getPasswordStrategyByAlias(
|
||||||
alias: string,
|
alias: string,
|
||||||
): Promise<({ accountId: string } & PasswordStrategy) | undefined> {
|
): Promise<({ accountId: string } & PasswordStrategy) | undefined> {
|
||||||
const account = await db.collection("accounts").findOne({
|
const account = await db.collection("identities").findOne({
|
||||||
strategies: {
|
strategies: {
|
||||||
$elemMatch: { type: "password", alias },
|
$elemMatch: { type: "password", alias },
|
||||||
},
|
},
|
||||||
7
modules/identity/errors.ts
Normal file
7
modules/identity/errors.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { ConflictError } from "@platform/relay";
|
||||||
|
|
||||||
|
export class IdentityEmailClaimedError extends ConflictError {
|
||||||
|
constructor(email: string) {
|
||||||
|
super(`Email '${email}' is already claimed by another identity.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
modules/identity/event-store.ts
Normal file
54
modules/identity/event-store.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { container } from "@platform/database/container.ts";
|
||||||
|
import { EventFactory, EventStore, Prettify, Projector } from "@valkyr/event-store";
|
||||||
|
import { MongoAdapter } from "@valkyr/event-store/mongo";
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Event Factory
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
const eventFactory = new EventFactory([
|
||||||
|
...(await import("./events/code.ts")).default,
|
||||||
|
...(await import("./events/identity.ts")).default,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Event Store
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const eventStore = new EventStore({
|
||||||
|
adapter: new MongoAdapter(() => container.get("mongo"), `identity:event-store`),
|
||||||
|
events: eventFactory,
|
||||||
|
snapshot: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Projector
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const projector = new Projector<EventStoreFactory>();
|
||||||
|
|
||||||
|
eventStore.onEventsInserted(async (records, { batch }) => {
|
||||||
|
if (batch !== undefined) {
|
||||||
|
await projector.pushMany(batch, records);
|
||||||
|
} else {
|
||||||
|
for (const record of records) {
|
||||||
|
await projector.push(record, { hydrated: false, outdated: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Events
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type EventStoreFactory = typeof eventFactory;
|
||||||
|
|
||||||
|
export type EventRecord = Prettify<EventStoreFactory["$events"][number]["$record"]>;
|
||||||
@@ -2,7 +2,7 @@ import { event } from "@valkyr/event-store";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
const CodeIdentitySchema = z.object({
|
const CodeIdentitySchema = z.object({
|
||||||
accountId: z.string(),
|
id: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
21
modules/identity/events/identity.ts
Normal file
21
modules/identity/events/identity.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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),
|
||||||
|
];
|
||||||
35
modules/identity/models/identity.ts
Normal file
35
modules/identity/models/identity.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Parsers
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const parseIdentity = makeDocumentParser(IdentitySchema);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
| Types
|
||||||
|
|--------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Identity = z.infer<typeof IdentitySchema>;
|
||||||
28
modules/identity/package.json
Normal file
28
modules/identity/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@modules/identity",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./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",
|
||||||
|
"zod": "4.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
apiVersion: api.cerbos.dev/v1
|
apiVersion: api.cerbos.dev/v1
|
||||||
resourcePolicy:
|
resourcePolicy:
|
||||||
resource: account
|
resource: identity
|
||||||
version: default
|
version: default
|
||||||
rules:
|
rules:
|
||||||
|
|
||||||
16
modules/identity/routes/identities/get/handle.ts
Normal file
16
modules/identity/routes/identities/get/handle.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ForbiddenError, NotFoundError } from "@platform/relay";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return new NotFoundError("Identity does not exist, or has been removed.");
|
||||||
|
}
|
||||||
|
const decision = await access.isAllowed({ kind: "identity", id: identity.id, attr: {} }, "read");
|
||||||
|
if (decision === false) {
|
||||||
|
return new ForbiddenError("You do not have permission to view this identity.");
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
});
|
||||||
12
modules/identity/routes/identities/get/spec.ts
Normal file
12
modules/identity/routes/identities/get/spec.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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")
|
||||||
|
.params({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
.errors([UnauthorizedError, ForbiddenError, NotFoundError])
|
||||||
|
.response(IdentitySchema);
|
||||||
12
modules/identity/routes/identities/me/handle.ts
Normal file
12
modules/identity/routes/identities/me/handle.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
5
modules/identity/routes/identities/me/spec.ts
Normal file
5
modules/identity/routes/identities/me/spec.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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]);
|
||||||
11
modules/identity/routes/identities/register/handle.ts
Normal file
11
modules/identity/routes/identities/register/handle.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
17
modules/identity/routes/identities/register/spec.ts
Normal file
17
modules/identity/routes/identities/register/spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
13
modules/identity/routes/identities/resolve/handle.ts
Normal file
13
modules/identity/routes/identities/resolve/handle.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
5
modules/identity/routes/identities/resolve/keys.ts
Normal file
5
modules/identity/routes/identities/resolve/keys.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { importVault } from "@platform/vault";
|
||||||
|
|
||||||
|
import { config } from "../../../config.ts";
|
||||||
|
|
||||||
|
export const vault = importVault(config.internal);
|
||||||
12
modules/identity/routes/identities/resolve/spec.ts
Normal file
12
modules/identity/routes/identities/resolve/spec.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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]);
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { code } from "@platform/spec/auth/routes.ts";
|
import { logger } from "@platform/logger";
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
|
|
||||||
import { auth, config } from "~libraries/auth/mod.ts";
|
import { Code } from "../../../aggregates/code.ts";
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
import { Identity } from "../../../aggregates/identity.ts";
|
||||||
import { Account } from "~stores/event-store/aggregates/account.ts";
|
import { auth } from "../../../auth.ts";
|
||||||
import { Code } from "~stores/event-store/aggregates/code.ts";
|
import { config } from "../../../config.ts";
|
||||||
import { eventStore } from "~stores/event-store/event-store.ts";
|
import { eventStore } from "../../../event-store.ts";
|
||||||
|
import route from "./spec.ts";
|
||||||
|
|
||||||
export default code.access("public").handle(async ({ params: { accountId, codeId, value }, query: { next } }) => {
|
export default route.access("public").handle(async ({ params: { identityId, codeId, value }, query: { next } }) => {
|
||||||
const code = await eventStore.aggregate.getByStream(Code, codeId);
|
const code = await eventStore.aggregate.getByStream(Code, codeId);
|
||||||
|
|
||||||
if (code === undefined) {
|
if (code === undefined) {
|
||||||
@@ -40,23 +41,23 @@ export default code.access("public").handle(async ({ params: { accountId, codeId
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code.identity.accountId !== accountId) {
|
if (code.identity.id !== identityId) {
|
||||||
return logger.info({
|
return logger.info({
|
||||||
type: "code:claimed",
|
type: "code:claimed",
|
||||||
session: false,
|
session: false,
|
||||||
message: "Invalid Account ID",
|
message: "Invalid Identity ID",
|
||||||
expected: code.identity.accountId,
|
expected: code.identity.id,
|
||||||
received: accountId,
|
received: identityId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await eventStore.aggregate.getByStream(Account, accountId);
|
const account = await eventStore.aggregate.getByStream(Identity, identityId);
|
||||||
if (account === undefined) {
|
if (account === undefined) {
|
||||||
return logger.info({
|
return logger.info({
|
||||||
type: "code:claimed",
|
type: "code:claimed",
|
||||||
session: false,
|
session: false,
|
||||||
message: "Account Not Found",
|
message: "Account Not Found",
|
||||||
expected: code.identity.accountId,
|
expected: code.identity.id,
|
||||||
received: undefined,
|
received: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
13
modules/identity/routes/login/code/spec.ts
Normal file
13
modules/identity/routes/login/code/spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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(),
|
||||||
|
})
|
||||||
|
.query({
|
||||||
|
next: z.string().optional(),
|
||||||
|
});
|
||||||
27
modules/identity/routes/login/email/handle.ts
Normal file
27
modules/identity/routes/login/email/handle.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { logger } from "@platform/logger";
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return logger.info({
|
||||||
|
type: "auth:email",
|
||||||
|
code: false,
|
||||||
|
message: "Identity Not Found",
|
||||||
|
received: email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const code = await eventStore.aggregate.from(Code).create({ id: identity.id }).save();
|
||||||
|
logger.info({
|
||||||
|
type: "auth:email",
|
||||||
|
data: {
|
||||||
|
code: code.id,
|
||||||
|
identityId: identity.id,
|
||||||
|
},
|
||||||
|
link: `${base}/api/v1/admin/auth/${identity.id}/code/${code.id}/${code.value}?next=${base}/admin`,
|
||||||
|
});
|
||||||
|
});
|
||||||
9
modules/identity/routes/login/email/spec.ts
Normal file
9
modules/identity/routes/login/email/spec.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { route } from "@platform/relay";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export default route.post("/api/v1/identities/login/email").body(
|
||||||
|
z.object({
|
||||||
|
base: z.url(),
|
||||||
|
email: z.email(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { logger } from "@platform/logger";
|
||||||
import { BadRequestError } from "@platform/relay";
|
import { BadRequestError } from "@platform/relay";
|
||||||
import { password as route } from "@platform/spec/auth/routes.ts";
|
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
|
|
||||||
import { config } from "~config";
|
import { auth } from "../../../auth.ts";
|
||||||
import { auth } from "~libraries/auth/mod.ts";
|
import { config } from "../../../config.ts";
|
||||||
import { password } from "~libraries/crypto/mod.ts";
|
import { password } from "../../../crypto/password.ts";
|
||||||
import { logger } from "~libraries/logger/mod.ts";
|
import { getPasswordStrategyByAlias } from "../../../database.ts";
|
||||||
import { getPasswordStrategyByAlias } from "~stores/read-store/methods.ts";
|
import route from "./spec.ts";
|
||||||
|
|
||||||
export default route.access("public").handle(async ({ body: { alias, password: userPassword } }) => {
|
export default route.access("public").handle(async ({ body: { alias, password: userPassword } }) => {
|
||||||
const strategy = await getPasswordStrategyByAlias(alias);
|
const strategy = await getPasswordStrategyByAlias(alias);
|
||||||
9
modules/identity/routes/login/password/spec.ts
Normal file
9
modules/identity/routes/login/password/spec.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { route } from "@platform/relay";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
export default route.post("/api/v1/identities/login/password").body(
|
||||||
|
z.object({
|
||||||
|
alias: z.string(),
|
||||||
|
password: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -30,4 +30,8 @@ export const StrategySchema = z.discriminatedUnion("type", [
|
|||||||
PasskeyStrategySchema,
|
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>;
|
export type Strategy = z.infer<typeof StrategySchema>;
|
||||||
96
modules/identity/server.ts
Normal file
96
modules/identity/server.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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/login/code/handle.ts")).default,
|
||||||
|
(await import("./routes/login/email/handle.ts")).default,
|
||||||
|
(await import("./routes/login/password/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
38
modules/identity/types.d.ts
vendored
Normal file
38
modules/identity/types.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import "@platform/relay";
|
||||||
|
import "@platform/storage";
|
||||||
|
|
||||||
|
import type { Access } from "./auth/access.ts";
|
||||||
|
import type { Principal } from "./auth/principal.ts";
|
||||||
|
|
||||||
|
declare module "@platform/storage" {
|
||||||
|
interface StorageContext {
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
principal?: Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
access?: Access;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@platform/relay" {
|
||||||
|
interface ServerContext {
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
principal: Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
access: Access;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
platform/cerbos/package.json
Normal file
10
platform/cerbos/package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@platform/cerbos",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@cerbos/http": "0.23.1",
|
||||||
|
"@valkyr/auth": "npm:@jsr/valkyr__auth@2.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
platform/cerbos/policies/identity.yaml
Normal file
47
platform/cerbos/policies/identity.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 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
|
||||||
@@ -2,7 +2,7 @@ import { ResourceRegistry } from "@valkyr/auth";
|
|||||||
|
|
||||||
export const resources = new ResourceRegistry([
|
export const resources = new ResourceRegistry([
|
||||||
{
|
{
|
||||||
kind: "account",
|
kind: "identity",
|
||||||
attr: {},
|
attr: {},
|
||||||
},
|
},
|
||||||
] as const);
|
] as const);
|
||||||
10
platform/config/dotenv.ts
Normal file
10
platform/config/dotenv.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { load } from "@std/dotenv";
|
||||||
|
|
||||||
|
const env = await load();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO ...
|
||||||
|
*/
|
||||||
|
export function getDotEnvVariable(key: string): string {
|
||||||
|
return env[key] ?? Deno.env.get(key);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user