Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ jobs:
run: node scripts/check-ignore-hygiene.mjs
- name: Secret scan staged/worktree
run: scripts/scan-secrets.sh --worktree
- name: Secret scan regression
run: tests/security/scan-secrets.test.sh
- name: Route Ops classifier tests
if: needs.route-ops-changes.outputs.workflow_changed == 'true' || needs.route-ops-changes.outputs.full_required == 'true'
run: node tests/ci/route-ops-change-classifier.test.mjs
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ backups/
.gitleaks-report.*
.trufflehog-report.*
.secret-scan/
google-services.json
service-account*.json
firebase-adminsdk*.json
credentials.json
*.credentials.json

# Evidence/private artifacts
evidence/
Expand All @@ -78,6 +83,7 @@ docs/proof-media-production-evidence-manifest-*.md

# Local orchestration/editor noise
.omx/
.worktrees/
worktrees/
.DS_Store
Thumbs.db
Expand Down
6 changes: 2 additions & 4 deletions apps/delivery-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ POSTGRES_USER=clever
POSTGRES_PASSWORD=clever

# Driver API auth secret; required for driver login/session and route access APIs.
JWT_SECRET=change-me
# Use at least 32 random characters and keep it in runtime secret storage.
JWT_SECRET=
# Register the DSV Driver app login/signup routes only when explicitly enabled.
CLEVER_DSV_DRIVER_AUTH_ENABLED=false
# Keyed fingerprint secret for the DSV Driver app's resident-number-front matching.
# Use at least 32 random characters and keep it in runtime secret storage.
DSV_DRIVER_IDENTITY_SECRET=

# Generic server-side master key for encrypting customer connector credentials in DB.
# Generate with: node -e "console.log('base64:' + require('crypto').randomBytes(32).toString('base64'))"
Expand Down
2 changes: 1 addition & 1 deletion apps/delivery-api/docs/api/dsv-driver-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ DSV 전용 Driver 앱은 기존 CLEVER Routes 앱의 Shopify 초대 및 전화
## 운영 활성화

인증 라우트는 `CLEVER_DSV_DRIVER_AUTH_ENABLED=true`일 때만 등록된다. 이 값이 켜진
환경에서는 `JWT_SECRET`이 누락되면 서버가 기동을 거부한다.
환경에서는 `JWT_SECRET`이 누락되거나 32자보다 짧으면 서버가 기동을 거부한다.

과거 `DsvDriverAccountSignupInvite` 테이블과 migration은 감사 이력 보존을 위해 유지하지만,
현행 서버는 토큰을 발급·검증·소비하지 않는다.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PrismaDriverAuthRepository } from './driver-auth.repository.js';
import { PrismaDriverTokenAccessRepository } from './driver-token-access.repository.js';
import { PrismaDriverPushTokenService } from '../route-grouping/driver-push-token.service.js';
import type { DriverAuthDependencies } from '../../routes/driver-auth.routes.js';
import { readDriverJwtSecret } from './driver-token-verifier.js';

type LoadDriverAuthDependenciesInput = {
env: Partial<Record<'JWT_SECRET', string>>;
Expand All @@ -12,10 +13,8 @@ type LoadDriverAuthDependenciesInput = {
export function loadDriverAuthDependencies(
input: LoadDriverAuthDependenciesInput
): DriverAuthDependencies | undefined {
const jwtSecret = input.env.JWT_SECRET?.trim();
if (!jwtSecret) {
return undefined;
}
const jwtSecret = readDriverJwtSecret(input.env.JWT_SECRET);
if (jwtSecret === undefined) return undefined;

return {
driverAuthRepository: new PrismaDriverAuthRepository(input.prisma),
Expand Down
10 changes: 10 additions & 0 deletions apps/delivery-api/src/modules/driver/driver-token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { normalizeDriverCommerceDomain } from './driver-commerce-domain.js';
const DRIVER_AUDIENCE = 'clever-delivery-driver';
const DRIVER_ACCOUNT_AUDIENCE = 'clever-driver-account';
const DRIVER_ROUTE_AUDIENCE = 'clever-delivery-driver-route';
const MIN_DRIVER_JWT_SECRET_CHARACTERS = 32;

export function readDriverJwtSecret(value: string | undefined): string | undefined {
const secret = value?.trim();
if (!secret) return undefined;
if (secret.length < MIN_DRIVER_JWT_SECRET_CHARACTERS) {
throw new Error('JWT_SECRET must contain at least 32 characters');
}
return secret;
}

export type VerifiedDriverAccountToken = {
accountId: string;
Expand Down
3 changes: 2 additions & 1 deletion apps/delivery-api/src/modules/driver/driver.dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PrismaDriverRouteAccessRepository } from './driver-route-access.reposit
import { PrismaDriverRouteSessionRepository } from './driver-route-session.repository.js';
import { PrismaDriverSelfServiceRepository } from './driver-self-service.repository.js';
import { PrismaDriverTokenAccessRepository } from './driver-token-access.repository.js';
import { readDriverJwtSecret } from './driver-token-verifier.js';
import { createS3DriverProofMediaStorage } from './driver-proof-media-s3-storage.js';
import {
createHttpDriverProofMediaScanMonitor,
Expand Down Expand Up @@ -123,7 +124,7 @@ export function loadDriverApiDependencies(
input: LoadDriverApiDependenciesInput
): DriverApiDependencies | undefined {
const completionInvariantMode = loadDriverRouteCompletionInvariantMode(input.env);
const jwtSecret = readOptional(input.env.JWT_SECRET);
const jwtSecret = readDriverJwtSecret(input.env.JWT_SECRET);
if (jwtSecret === undefined) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PrismaClient } from '@prisma/client';

import type { DsvDriverAuthDependencies } from '../../routes/dsv-driver-auth.routes.js';
import { readDriverJwtSecret } from '../driver/driver-token-verifier.js';
import { PrismaDsvDriverAuthRepository } from './dsv-driver-auth.repository.js';

export type DsvDriverAuthRuntimeEnv = Partial<Record<
Expand All @@ -16,8 +17,8 @@ export function loadDsvDriverAuthDependencies(input: {
const enabled = readBoolean(input.env.CLEVER_DSV_DRIVER_AUTH_ENABLED);
if (enabled !== true) return undefined;

const jwtSecret = input.env.JWT_SECRET?.trim();
if (jwtSecret === undefined || jwtSecret === '') {
const jwtSecret = readDriverJwtSecret(input.env.JWT_SECRET);
if (jwtSecret === undefined) {
throw new Error('CLEVER_DSV_DRIVER_AUTH_ENABLED=true requires JWT_SECRET');
}
return {
Expand Down
99 changes: 0 additions & 99 deletions apps/delivery-api/src/modules/dsv/dsv-driver-test-identity.seed.ts

This file was deleted.

32 changes: 0 additions & 32 deletions apps/delivery-api/src/scripts/seed-dsv-driver-test-identity.ts

This file was deleted.

14 changes: 14 additions & 0 deletions apps/delivery-api/tests/driver-token-verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createHmac } from 'node:crypto';
import { describe, expect, test } from 'vitest';

import {
readDriverJwtSecret,
signDriverRouteToken,
signDriverToken,
verifyDriverRouteToken,
Expand All @@ -11,6 +12,19 @@ import {
const secret = 'driver-secret';
const now = new Date('2026-05-07T06:10:00Z');

describe('readDriverJwtSecret', () => {
test('keeps the API disabled when absent and rejects weak configured secrets', () => {
expect(readDriverJwtSecret(undefined)).toBeUndefined();
expect(readDriverJwtSecret(' ')).toBeUndefined();
expect(() => readDriverJwtSecret('short-secret')).toThrow(
'JWT_SECRET must contain at least 32 characters'
);
expect(readDriverJwtSecret(' test-driver-jwt-secret-32-characters ')).toBe(
'test-driver-jwt-secret-32-characters'
);
});
});

describe('verifyDriverToken', () => {
test('signs route access with only the global account and assigned route scope', () => {
const result = signDriverRouteToken(
Expand Down
Loading