diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8cc007ad --- /dev/null +++ b/.dockerignore @@ -0,0 +1,39 @@ + +# Node cruft +node_modules +.pnpm +dist +build +.cache +turbo +out + +# Dependency locks from other ecosystems +package-lock.json +yarn.lock + +# Logs & temp +*.log +*.tsbuildinfo +.vscode +.idea +.env +.env.* + +# OS junk +.DS_Store +Thumbs.db + +# Local-only files +coverage +*.local.* +*.swp +*.swo + +# Git stuff +.git +.gitignore + +# Prevent docker context pollution from other packages in monorepo +apps/*/node_modules +packages/*/node_modules diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..acdfda8a --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Neo4j Configuration +NEO4J_URI=bolt://neo4j:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=your_secure_password_here + +# eVault Configuration +PORT=4000 \ No newline at end of file diff --git a/.github/workflows/tests-evault-core.yml b/.github/workflows/tests-evault-core.yml index 4d4d852e..0b600c9d 100644 --- a/.github/workflows/tests-evault-core.yml +++ b/.github/workflows/tests-evault-core.yml @@ -8,7 +8,7 @@ on: pull_request: branches: [main] paths: - - 'infrastructure/w3id/**' + - 'infrastructure/evault-core/**' jobs: test: diff --git a/docker/Dockerfile.evault b/docker/Dockerfile.evault new file mode 100644 index 00000000..2cbee198 --- /dev/null +++ b/docker/Dockerfile.evault @@ -0,0 +1,23 @@ +FROM node:22-slim AS deps +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +COPY . /app +WORKDIR /app +RUN npm i -g corepack@latest +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm turbo prune evault-core --docker --use-gitignore=false +RUN mkdir /out +RUN cp -R ./out/full/* /out/ +RUN cp -R ./out/json/* /out/ +RUN cp ./out/pnpm-lock.yaml /out/pnpm-lock.yaml +RUN cp -R node_modules/ /out/ + + +FROM node:22-slim AS core-api +WORKDIR /app +RUN npm i -g corepack@latest +COPY --from=deps /out/ /app +EXPOSE 4000 +workdir /app/infrastructure/evault-core +CMD ["pnpm", "dev"] diff --git a/evault.docker-compose.yml b/evault.docker-compose.yml new file mode 100644 index 00000000..2228175b --- /dev/null +++ b/evault.docker-compose.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + evault: + build: + context: . + dockerfile: ./docker/Dockerfile.evault + ports: + - "4000:4000" + environment: + - NEO4J_URI=${NEO4J_URI} + - NEO4J_USER=${NEO4J_USER} + - NEO4J_PASSWORD=${NEO4J_PASSWORD} + networks: + - graphnet + depends_on: + - neo4j + develop: + watch: + - action: sync+restart + path: ./infrastructure/evault-core/ + target: /app/infrastructure/evault-core + ignore: + - node_modules + - action: rebuild + path: ./infrastructure/evault-core/package.json + - action: rebuild + path: ./.env + + neo4j: + image: neo4j:5.15 + container_name: evault-neo4j + ports: + - "7474:7474" # HTTP + - "7687:7687" # Bolt + environment: + - NEO4J_AUTH=${NEO4J_USER}/${NEO4J_PASSWORD} + volumes: + - neo4j_data:/data + - neo4j_logs:/log + networks: + - graphnet + +volumes: + neo4j_data: + neo4j_logs: + +networks: + graphnet: + driver: bridge diff --git a/infrastructure/evault-core/docker-compose.yml b/infrastructure/evault-core/docker-compose.yml deleted file mode 100644 index 48f9d70d..00000000 --- a/infrastructure/evault-core/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.8' - -services: - neo4j: - image: neo4j:5.15 - container_name: neo4j - ports: - - "7474:7474" - - "7687:7687" - environment: - - NEO4J_AUTH=neo4j/testpass - volumes: - - neo4j_data:/data - networks: - - graphnet - -volumes: - neo4j_data: - -networks: - graphnet: - driver: bridge diff --git a/infrastructure/evault-core/package.json b/infrastructure/evault-core/package.json index a8feaa5c..d3d2ae20 100644 --- a/infrastructure/evault-core/package.json +++ b/infrastructure/evault-core/package.json @@ -1,32 +1,35 @@ { - "name": "evault-core", - "version": "0.1.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "vitest --config vitest.config.ts" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@types/json-schema": "^7.0.15", - "@types/node": "^22.13.10", - "dotenv": "^16.5.0", - "testcontainers": "^10.24.2", - "tsx": "^4.19.3", - "typescript": "^5.8.3", - "uuid": "^11.1.0", - "vitest": "^3.0.9" - }, - "dependencies": { - "@testcontainers/neo4j": "^10.24.2", - "graphql": "^16.10.0", - "graphql-type-json": "^0.3.2", - "graphql-voyager": "^2.1.0", - "graphql-yoga": "^5.13.4", - "json-schema": "^0.4.0", - "neo4j-driver": "^5.28.1", - "w3id": "workspace:*" - } + "name": "evault-core", + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "vitest --config vitest.config.ts", + "build": "tsc", + "dev": "node --watch --import tsx src/evault.ts" + }, + "packageManager": "pnpm@10.6.5", + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/json-schema": "^7.0.15", + "@types/node": "^22.13.10", + "dotenv": "^16.5.0", + "testcontainers": "^10.24.2", + "tsx": "^4.19.3", + "typescript": "^5.8.3", + "uuid": "^11.1.0", + "vitest": "^3.0.9" + }, + "dependencies": { + "@testcontainers/neo4j": "^10.24.2", + "graphql": "^16.10.0", + "graphql-type-json": "^0.3.2", + "graphql-voyager": "^2.1.0", + "graphql-yoga": "^5.13.4", + "json-schema": "^0.4.0", + "neo4j-driver": "^5.28.1", + "w3id": "workspace:*" + } } diff --git a/infrastructure/evault-core/src/db/db.service.ts b/infrastructure/evault-core/src/db/db.service.ts index 188533f0..a6efadb9 100644 --- a/infrastructure/evault-core/src/db/db.service.ts +++ b/infrastructure/evault-core/src/db/db.service.ts @@ -16,11 +16,14 @@ import { * with proper type handling and access control. */ export class DbService { + private driver: Driver; + /** * Creates a new instance of the DbService. - * @param driver - The Neo4j driver instance */ - constructor(private driver: Driver) {} + constructor(driver: Driver) { + this.driver = driver; + } /** * Executes a Cypher query with the given parameters. @@ -174,6 +177,59 @@ export class DbService { }); } + /** + * Finds multiple meta-envelopes by an array of IDs. + * @param ids - Array of MetaEnvelope IDs + * @returns Array of meta-envelopes with envelopes and parsed payload + */ + async findMetaEnvelopesByIds< + T extends Record = Record, + >(ids: string[]): Promise[]> { + if (!ids.length) return []; + + const result = await this.runQuery( + ` + MATCH (m:MetaEnvelope)-[:LINKS_TO]->(e:Envelope) + WHERE m.id IN $ids + RETURN m.id AS id, m.ontology AS ontology, m.acl AS acl, collect(e) AS envelopes + `, + { ids }, + ); + + return result.records.map((record): MetaEnvelopeResult => { + const envelopes = record + .get("envelopes") + .map((node: any): Envelope => { + const props = node.properties; + return { + id: props.id, + ontology: props.ontology, + value: deserializeValue( + props.value, + props.valueType, + ) as T[keyof T], + valueType: props.valueType, + }; + }); + + const parsed = envelopes.reduce( + (acc: T, env: Envelope) => { + (acc as any)[env.ontology] = env.value; + return acc; + }, + {} as T, + ); + + return { + id: record.get("id"), + ontology: record.get("ontology"), + acl: record.get("acl"), + envelopes, + parsed, + }; + }); + } + /** * Finds a meta-envelope by its ID. * @param id - The ID of the meta-envelope to find @@ -226,20 +282,53 @@ export class DbService { } /** - * Finds all meta-envelope IDs for a given ontology. + * Finds all meta-envelopes by ontology with their envelopes and parsed payload. * @param ontology - The ontology to search for - * @returns Array of meta-envelope IDs + * @returns Array of meta-envelopes */ - async findMetaEnvelopesByOntology(ontology: string): Promise { + async findMetaEnvelopesByOntology< + T extends Record = Record, + >(ontology: string): Promise[]> { const result = await this.runQuery( ` - MATCH (m:MetaEnvelope { ontology: $ontology }) - RETURN m.id AS id - `, + MATCH (m:MetaEnvelope { ontology: $ontology })-[:LINKS_TO]->(e:Envelope) + RETURN m.id AS id, m.ontology AS ontology, m.acl AS acl, collect(e) AS envelopes + `, { ontology }, ); - return result.records.map((r) => r.get("id")); + return result.records.map((record) => { + const envelopes = record + .get("envelopes") + .map((node: any): Envelope => { + const properties = node.properties; + return { + id: properties.id, + ontology: properties.ontology, + value: deserializeValue( + properties.value, + properties.valueType, + ) as T[keyof T], + valueType: properties.valueType, + }; + }); + + const parsed = envelopes.reduce( + (acc: T, envelope: Envelope) => { + (acc as any)[envelope.ontology] = envelope.value; + return acc; + }, + {} as T, + ); + + return { + id: record.get("id"), + ontology: record.get("ontology"), + acl: record.get("acl"), + envelopes, + parsed, + }; + }); } /** diff --git a/infrastructure/evault-core/src/evault.ts b/infrastructure/evault-core/src/evault.ts index 7a930b6c..8e84a350 100644 --- a/infrastructure/evault-core/src/evault.ts +++ b/infrastructure/evault-core/src/evault.ts @@ -1,12 +1,44 @@ -import neo4j from "neo4j-driver"; +import { Server } from "http"; import { DbService } from "./db/db.service"; import { GraphQLServer } from "./protocol/graphql-server"; +import dotenv from "dotenv"; +import path from "path"; +import neo4j from "neo4j-driver"; + +dotenv.config({ path: path.resolve(__dirname, "../../../.env") }); + +class EVault { + server: Server; + + constructor() { + const uri = process.env.NEO4J_URI || "bolt://localhost:7687"; + const user = process.env.NEO4J_USER || "neo4j"; + const password = process.env.NEO4J_PASSWORD || "neo4j"; + + if ( + !process.env.NEO4J_URI || + !process.env.NEO4J_USER || + !process.env.NEO4J_PASSWORD + ) { + console.warn( + "Using default Neo4j connection parameters. Set NEO4J_URI, NEO4J_USER, and NEO4J_PASSWORD environment variables for custom configuration.", + ); + } + + const driver = neo4j.driver(uri, neo4j.auth.basic(user, password)); + const dbService = new DbService(driver); + const gqlServer = new GraphQLServer(dbService); + this.server = gqlServer.server as Server; + } -async function startEVault() { - const uri = `bolt://localhost:7687`; - const driver = neo4j.driver(uri, neo4j.auth.basic("neo4j", "testpass")); - const dbService = new DbService(driver); - new GraphQLServer(dbService); + start() { + const port = process.env.PORT ?? 4000; + this.server.listen(port, () => { + console.log(`GraphQL Server started on http://localhost:${port}`); + console.log(`Voyager started on http://localhost:${port}`); + }); + } } -startEVault(); +const evault = new EVault(); +evault.start(); diff --git a/infrastructure/evault-core/src/protocol/examples/examples.ts b/infrastructure/evault-core/src/protocol/examples/examples.ts new file mode 100644 index 00000000..455f16b4 --- /dev/null +++ b/infrastructure/evault-core/src/protocol/examples/examples.ts @@ -0,0 +1,112 @@ +export const exampleQueries = ` +# Welcome to eVault GraphQL Playground! +# This GraphiQL is pre-loaded with real examples you can try instantly. +# +# Each object is stored as a MetaEnvelope, which is a flat graph of Envelopes. +# You can issue credentials, store data, update specific fields, or search by +# content. +# +# πŸ‘‡ Scroll down and uncomment the examples you want to run. Let's go πŸš€ + +################################################################################ +# βœ… 1. Store a SocialMediaPost +################################################################################ + +# mutation { +# storeMetaEnvelope(input: { +# ontology: "SocialMediaPost", +# payload: { +# text: "gm world!", +# image: "https://example.com/pic.jpg", +# dateCreated: "2025-04-10T10:00:00Z", +# userLikes: ["@user1", "@user2"] +# }, +# acl: ["@d1fa5cb1-6178-534b-a096-59794d485f65"] # Who can access this object +# }) { +# metaEnvelope { +# id +# ontology +# parsed +# } +# envelopes { +# id +# ontology +# value +# valueType +# } +# } +# } + +################################################################################ +# πŸ” 2. Retrieve a MetaEnvelope by ID +################################################################################ + +# query { +# getMetaEnvelopeById(id: "YOUR_META_ENVELOPE_ID_HERE") { +# id +# ontology +# parsed +# envelopes { +# id +# ontology +# value +# valueType +# } +# } +# } + +################################################################################ +# πŸ”Ž 3. Search MetaEnvelopes by Ontology + Keyword +################################################################################ + +# query { +# searchMetaEnvelopes(ontology: "SocialMediaPost", term: "gm") { +# id +# parsed +# envelopes { +# ontology +# value +# } +# } +# } + +################################################################################ +# πŸ“š 4. Find All MetaEnvelope IDs by Ontology +################################################################################ + +# query { +# findMetaEnvelopesByOntology(ontology: "SocialMediaPost") +# } + +################################################################################ +# ✏️ 5. Update a Single Envelope’s Value +################################################################################ + +# mutation { +# updateEnvelopeValue( +# envelopeId: "YOUR_ENVELOPE_ID_HERE", +# newValue: "Updated value" +# ) +# } + +################################################################################ +# 🧼 6. Delete a MetaEnvelope (and all linked Envelopes) +################################################################################ + +# mutation { +# deleteMetaEnvelope(id: "YOUR_META_ENVELOPE_ID_HERE") +# } + +################################################################################ +# πŸ“¦ 7. List All Envelopes in the System +################################################################################ + +# query { +# getAllEnvelopes { +# id +# ontology +# value +# valueType +# } +# } +`; diff --git a/infrastructure/evault-core/src/protocol/graphql-server.ts b/infrastructure/evault-core/src/protocol/graphql-server.ts index d0696cf5..6a1c513c 100644 --- a/infrastructure/evault-core/src/protocol/graphql-server.ts +++ b/infrastructure/evault-core/src/protocol/graphql-server.ts @@ -1,138 +1,147 @@ import { createSchema, createYoga, YogaInitialContext } from "graphql-yoga"; -import { createServer } from "http"; +import { createServer, Server } from "http"; import { typeDefs } from "./typedefs"; import { renderVoyagerPage } from "graphql-voyager/middleware"; import { getJWTHeader } from "w3id"; import { DbService } from "../db/db.service"; import { VaultAccessGuard, VaultContext } from "./vault-access-guard"; import { GraphQLSchema } from "graphql"; +import { exampleQueries } from "./examples/examples"; export class GraphQLServer { - private db: DbService; - private accessGuard: VaultAccessGuard; - private schema: GraphQLSchema = createSchema({ - typeDefs, - resolvers: {}, - }); - - constructor(db: DbService) { - this.db = db; - this.accessGuard = new VaultAccessGuard(db); - this.instantiateServer(); - } + private db: DbService; + private accessGuard: VaultAccessGuard; + private schema: GraphQLSchema = createSchema({ + typeDefs, + resolvers: {}, + }); + server?: Server; - public getSchema(): GraphQLSchema { - return this.schema; - } + constructor(db: DbService) { + this.db = db; + this.accessGuard = new VaultAccessGuard(db); + this.instantiateServer(); + } - private instantiateServer() { - const resolvers = { - JSON: require("graphql-type-json"), + public getSchema(): GraphQLSchema { + return this.schema; + } - Query: { - getMetaEnvelopeById: this.accessGuard.middleware( - (_: any, { id }: { id: string }) => { - return this.db.findMetaEnvelopeById(id); - } - ), - findMetaEnvelopesByOntology: this.accessGuard.middleware( - (_: any, { ontology }: { ontology: string }) => { - return this.db.findMetaEnvelopesByOntology(ontology); - } - ), - searchMetaEnvelopes: this.accessGuard.middleware( - (_: any, { ontology, term }: { ontology: string; term: string }) => { - return this.db.findMetaEnvelopesBySearchTerm(ontology, term); - } - ), - getAllEnvelopes: this.accessGuard.middleware(() => { - return this.db.getAllEnvelopes(); - }), - }, + private instantiateServer() { + const resolvers = { + JSON: require("graphql-type-json"), - Mutation: { - storeMetaEnvelope: this.accessGuard.middleware( - async ( - _: any, - { - input, - }: { - input: { - ontology: string; - payload: any; - acl: string[]; - }; - } - ) => { - const result = await this.db.storeMetaEnvelope( - { - ontology: input.ontology, - payload: input.payload, - acl: input.acl, - }, - input.acl - ); - return result; - } - ), - deleteMetaEnvelope: this.accessGuard.middleware( - async (_: any, { id }: { id: string }) => { - await this.db.deleteMetaEnvelope(id); - return true; - } - ), - updateEnvelopeValue: this.accessGuard.middleware( - async ( - _: any, - { envelopeId, newValue }: { envelopeId: string; newValue: any } - ) => { - await this.db.updateEnvelopeValue(envelopeId, newValue); - return true; - } - ), - }, - }; + Query: { + getMetaEnvelopeById: this.accessGuard.middleware( + (_: any, { id }: { id: string }) => { + return this.db.findMetaEnvelopeById(id); + }, + ), + findMetaEnvelopesByOntology: this.accessGuard.middleware( + (_: any, { ontology }: { ontology: string }) => { + return this.db.findMetaEnvelopesByOntology(ontology); + }, + ), + searchMetaEnvelopes: this.accessGuard.middleware( + ( + _: any, + { ontology, term }: { ontology: string; term: string }, + ) => { + return this.db.findMetaEnvelopesBySearchTerm( + ontology, + term, + ); + }, + ), + getAllEnvelopes: this.accessGuard.middleware(() => { + return this.db.getAllEnvelopes(); + }), + }, - this.schema = createSchema({ - typeDefs, - resolvers, - }); + Mutation: { + storeMetaEnvelope: this.accessGuard.middleware( + async ( + _: any, + { + input, + }: { + input: { + ontology: string; + payload: any; + acl: string[]; + }; + }, + ) => { + const result = await this.db.storeMetaEnvelope( + { + ontology: input.ontology, + payload: input.payload, + acl: input.acl, + }, + input.acl, + ); + return result; + }, + ), + deleteMetaEnvelope: this.accessGuard.middleware( + async (_: any, { id }: { id: string }) => { + await this.db.deleteMetaEnvelope(id); + return true; + }, + ), + updateEnvelopeValue: this.accessGuard.middleware( + async ( + _: any, + { + envelopeId, + newValue, + }: { envelopeId: string; newValue: any }, + ) => { + await this.db.updateEnvelopeValue(envelopeId, newValue); + return true; + }, + ), + }, + }; - const yoga = createYoga({ - schema: this.schema, - context: async ({ request }) => { - const authHeader = request.headers.get("authorization") ?? ""; - const token = authHeader.replace("Bearer ", ""); + this.schema = createSchema({ + typeDefs, + resolvers, + }); - if (token) { - const id = getJWTHeader(token).kid?.split("#")[0]; - return { - currentUser: id ?? null, - }; - } + const yoga = createYoga({ + schema: this.schema, + graphiql: { + defaultQuery: exampleQueries, + }, + context: async ({ request }) => { + const authHeader = request.headers.get("authorization") ?? ""; + const token = authHeader.replace("Bearer ", ""); - return { - currentUser: null, - }; - }, - }); + if (token) { + const id = getJWTHeader(token).kid?.split("#")[0]; + return { + currentUser: id ?? null, + }; + } - const server = createServer((req, res) => { - if (req.url === "/voyager") { - res.writeHead(200, { "Content-Type": "text/html" }); - res.end( - renderVoyagerPage({ - endpointUrl: "/graphql", - }) - ); - } else { - yoga(req, res); - } - }); + return { + currentUser: null, + }; + }, + }); - server.listen(4000, () => { - console.log("πŸš€ GraphQL at http://localhost:4000/graphql"); - console.log("πŸ›°οΈ Voyager at http://localhost:4000/voyager"); - }); - } + this.server = createServer((req, res) => { + if (req.url === "/voyager") { + res.writeHead(200, { "Content-Type": "text/html" }); + res.end( + renderVoyagerPage({ + endpointUrl: "/graphql", + }), + ); + } else { + yoga(req, res); + } + }); + } } diff --git a/infrastructure/evault-core/src/protocol/typedefs.ts b/infrastructure/evault-core/src/protocol/typedefs.ts index 2fb6fdde..fdd0b7cf 100644 --- a/infrastructure/evault-core/src/protocol/typedefs.ts +++ b/infrastructure/evault-core/src/protocol/typedefs.ts @@ -1,42 +1,42 @@ // GraphQL Schema Definition export const typeDefs = /* GraphQL */ ` - scalar JSON + scalar JSON - type Envelope { - id: String! - ontology: String! - value: JSON - valueType: String - } + type Envelope { + id: String! + ontology: String! + value: JSON + valueType: String + } - type MetaEnvelope { - id: String! - ontology: String! - envelopes: [Envelope!]! - parsed: JSON - } + type MetaEnvelope { + id: String! + ontology: String! + envelopes: [Envelope!]! + parsed: JSON + } - type StoreMetaEnvelopeResult { - metaEnvelope: MetaEnvelope! - envelopes: [Envelope!]! - } + type StoreMetaEnvelopeResult { + metaEnvelope: MetaEnvelope! + envelopes: [Envelope!]! + } - type Query { - getMetaEnvelopeById(id: String!): MetaEnvelope - findMetaEnvelopesByOntology(ontology: String!): [String!]! - searchMetaEnvelopes(ontology: String!, term: String!): [MetaEnvelope!]! - getAllEnvelopes: [Envelope!]! - } + type Query { + getMetaEnvelopeById(id: String!): MetaEnvelope + findMetaEnvelopesByOntology(ontology: String!): [MetaEnvelope!]! + searchMetaEnvelopes(ontology: String!, term: String!): [MetaEnvelope!]! + getAllEnvelopes: [Envelope!]! + } - input MetaEnvelopeInput { - ontology: String! - payload: JSON! - acl: [String!]! - } + input MetaEnvelopeInput { + ontology: String! + payload: JSON! + acl: [String!]! + } - type Mutation { - storeMetaEnvelope(input: MetaEnvelopeInput!): StoreMetaEnvelopeResult! - deleteMetaEnvelope(id: String!): Boolean! - updateEnvelopeValue(envelopeId: String!, newValue: JSON!): Boolean! - } + type Mutation { + storeMetaEnvelope(input: MetaEnvelopeInput!): StoreMetaEnvelopeResult! + deleteMetaEnvelope(id: String!): Boolean! + updateEnvelopeValue(envelopeId: String!, newValue: JSON!): Boolean! + } `; diff --git a/infrastructure/evault-core/src/protocol/vault-access-guard.ts b/infrastructure/evault-core/src/protocol/vault-access-guard.ts index 8c77589f..a08b8f07 100644 --- a/infrastructure/evault-core/src/protocol/vault-access-guard.ts +++ b/infrastructure/evault-core/src/protocol/vault-access-guard.ts @@ -1,5 +1,6 @@ import { YogaInitialContext } from "graphql-yoga"; import { DbService } from "../db/db.service"; +import { MetaEnvelope } from "../db/types"; export type VaultContext = YogaInitialContext & { currentUser: string | null; @@ -54,16 +55,13 @@ export class VaultAccessGuard { * @returns Promise - Filtered list of meta envelopes */ private async filterEnvelopesByAccess( - envelopes: any[], + envelopes: MetaEnvelope[], context: VaultContext, ): Promise { - if (!context.currentUser) { - return []; - } - const filteredEnvelopes = []; for (const envelope of envelopes) { - const hasAccess = await this.checkAccess(envelope.id, context); + const hasAccess = envelope.acl.includes("*") || + envelope.acl.includes(context.currentUser ?? ""); if (hasAccess) { filteredEnvelopes.push(this.filterACL(envelope)); } @@ -109,6 +107,7 @@ export class VaultAccessGuard { throw new Error("Access denied"); } + // console.log const result = await resolver(parent, args, context); return this.filterACL(result); }; diff --git a/infrastructure/evault-core/tests/evault.spec.ts b/infrastructure/evault-core/tests/evault.spec.ts index b78b2726..63361f66 100644 --- a/infrastructure/evault-core/tests/evault.spec.ts +++ b/infrastructure/evault-core/tests/evault.spec.ts @@ -1,4 +1,3 @@ -// βœ… Full aligned test suite for eVault import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { createYoga } from "graphql-yoga"; import { createServer } from "http"; @@ -32,6 +31,7 @@ describe("eVault E2E", () => { let driver; let w3id; let testEnvelopeId; + let port: number; const testOntology = "SocialMediaPost"; const testPayload = { @@ -68,7 +68,7 @@ describe("eVault E2E", () => { }); const httpServer = createServer(yoga); - const port = await getFreePort(); + port = await getFreePort(); await new Promise((resolve) => httpServer.listen(port, resolve)); server = httpServer; }); @@ -79,7 +79,7 @@ describe("eVault E2E", () => { }); const executeGraphQL = async (query, variables = {}, token) => { - const res = await fetch("http://localhost:4000/graphql", { + const res = await fetch(`http://localhost:${port}/graphql`, { method: "POST", headers: { "Content-Type": "application/json", @@ -129,6 +129,27 @@ describe("eVault E2E", () => { }); it("should reject unauthorized access", async () => { + const token = await w3id.signJWT({ sub: w3id.id }); + const store = await executeGraphQL( + `mutation Store($input: MetaEnvelopeInput!) { + storeMetaEnvelope(input: $input) { + metaEnvelope { id ontology parsed } + envelopes { id ontology value valueType } + } + }`, + { + input: { + ontology: testOntology, + payload: testPayload, + acl: ["@001231232"], + }, + }, + token, + ); + + expect(store.errors).toBeUndefined(); + const unauthEnvId = store.data.storeMetaEnvelope.metaEnvelope.id; + const otherSigner = createMockSigner(); const otherRepo = new MockStorage(); const other = await new W3IDBuilder() @@ -142,7 +163,7 @@ describe("eVault E2E", () => { `query Read($id: String!) { getMetaEnvelopeById(id: $id) { id } }`, - { id: testEnvelopeId }, + { id: unauthEnvId }, otherToken, ); diff --git a/infrastructure/evault-core/tests/utils/mock-storage.ts b/infrastructure/evault-core/tests/utils/mock-storage.ts index c380fb6d..2a7f4b8b 100644 --- a/infrastructure/evault-core/tests/utils/mock-storage.ts +++ b/infrastructure/evault-core/tests/utils/mock-storage.ts @@ -1,60 +1,60 @@ import { StorageSpec } from "../../src/types/w3id"; export class MockStorage implements StorageSpec { - private store: Map = new Map(); - private dataStore: Map = new Map(); - - async get(key: string): Promise { - return this.store.get(key) ?? null; - } - - async set(key: string, value: string): Promise { - this.store.set(key, value); - } - - async delete(key: string): Promise { - this.store.delete(key); - } - - async list(prefix: string): Promise { - return Array.from(this.store.keys()).filter((key) => - key.startsWith(prefix) - ); - } - - async create(data: T): Promise { - const id = Math.random().toString(36).substring(7); - this.dataStore.set(id, data); - return data as unknown as U; - } - - async findOne(query: Partial): Promise { - for (const [_, data] of this.dataStore) { - if (this.matchesQuery(data, query)) { + private store: Map = new Map(); + private dataStore: Map = new Map(); + + async get(key: string): Promise { + return this.store.get(key) ?? null; + } + + async set(key: string, value: string): Promise { + this.store.set(key, value); + } + + async delete(key: string): Promise { + this.store.delete(key); + } + + async list(prefix: string): Promise { + return Array.from(this.store.keys()).filter((key) => + key.startsWith(prefix), + ); + } + + async create(data: T): Promise { + const id = Math.random().toString(36).substring(7); + this.dataStore.set(id, data); return data as unknown as U; - } - } - return null; - } - - async findMany(query: Partial): Promise { - const results: U[] = []; - for (const [_, data] of this.dataStore) { - if (this.matchesQuery(data, query)) { - results.push(data as unknown as U); - } - } - return results; - } - - private matchesQuery(data: T, query: Partial): boolean { - return Object.entries(query).every(([key, value]) => { - return (data as any)[key] === value; - }); - } - - clear(): void { - this.store.clear(); - this.dataStore.clear(); - } + } + + async findOne(query: Partial): Promise { + for (const [_, data] of this.dataStore) { + if (this.matchesQuery(data, query)) { + return data as unknown as U; + } + } + return null; + } + + async findMany(query: Partial): Promise { + const results: U[] = []; + for (const [_, data] of this.dataStore) { + if (this.matchesQuery(data, query)) { + results.push(data as unknown as U); + } + } + return results; + } + + private matchesQuery(data: T, query: Partial): boolean { + return Object.entries(query).every(([key, value]) => { + return (data as any)[key] === value; + }); + } + + clear(): void { + this.store.clear(); + this.dataStore.clear(); + } } diff --git a/package.json b/package.json index dd0fd478..b9f4a9b2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "format": "turbo run format", "check-format": "turbo run check-format", "check": "turbo run check", - "check-types": "turbo run check-types" + "check-types": "turbo run check-types", + "dev:evault": "docker compose -f evault.docker-compose.yml up --watch" }, "devDependencies": { "prettier": "^3.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e51bfba..e5c2f8f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5578,7 +5578,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.27.0 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2