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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,42 @@ personal app activity, microphone, camera, or screenshots.
4. **Tamper-evident audit log** — every observation creates a hash-chained record
5. **Open source** — you are reading the enforcement code right now

## Quick Start

```typescript
import { createObserver } from '@wrkmark/observer'

// Wire everything up in one call
const { sessionManager, collector } = createObserver('./wrkmark.db')

// Start observing a VS Code session
sessionManager.startSession('VS Code')
collector.start(sessionManager)

// Your IPC bridge calls these as VS Code fires events:
// collector.onKeystroke(Date.now())
// collector.onUndo(Date.now())
// collector.onFileSwitch(Date.now())

// When the session ends
const completed = sessionManager.endSession()
console.log(`Session: ${completed.duration_ms / 60000} minutes`)
```

## Individual Imports

```typescript
import {
createDatabase,
AuditLog,
SignalAnonymizer,
SignalExtractor,
SessionManager,
VSCodeCollector,
WrkmarkObserverError
} from '@wrkmark/observer'
```

## Run the tests yourself

```bash
Expand Down
82 changes: 82 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* index.ts
*
* The public API of wrkmark-observer. If you're building
* something on top of this engine — this is your entry point.
*
* We export only what you need to integrate the observer.
* Internal implementation details stay internal — the less
* surface area we expose, the harder it is to misuse.
*/

// ─── Core Types Re-exports ──────────────────────────────────────────────────
export type {
SignalType,
RawSignal,
AnonymizedSignal,
ActiveSession,
CompletedSession,
FeatureVector,
ObserverStatus,
ObserverState,
ICollector,
ObserverErrorCode,
} from './types/index.js';

export { WrkmarkObserverError } from './types/index.js';

// ─── Module Re-exports ──────────────────────────────────────────────────────
export { AuditLog } from './privacy/audit-log.js';
export { SignalAnonymizer } from './processor/anonymizer.js';
export { createDatabase } from './db/database.js';
export type { WrkmarkDb } from './db/database.js';
export { SignalExtractor } from './processor/signal-extractor.js';
export { SessionManager } from './session-manager.js';
export { VSCodeCollector } from './collectors/vscode.js';

// ─── Version Definitions ────────────────────────────────────────────────────
export const VERSION = '0.1.0';

import { createDatabase as internalCreateDatabase } from './db/database.js';
import { AuditLog as InternalAuditLog } from './privacy/audit-log.js';
import { SignalAnonymizer as InternalSignalAnonymizer } from './processor/anonymizer.js';
import { SignalExtractor as InternalSignalExtractor } from './processor/signal-extractor.js';
import { SessionManager as InternalSessionManager } from './session-manager.js';
import { VSCodeCollector as InternalVSCodeCollector } from './collectors/vscode.js';

/**
* Wires together a fully configured observer system with sensible defaults.
*
* Most integrators do not need to wire dependencies manually. createObserver()
* gives you a fully configured observer in one call with sane defaults.
* If you need custom wiring — all the pieces are exported individually above.
* Use what you need.
*
* @param dbPath - Filesystem path to the SQLite database file, or ':memory:' for transient storage.
* @returns Fully constructed subsystem coordinates.
*/
export function createObserver(dbPath: string): {
sessionManager: InternalSessionManager;
collector: InternalVSCodeCollector;
auditLog: InternalAuditLog;
version: string;
} {
const db = internalCreateDatabase(dbPath);
const auditLog = new InternalAuditLog(db.rawDb);
const anonymizer = new InternalSignalAnonymizer(auditLog);
const extractor = new InternalSignalExtractor(db);
const sessionManager = new InternalSessionManager(
db,
auditLog,
anonymizer,
extractor
);
const collector = new InternalVSCodeCollector();

return {
sessionManager,
collector,
auditLog,
version: VERSION,
};
}
106 changes: 106 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* index.test.ts
*
* Integration and sanity smoke tests for the public index entry point of wrkmark-observer.
* Validates version exports, factory construction dependencies, lifecycle walkthroughs,
* and selective API exposure.
*
* What this does NOT do: It does not verify deep internal database queries or
* anonymizer buckets (those are covered by package unit tests).
*/

import { describe, it, expect } from 'vitest';
import {
VERSION,
createObserver,
WrkmarkObserverError,
AuditLog,
VSCodeCollector,
SessionManager
} from '../src/index.js';

describe('index.ts — Public API and Bootstrap Integration', () => {

it('exports the correct version identifier', () => {
expect(VERSION).toBe('0.1.0');
});

it('exposes all expected fields in the observer layout', () => {
const observer = createObserver(':memory:');
expect(observer).toBeDefined();
expect(observer.sessionManager).toBeDefined();
expect(observer.collector).toBeDefined();
expect(observer.auditLog).toBeDefined();
expect(observer.version).toBe('0.1.0');
});

it('correctly instantiates and wires session managers', () => {
const observer = createObserver(':memory:');
expect(observer.sessionManager).toBeInstanceOf(SessionManager);
});

it('correctly instantiates and wires VS Code collectors', () => {
const observer = createObserver(':memory:');
expect(observer.collector).toBeInstanceOf(VSCodeCollector);
});

it('correctly instantiates and wires audit log chains', () => {
const observer = createObserver(':memory:');
expect(observer.auditLog).toBeInstanceOf(AuditLog);
});

it('executes a clean observation lifecycle integration flow', () => {
const { sessionManager, collector, auditLog } = createObserver(':memory:');

// Start a new observation session
sessionManager.startSession('VS Code');
collector.start(sessionManager);

// Verify state matches active observation
expect(sessionManager.getState().status).toBe('active');
expect(collector.isRunning()).toBe(true);

const sessionId = sessionManager.getState().active_session!.id;

// Simulate keystrokes and document actions
// Privacy guarantee: No character content or paths are passed here.
collector.onKeystroke(1000);
collector.onKeystroke(1500); // Bucket 5
collector.onFileSwitch(2000);

// Conclude observation session
const completed = sessionManager.endSession();
collector.stop();

// Verify session data was finalized
expect(completed.id).toBe(sessionId);
expect(completed.signal_count).toBe(2);
expect(sessionManager.getState().status).toBe('stopped');
expect(collector.isRunning()).toBe(false);

// Verify audit log chain integrity remains unbroken
const verifyResult = auditLog.verifyChain();
expect(verifyResult.valid).toBe(true);
expect(verifyResult.total_records).toBeGreaterThan(0);
});

it('supports transient memory database initialization', () => {
expect(() => {
createObserver(':memory:');
}).not.toThrow();
});

it('exports the custom WrkmarkObserverError class', () => {
const error = new WrkmarkObserverError('Test error message', 'INVALID_SIGNAL_TYPE');
expect(error).toBeInstanceOf(WrkmarkObserverError);
expect(error.code).toBe('INVALID_SIGNAL_TYPE');
});

it('exports the AuditLog class for direct custom configurations', () => {
expect(AuditLog).toBeDefined();
});

it('exports the VSCodeCollector class for direct custom configurations', () => {
expect(VSCodeCollector).toBeDefined();
});
});
Loading