Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# deepevents.ai
deepevents.ai main codebase

## Bounty modules

- [identity-recovery-risk-guard](./identity-recovery-risk-guard/README.md) - account recovery and session-risk controls for user/project access restoration.
27 changes: 27 additions & 0 deletions identity-recovery-risk-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Identity Recovery Risk Guard

This module implements a focused User & Project Management slice for account recovery and session-risk review. It is intentionally not another broad RBAC or profile-management demo. Instead, it answers a specific operational question:

> Should SCIBASE restore account access or linked-identity control when the user has sensitive project memberships, active sessions, and identity evidence that may not line up?

The guard evaluates synthetic password resets, MFA resets, email changes, OAuth relinks, and SAML rebinds before project access is restored.

## What it covers

- Linked identities across email, ORCID, GitHub, Google, and SAML.
- MFA recovery evidence, backup codes, and institutional approval.
- Suspicious sessions, new devices, country changes, and recent failed-login clusters.
- Project exposure for owner/admin roles and sensitive object grants.
- Recovery packets with missing evidence and required reviewers.
- Project access holds, session revocation recommendations, and deterministic audit events.

## Run locally

```bash
npm run check
npm test
npm run demo
npm run demo:gif
```

The implementation is dependency-free and uses synthetic data only.
172 changes: 172 additions & 0 deletions identity-recovery-risk-guard/data/sample-recovery-cases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
{
"generatedAt": "2026-05-16T15:00:00.000Z",
"policy": {
"criticalRiskThreshold": 80,
"highRiskThreshold": 55,
"projectHoldRiskThreshold": 55,
"trustedInstitutionDomains": ["miskatonic.edu", "northbridge-labs.org"],
"recentWindowHours": 72
},
"users": [
{
"id": "user-ada",
"name": "Ada Monroe",
"primaryEmail": "ada@miskatonic.edu",
"institutionDomain": "miskatonic.edu",
"mfa": { "enrolled": true, "backupCodesIssued": true, "lastVerifiedAt": "2026-05-01T12:00:00.000Z" },
"linkedIdentities": [
{ "provider": "email", "subject": "ada@miskatonic.edu", "verified": true, "linkedAt": "2025-02-01T12:00:00.000Z" },
{ "provider": "orcid", "subject": "0000-0002-1111-2222", "verified": true, "linkedAt": "2025-02-02T12:00:00.000Z" },
{ "provider": "github", "subject": "ada-research", "verified": true, "linkedAt": "2025-02-03T12:00:00.000Z" },
{ "provider": "saml", "subject": "ada@miskatonic.edu", "verified": true, "linkedAt": "2025-02-04T12:00:00.000Z" }
]
},
{
"id": "user-lina",
"name": "Lina Kwan",
"primaryEmail": "lina@northbridge-labs.org",
"institutionDomain": "northbridge-labs.org",
"mfa": { "enrolled": true, "backupCodesIssued": true, "lastVerifiedAt": "2026-05-14T09:00:00.000Z" },
"linkedIdentities": [
{ "provider": "email", "subject": "lina@northbridge-labs.org", "verified": true, "linkedAt": "2025-04-01T12:00:00.000Z" },
{ "provider": "saml", "subject": "lina@northbridge-labs.org", "verified": true, "linkedAt": "2025-04-02T12:00:00.000Z" },
{ "provider": "orcid", "subject": "0000-0003-3333-4444", "verified": true, "linkedAt": "2025-04-03T12:00:00.000Z" }
]
},
{
"id": "user-omar",
"name": "Omar Silva",
"primaryEmail": "omar@example.org",
"institutionDomain": "example.org",
"mfa": { "enrolled": false, "backupCodesIssued": false, "lastVerifiedAt": null },
"linkedIdentities": [
{ "provider": "email", "subject": "omar@example.org", "verified": true, "linkedAt": "2026-01-01T12:00:00.000Z" }
]
}
],
"projects": [
{
"id": "project-quasar",
"title": "Quasar Proteomics Consortium",
"visibility": "institutional",
"sensitivity": "restricted",
"memberships": [
{ "userId": "user-ada", "role": "owner" },
{ "userId": "user-lina", "role": "reviewer" }
],
"objectGrants": [
{ "userId": "user-ada", "objectId": "dataset-raw-human", "action": "download", "sensitivity": "restricted" },
{ "userId": "user-lina", "objectId": "notebook-qc", "action": "comment", "sensitivity": "internal" }
]
},
{
"id": "project-atlas",
"title": "Atlas Methods Review",
"visibility": "private",
"sensitivity": "internal",
"memberships": [
{ "userId": "user-lina", "role": "admin" },
{ "userId": "user-omar", "role": "viewer" }
],
"objectGrants": [
{ "userId": "user-lina", "objectId": "review-notes", "action": "edit", "sensitivity": "internal" }
]
}
],
"sessions": [
{
"id": "sess-ada-known",
"userId": "user-ada",
"startedAt": "2026-05-16T08:15:00.000Z",
"lastSeenAt": "2026-05-16T14:30:00.000Z",
"ipCountry": "US",
"deviceFingerprint": "macbook-lab-a",
"trustedDevice": true,
"assuranceLevel": "mfa"
},
{
"id": "sess-ada-new",
"userId": "user-ada",
"startedAt": "2026-05-16T13:45:00.000Z",
"lastSeenAt": "2026-05-16T14:45:00.000Z",
"ipCountry": "RO",
"deviceFingerprint": "unknown-browser-77",
"trustedDevice": false,
"assuranceLevel": "password"
},
{
"id": "sess-lina-known",
"userId": "user-lina",
"startedAt": "2026-05-16T11:20:00.000Z",
"lastSeenAt": "2026-05-16T14:55:00.000Z",
"ipCountry": "US",
"deviceFingerprint": "northbridge-managed-2",
"trustedDevice": true,
"assuranceLevel": "mfa"
},
{
"id": "sess-omar-known",
"userId": "user-omar",
"startedAt": "2026-05-15T17:00:00.000Z",
"lastSeenAt": "2026-05-16T10:00:00.000Z",
"ipCountry": "US",
"deviceFingerprint": "home-laptop",
"trustedDevice": true,
"assuranceLevel": "password"
}
],
"authEvents": [
{ "userId": "user-ada", "type": "failed_login", "at": "2026-05-16T12:52:00.000Z", "ipCountry": "RO", "deviceFingerprint": "unknown-browser-77", "success": false },
{ "userId": "user-ada", "type": "failed_login", "at": "2026-05-16T12:54:00.000Z", "ipCountry": "RO", "deviceFingerprint": "unknown-browser-77", "success": false },
{ "userId": "user-ada", "type": "failed_login", "at": "2026-05-16T12:58:00.000Z", "ipCountry": "RO", "deviceFingerprint": "unknown-browser-77", "success": false },
{ "userId": "user-ada", "type": "provider_unlinked", "at": "2026-05-16T13:18:00.000Z", "ipCountry": "RO", "deviceFingerprint": "unknown-browser-77", "provider": "github", "success": true },
{ "userId": "user-lina", "type": "saml_assertion_changed", "at": "2026-05-16T14:05:00.000Z", "ipCountry": "US", "deviceFingerprint": "northbridge-managed-2", "provider": "saml", "success": true },
{ "userId": "user-omar", "type": "password_reset_requested", "at": "2026-05-16T09:20:00.000Z", "ipCountry": "US", "deviceFingerprint": "home-laptop", "success": true }
],
"recoveryRequests": [
{
"id": "rec-ada-mfa-reset",
"userId": "user-ada",
"type": "mfa_reset",
"requestedAt": "2026-05-16T14:50:00.000Z",
"source": { "ipCountry": "RO", "deviceFingerprint": "unknown-browser-77" },
"evidence": {
"emailVerified": false,
"institutionalAdminApproved": false,
"mfaBackupCode": false,
"orcidReverified": false,
"deviceTrusted": false
}
},
{
"id": "rec-lina-saml-rebind",
"userId": "user-lina",
"type": "saml_rebind",
"requestedAt": "2026-05-16T14:35:00.000Z",
"targetProvider": "saml",
"targetSubject": "lina@contractor-mail.example",
"source": { "ipCountry": "US", "deviceFingerprint": "northbridge-managed-2" },
"evidence": {
"emailVerified": true,
"institutionalAdminApproved": false,
"mfaBackupCode": true,
"orcidReverified": true,
"deviceTrusted": true
}
},
{
"id": "rec-omar-password",
"userId": "user-omar",
"type": "password_reset",
"requestedAt": "2026-05-16T10:05:00.000Z",
"source": { "ipCountry": "US", "deviceFingerprint": "home-laptop" },
"evidence": {
"emailVerified": true,
"institutionalAdminApproved": false,
"mfaBackupCode": false,
"orcidReverified": false,
"deviceTrusted": true
}
}
]
}
Binary file added identity-recovery-risk-guard/docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added identity-recovery-risk-guard/docs/demo.mp4
Binary file not shown.
26 changes: 26 additions & 0 deletions identity-recovery-risk-guard/docs/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions identity-recovery-risk-guard/docs/requirement-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Requirement Map

This module maps issue #11 to a focused recovery-risk control layer for User & Project Management.

| Issue #11 requirement | Implementation |
| --- | --- |
| Email/password login with 2FA | `mfa_reset` and `password_reset` requests evaluate MFA backup-code, verified email, and device evidence before access is restored. |
| OAuth integrations and account linking | Linked identity records model email, ORCID, GitHub, Google, and SAML providers; relinks are checked for subject changes. |
| Institutional login via SAML | `saml_rebind` requests require trusted institutional domains and institution-admin approval. |
| Account linking for unified identity | `linked_identity_subject_changed` findings detect identity subject drift before project access is restored. |
| Public/private profile and attribution safety | Risky recovery decisions hold profile attribution changes until identity is reviewed. |
| Project spaces and linked collaborators | Project memberships and object grants are scanned for owner/admin exposure and sensitive project access. |
| Role-based access | Owner/admin roles trigger project access holds during high-risk recovery. |
| Object-level control | Restricted datasets and download grants are held separately from general project membership. |
| Project audit log | `auditEvents` records deterministic hashes for risk scoring, decisions, review packets, project holds, and session actions. |

## Distinctness

Existing issue #11 submissions cover broad RBAC, workspace governance, member offboarding, institutional recertification, anonymous-review escrow, and identity merge/export. This slice focuses on account recovery and active-session risk before access restoration. It is designed to sit in front of those systems and prevent a compromised recovery flow from inheriting sensitive project access.
12 changes: 12 additions & 0 deletions identity-recovery-risk-guard/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "identity-recovery-risk-guard",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"check": "node --check src/identity-recovery-risk-guard.js && node --check scripts/demo.js && node --check scripts/write-demo-gif.js && node --check test/identity-recovery-risk-guard.test.js",
"test": "node --test test/identity-recovery-risk-guard.test.js",
"demo": "node scripts/demo.js",
"demo:gif": "node scripts/write-demo-gif.js"
}
}
34 changes: 34 additions & 0 deletions identity-recovery-risk-guard/scripts/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { evaluateIdentityRecoveryRisk } from "../src/identity-recovery-risk-guard.js";

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const sample = JSON.parse(readFileSync(join(root, "data", "sample-recovery-cases.json"), "utf8"));
const report = evaluateIdentityRecoveryRisk(sample);

console.log(JSON.stringify({
digest: report.evidenceDigest,
summary: report.summary,
blockedRecoveries: report.recoveryCases
.filter((item) => item.severity === "critical" || item.severity === "high")
.map((item) => ({
requestId: item.requestId,
user: item.userName,
type: item.type,
severity: item.severity,
riskScore: item.riskScore,
factors: item.factors
})),
decisions: report.decisions.map((decision) => ({
requestId: decision.requestId,
recovery: decision.recovery,
projectAccess: decision.projectAccess,
sessions: decision.sessions
})),
requiredReviewPackets: report.recoveryPackets.map((packet) => ({
requestId: packet.requestId,
reviewers: packet.requiredReviewers,
missingEvidence: packet.missingEvidence
}))
}, null, 2));
Loading