Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dc9f358
Merge pull request #100 from YOLOVibeCode/develop
rvegajr Jul 23, 2026
1c5314e
Merge pull request #105 from YOLOVibeCode/develop
rvegajr Aug 4, 2026
f86c920
Merge pull request #108 from YOLOVibeCode/develop
rvegajr Aug 4, 2026
e54d275
Merge pull request #112 from YOLOVibeCode/release
rvegajr Aug 5, 2026
3dbfa79
Merge pull request #211 from YOLOVibeCode/release
rvegajr Aug 24, 2026
037d43c
Merge pull request #214 from YOLOVibeCode/release
rvegajr Aug 24, 2026
8fc4df8
fix(direct): play event streams without double error overlays
rvegajr Sep 5, 2026
9bfe815
chore: keep hierarchical bootstrap tests off main until path rewrite …
rvegajr Sep 5, 2026
07c356e
chore: bump version to 1.3.0.004
rvegajr Sep 5, 2026
c343c2b
fix(ci): pin pnpm 8.15.9 so Railway Railpack can resolve the package …
rvegajr Sep 2, 2026
9f307e7
fix(direct): dismiss loading overlay when the video element can play
rvegajr Sep 5, 2026
094cb19
Show the official Apple Pay button on the paywall.
rvegajr Sep 17, 2026
8516b4e
Merge pull request #226 from YOLOVibeCode/promote/official-apple-pay-…
rvegajr Sep 17, 2026
4e47b32
Merge release into main for production promote.
rvegajr Sep 17, 2026
b575fac
Merge pull request #229 from YOLOVibeCode/promote/uat-to-prod
rvegajr Sep 17, 2026
55ba7e1
security: remove hardcoded production database credentials
rvegajr Sep 21, 2026
692f699
Tell agents mail, text, and AI go through Noctusoft platforms.
rvegajr Sep 23, 2026
ceee8a2
Point LLM Relay docs at ai.noctusoft.com/v1.
rvegajr Sep 23, 2026
084ec22
Merge pull request #232 from YOLOVibeCode/chore/ai-noctusoft-url
rvegajr Sep 23, 2026
0bb75ce
Merge pull request #240 from YOLOVibeCode/promote/uat-to-prod-0927
rvegajr Sep 28, 2026
c06f52d
Merge pull request #242 from YOLOVibeCode/develop
rvegajr Oct 4, 2026
b406208
Merge pull request #245 from YOLOVibeCode/develop
rvegajr Oct 4, 2026
aab0dad
Merge pull request #250 from YOLOVibeCode/develop
rvegajr Oct 11, 2026
4ef336c
security: remove the last committed credentials
rvegajr Oct 12, 2026
e49284f
Merge pull request #251 from YOLOVibeCode/security/remove-remaining-c…
rvegajr Oct 13, 2026
a48132d
feat(direct): hold the video 2s to reveal skip controls (#252)
rvegajr Oct 15, 2026
e71fe1c
fix(refunds): update purchases only after the store refunds, reverse …
rvegajr Oct 15, 2026
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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ the API boots on `PORT` and answers `GET /health`, and the Next.js web app
builds. Production runs on Railway (services `api` and `web`); deploys are
**human-only**.

## Noctusoft platforms

This product does not sign up for OpenAI, Twilio, or SendGrid. It calls two Noctusoft platforms. The product id is the only difference.

- **LLM Relay** — OpenAI-compatible API at `https://ai.noctusoft.com/v1` on litellm-vm (Azure `20.46.250.159`, Tailscale `100.112.233.46`). Virtual key. No provider SDK. Not noctusoft-relay on `ns`.
- **Mail** — `POST /email/send` or SendGrid drop-in `POST /v3/mail/send` on `api.sendgrid.noctusoft.com`, with the product key and `X-App-Env` (`dev` captures in smtp4dev, `uat` tags and sends, anything else is real delivery).
- **Text** — `POST /sms/send` or Twilio drop-in on `api.twilio.noctusoft.com`.
- **One store** — the product's billing alias on noctusoft-relay. Square item codes are `NOCTU-{PRODUCT}-…`. The Square category is `Noctusoft — {brand}`.
- **Marketplace** — Connect Hub, the FieldView shape: `POST /connect/{productKey}/recipients/{seller}/charge`. The seller is the merchant. The fee lives on that product's `connect_apps` row.

## Layout

pnpm workspace (`pnpm-workspace.yaml`: `packages/*`, `apps/*`).
Expand Down
6 changes: 6 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Noctusoft store marketplace (see noctusoft-relay docs/api/store/README.md)
NOCTUSOFT_API_KEY=nsins_dk_your_deploy_key_here

# SMS relay inbound webhook HMAC (relay signs POST bodies to /api/webhooks/twilio*)
RELAY_INBOUND_SECRET=whsec_your_relay_inbound_secret_here

# Optional override (default https://api.twilio.noctusoft.com)
# NOCTUSOFT_SMS_RELAY_BASE_URL=https://api.twilio.noctusoft.com
NOCTUSOFT_STORE_BASE_URL=https://api.store.noctusoft.com
NOCTUSOFT_PRODUCT_KEY=fieldview

Expand Down
7 changes: 7 additions & 0 deletions apps/api/__tests__/integration/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,12 @@ describe('Health Endpoint', () => {
expect(response.body.checks).toBeDefined();
expect(response.body.checks.database).toBeDefined();
expect(response.body.checks.redis).toBeDefined();

expect(typeof response.body.ok).toBe('boolean');
expect(response.body.service).toBe('fieldview-api');
expect(typeof response.body.commit).toBe('string');
expect(typeof response.body.env).toBe('string');
expect(typeof response.body.utc).toBe('string');
expect(response.body.ok).toBe(response.body.status === 'healthy');
});
});
4 changes: 2 additions & 2 deletions apps/api/__tests__/integration/public.checkout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ describe('Public Checkout Routes', () => {
.post('/api/public/games/game-1/checkout')
.send({
viewerEmail: 'test@example.com',
viewerPhone: '+1234567890',
viewerPhone: '+12025550100',
})
.expect(200);

expect(mockPaymentService.createCheckout).toHaveBeenCalledWith(
'game-1',
'test@example.com',
'+1234567890',
'+12025550100',
undefined,
undefined,
expect.any(CouponService)
Expand Down
25 changes: 21 additions & 4 deletions apps/api/__tests__/integration/public.subscriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('@/lib/prisma', () => ({
prisma: {
viewerIdentity: {
findUnique: vi.fn(),
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn(),
update: vi.fn(),
},
Expand All @@ -25,6 +26,12 @@ vi.mock('@/lib/prisma', () => ({
updateMany: vi.fn(),
findUnique: vi.fn(),
},
smsConsent: {
upsert: vi.fn().mockResolvedValue({ id: 'consent-1' }),
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
},
},
}));

Expand All @@ -37,9 +44,17 @@ vi.mock('@/services/NotificationService', () => ({
})),
}));

// Mock ViewerIdentityRepository
// Mock ViewerIdentityRepository (used by SMS consent recording)
vi.mock('@/repositories/implementations/ViewerIdentityRepository', () => ({
ViewerIdentityRepository: vi.fn(),
ViewerIdentityRepository: vi.fn().mockImplementation(() => ({
getByPhone: vi.fn().mockResolvedValue(null),
getById: vi.fn(),
getByEmail: vi.fn(),
getByEmailVerified: vi.fn(),
create: vi.fn(),
update: vi.fn(),
markEmailVerified: vi.fn(),
})),
}));

describe('Public Subscriptions Routes', () => {
Expand Down Expand Up @@ -83,7 +98,8 @@ describe('Public Subscriptions Routes', () => {
.post('/api/public/subscriptions')
.send({
email: 'viewer@example.com',
phoneE164: '+1234567890',
phoneE164: '+12025550100',
smsOptIn: true,
organizationId: '00000000-0000-0000-0000-000000000001', // Valid UUID format
preference: 'both',
})
Expand Down Expand Up @@ -119,7 +135,8 @@ describe('Public Subscriptions Routes', () => {
.post('/api/public/subscriptions')
.send({
email: 'viewer@example.com',
phoneE164: '1234567890', // Missing +
phoneE164: 'not-a-phone',
smsOptIn: true,
organizationId: 'org-1',
preference: 'sms',
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import crypto from 'crypto';

import request from 'supertest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { SMS_INBOUND_WEBHOOK_URL } from '@fieldview/data-model';

import app from '@/server';
import * as twilioWebhookRoute from '@/routes/webhooks.twilio';
import { SmsService } from '@/services/SmsService';

function signBody(secret: string, url: string, body: string): string {
return crypto.createHmac('sha256', secret).update(url + body).digest('base64');
}

vi.mock('@/lib/prisma', () => ({
prisma: {
sMSMessage: { create: vi.fn(), updateMany: vi.fn() },
smsConsent: { findUnique: vi.fn(), upsert: vi.fn(), create: vi.fn(), update: vi.fn() },
viewerIdentity: {
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn(),
update: vi.fn(),
},
game: { findFirst: vi.fn().mockResolvedValue(null) },
},
}));

describe('Twilio webhook middleware order (full app)', () => {
const secret = 'relay-test-secret';

beforeEach(() => {
vi.stubEnv('RELAY_INBOUND_SECRET', secret);
twilioWebhookRoute.setSmsService({
handleStop: vi.fn(),
handleStart: vi.fn(),
handleHelp: vi.fn(),
handleYes: vi.fn(),
findByKeyword: vi.fn(),
sendPaymentLink: vi.fn(),
logSmsMessage: vi.fn(),
recordKeywordConsent: vi.fn(),
getHelpTwimlBody: vi.fn(),
} as unknown as SmsService);
});

afterEach(() => {
vi.unstubAllEnvs();
});

it('rejects re-encoded inbound body before route handler runs', async () => {
const signedBody = 'From=%2B15551234567&Body=STOP';
const sig = signBody(secret, SMS_INBOUND_WEBHOOK_URL, signedBody);
const reencodedBody = 'From=+15551234567&Body=STOP';

const res = await request(app)
.post('/api/webhooks/twilio')
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('x-relay-signature', sig)
.send(reencodedBody);

expect(res.status).toBe(401);
});
});
Loading
Loading