Skip to content

Commit 87b4b39

Browse files
authored
Merge branch 'main' into feature/user-data-export
2 parents b94f022 + 4b6ac0d commit 87b4b39

27 files changed

Lines changed: 1332 additions & 4 deletions

.github/workflows/ci.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ jobs:
3939
- name: Record wasm SHA-256
4040
run: sha256sum target/wasm32-unknown-unknown/release/niffyinsure.wasm | tee niffyinsure.wasm.sha256
4141

42+
- name: Simulate wasm drift (staging acceptance test)
43+
run: |
44+
ACTUAL=$(cat niffyinsure.wasm.sha256 | awk '{print $1}')
45+
EXPECTED=$(jq -r '.contracts[0].expectedWasmHash' contracts/deployment-registry.json)
46+
# In CI the registry holds a placeholder; drift is detected when they differ.
47+
# In staging, set NIFFYINSURE_EXPECTED_WASM_HASH to a known-wrong value to
48+
# verify the alert path fires. Exit 0 here — alerting is runtime, not build-time.
49+
if [ "$EXPECTED" = "\${NIFFYINSURE_EXPECTED_WASM_HASH}" ]; then
50+
echo "Registry uses env placeholder — skipping drift comparison in CI"
51+
elif [ "$ACTUAL" != "$EXPECTED" ]; then
52+
echo "::warning::Wasm drift detected: expected=$EXPECTED actual=$ACTUAL"
53+
else
54+
echo "Wasm hash matches registry: $ACTUAL"
55+
fi
56+
4257
- uses: actions/upload-artifact@v4
4358
with:
4459
name: niffyinsure-wasm-${{ github.sha }}
@@ -84,6 +99,57 @@ jobs:
8499
- run: npm run build
85100
- run: npm test
86101

102+
# ── Dependency audit / supply-chain ─────────────────────────────────────
103+
# Policy: CRITICAL CVEs fail the build. HIGH CVEs produce a warning and
104+
# must be triaged within 7 days. Accepted risks require a signed-off entry
105+
# in docs/ops/audit-exceptions.md before the override label is applied.
106+
# Override process:
107+
# 1. Engineer opens a PR adding the CVE to audit-exceptions.md with
108+
# justification, mitigations, and a review-by date.
109+
# 2. A second engineer approves the PR.
110+
# 3. Add the GitHub label `audit-exception-approved` to the failing PR.
111+
# 4. Re-run this job — it will pass once the exception is documented.
112+
dependency-audit:
113+
name: Dependency Audit (npm / SBOM)
114+
runs-on: ubuntu-latest
115+
steps:
116+
- uses: actions/checkout@v4
117+
118+
- uses: actions/setup-node@v4
119+
with:
120+
node-version: 22
121+
122+
- name: Audit backend dependencies
123+
working-directory: backend
124+
run: |
125+
npm install --ignore-scripts
126+
# Fail on critical; warn on high (exit 0 so we can capture output)
127+
npm audit --audit-level=critical
128+
npm audit --audit-level=high || echo "::warning::High-severity advisories found — triage within 7 days per audit policy"
129+
130+
- name: Audit frontend dependencies
131+
working-directory: frontend
132+
run: |
133+
npm ci --ignore-scripts
134+
npm audit --audit-level=critical
135+
npm audit --audit-level=high || echo "::warning::High-severity advisories found — triage within 7 days per audit policy"
136+
137+
- name: Generate SBOM (backend)
138+
working-directory: backend
139+
run: npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file ../sbom-backend.json
140+
141+
- name: Generate SBOM (frontend)
142+
working-directory: frontend
143+
run: npx --yes @cyclonedx/cyclonedx-npm --output-format JSON --output-file ../sbom-frontend.json
144+
145+
- uses: actions/upload-artifact@v4
146+
with:
147+
name: sbom-${{ github.sha }}
148+
path: |
149+
sbom-backend.json
150+
sbom-frontend.json
151+
retention-days: 90
152+
87153
# ── Frontend ──────────────────────────────────────────────────────────────
88154
frontend:
89155
name: Frontend (Next.js / TypeScript)

backend/docs/feature-flags.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Backend Feature Flags Runbook
2+
3+
## Purpose
4+
Feature flags gate experimental backend routes at the API edge so unfinished modules are fully unreachable when disabled.
5+
6+
Safe default: any missing flag is treated as `false`.
7+
8+
## Configuration
9+
Set `FEATURE_FLAGS_JSON` as a JSON object at process start:
10+
11+
```bash
12+
FEATURE_FLAGS_JSON='{"experimental.oracleHooks":false,"experimental.betaCalculators":false}'
13+
```
14+
15+
Optional response strategy for disabled routes:
16+
17+
- `FEATURE_FLAGS_DISABLED_STATUS=404` (default, hides route existence)
18+
- `FEATURE_FLAGS_DISABLED_STATUS=403` (explicitly forbidden)
19+
20+
## Current Flags
21+
| Flag name | Default | Owner | Meaning |
22+
|---|---|---|---|
23+
| `experimental.oracleHooks` | `false` | Backend Platform Team | Enables `/experimental/oracle-hooks/*` ingestion hooks for oracle event experiments. |
24+
| `experimental.betaCalculators` | `false` | Underwriting Engine Team | Enables `/experimental/beta-calculators/*` premium preview APIs under validation. |
25+
26+
## Lifecycle
27+
1. Creation: add a single-purpose flag and owner in this document before merging code.
28+
2. Enablement: turn on only in non-production first, monitor error rate and access logs.
29+
3. Promotion: after stable metrics and review, enable for production rollout with change ticket.
30+
4. Removal: once fully adopted, delete guard/decorator usage and remove the flag from env + docs.
31+
32+
## Operational Guardrails
33+
- Feature flags do not replace authentication or authorization controls.
34+
- Keep total flag count low to avoid long-lived configuration sprawl.
35+
- Disabled-access attempts are logged (`FeatureFlagsGuard`) for metrics pipelines.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@nestjs/platform-express": "^10.4.5",
4040
"@nestjs/swagger": "^7.4.2",
4141
"@nestjs/terminus": "^10.2.1",
42+
"@nestjs/schedule": "^4.1.0",
4243
"@nestjs/throttler": "^6.5.0",
4344
"@prisma/client": "^6.6.0",
4445
"@stellar/stellar-sdk": "^14.6.1",

backend/prisma/schema.prisma

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,41 @@ model FeatureFlag {
156156
@@map("feature_flags")
157157
}
158158

159+
/// Deduplication table for wasm drift alerts — one row per (contract, actual-hash) pair.
160+
/// Prevents repeated webhook spam for the same unresolved drift.
161+
model WasmDriftAlert {
162+
id String @id @default(uuid())
163+
dedupKey String @unique /// "<contractName>:<actualHash>"
164+
contractName String
165+
contractId String
166+
expectedHash String
167+
actualHash String
168+
resolvedAt DateTime?
169+
createdAt DateTime @default(now())
170+
171+
@@index([contractName])
172+
@@map("wasm_drift_alerts")
173+
}
174+
175+
/// Tracks staff-initiated privacy requests (GDPR/CCPA-style) for off-chain data.
176+
/// On-chain and IPFS data is immutable and cannot be erased — see runbook.
177+
model PrivacyRequest {
178+
id String @id @default(uuid())
179+
subjectWalletAddress String
180+
requestType String /// "ANONYMIZE" | "DELETE"
181+
requestedBy String /// staff actor (wallet address or email)
182+
status String @default("IN_PROGRESS") /// "IN_PROGRESS" | "COMPLETED" | "FAILED"
183+
rowsAffected Int @default(0)
184+
notes String?
185+
errorMessage String?
186+
completedAt DateTime?
187+
createdAt DateTime @default(now())
188+
189+
@@index([subjectWalletAddress])
190+
@@index([status])
191+
@@map("privacy_requests")
192+
}
193+
159194
/// Off-chain metadata for allowlisted SEP-41 asset contracts.
160195
/// Populated by the indexer when it observes AssetAdded events.
161196
/// Used by the frontend and API to display correct symbol/decimals.

backend/src/admin/admin.controller.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
HttpStatus,
1313
} from '@nestjs/common';
1414
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
15+
import { IsEnum, IsOptional, IsString } from 'class-validator';
1516
import { Request } from 'express';
1617
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
1718
import { AdminRoleGuard } from './guards/admin-role.guard';
@@ -20,6 +21,13 @@ import { AuditService } from './audit.service';
2021
import { ReindexDto } from './dto/reindex.dto';
2122
import { AuditQueryDto } from './dto/audit-query.dto';
2223
import { FeatureFlagDto } from './dto/feature-flag.dto';
24+
import { PrivacyService, PrivacyRequestType } from '../maintenance/privacy.service';
25+
26+
class PrivacyRequestDto {
27+
@IsString() subjectWalletAddress!: string;
28+
@IsEnum(['ANONYMIZE', 'DELETE']) requestType!: PrivacyRequestType;
29+
@IsOptional() @IsString() notes?: string;
30+
}
2331

2432
type AdminRequest = Request & {
2533
user?: {
@@ -35,6 +43,7 @@ export class AdminController {
3543
constructor(
3644
private readonly adminService: AdminService,
3745
private readonly auditService: AuditService,
46+
private readonly privacyService: PrivacyService,
3847
) {}
3948

4049
/**
@@ -96,6 +105,28 @@ export class AdminController {
96105
* officer and are subject to applicable insurance-regulation obligations.
97106
* The audit row created here serves as the immutable record of that action.
98107
*/
108+
/** POST /admin/privacy/requests — execute anonymization or deletion for a subject. */
109+
@Post('privacy/requests')
110+
@HttpCode(HttpStatus.ACCEPTED)
111+
@ApiOperation({ summary: 'Submit a privacy request (anonymize or delete off-chain data)' })
112+
async submitPrivacyRequest(@Body() dto: PrivacyRequestDto, @Req() req: Request) {
113+
const actor = (req.user as any)?.walletAddress ?? 'unknown';
114+
return this.privacyService.handleRequest({
115+
subjectWalletAddress: dto.subjectWalletAddress,
116+
requestType: dto.requestType,
117+
requestedBy: actor,
118+
ipAddress: req.ip,
119+
notes: dto.notes,
120+
});
121+
}
122+
123+
/** GET /admin/privacy/requests — list all privacy requests. */
124+
@Get('privacy/requests')
125+
@ApiOperation({ summary: 'List privacy requests' })
126+
async listPrivacyRequests(@Query('page') page = 1, @Query('limit') limit = 20) {
127+
return this.privacyService.listRequests(Number(page), Number(limit));
128+
}
129+
99130
@Patch('feature-flags/:key')
100131
@ApiOperation({ summary: 'Set a feature flag value' })
101132
async setFeatureFlag(

backend/src/admin/admin.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { AdminService } from './admin.service';
44
import { AuditService } from './audit.service';
55
import { PrismaModule } from '../prisma/prisma.module';
66
import { AuthModule } from '../auth/auth.module';
7+
import { MaintenanceModule } from '../maintenance/maintenance.module';
78

89
@Module({
9-
imports: [PrismaModule, AuthModule],
10+
imports: [PrismaModule, AuthModule, MaintenanceModule],
1011
controllers: [AdminController],
1112
providers: [AdminService, AuditService],
1213
exports: [AuditService],

backend/src/app.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { QuoteModule } from './quote/quote.module';
1616
import { PolicyModule } from './policy/policy.module';
1717
import { NotificationsModule } from './notifications/notifications.module';
1818
import { TxModule } from './tx/tx.module';
19+
import { FeatureFlagsModule } from './feature-flags/feature-flags.module';
20+
import { OracleHooksController } from './experimental/oracle-hooks.controller';
21+
import { BetaCalculatorsController } from './experimental/beta-calculators.controller';
1922

2023
@Module({
2124
imports: [
@@ -42,7 +45,8 @@ import { TxModule } from './tx/tx.module';
4245
PolicyModule,
4346
NotificationsModule,
4447
TxModule,
48+
FeatureFlagsModule,
4549
],
50+
controllers: [OracleHooksController, BetaCalculatorsController],
4651
})
4752
export class AppModule {}
48-
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { IsNumber, Min } from 'class-validator';
2+
3+
export class BetaCalculatorDto {
4+
@IsNumber()
5+
@Min(0)
6+
basePremium!: number;
7+
8+
@IsNumber()
9+
@Min(0)
10+
riskMultiplier!: number;
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Body, Controller, Post } from '@nestjs/common';
2+
import { BetaCalculatorDto } from '../dto/beta-calculator.dto';
3+
import { Feature } from '../feature-flags/feature.decorator';
4+
5+
@Controller('experimental/beta-calculators')
6+
@Feature('experimental.betaCalculators')
7+
export class BetaCalculatorsController {
8+
@Post('premium-preview')
9+
premiumPreview(@Body() body: BetaCalculatorDto) {
10+
return {
11+
premium: Number((body.basePremium * body.riskMultiplier).toFixed(2)),
12+
};
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Body, Controller, Post } from '@nestjs/common';
2+
import { Feature } from '../feature-flags/feature.decorator';
3+
4+
@Controller('experimental/oracle-hooks')
5+
@Feature('experimental.oracleHooks')
6+
export class OracleHooksController {
7+
@Post('ingest')
8+
ingest(@Body() body: Record<string, unknown>) {
9+
return {
10+
accepted: true,
11+
receivedKeys: Object.keys(body || {}),
12+
};
13+
}
14+
}

0 commit comments

Comments
 (0)