Shared Packages · @tjoc

The shared layer
behind the portfolio.

Six TypeScript packages on npm that every tjoc.dev product reaches for: types, authentication, audit, notifications, analytics, and AI primitives. Built on AWS-aligned services (Cognito, DynamoDB, Lambda) with framework- agnostic cores and adapter packages for each environment.

6 Packages
TS TypeScript-first
ESM + CJS dual build
npm Public registry
01 — Foundation

@tjoc/types

v4.9.0 ● npm TypeScript Type-only Per-product subpaths

Shared TypeScript type definitions for every tjoc.dev product. Type-only — no runtime, no Zod, no validation; just the interfaces every consumer agrees on. Per-product subpaths (@tjoc/types/jobs, /wisecv, /star-club, /wisemarket) so consumers pull only what they need.

$ npm install @tjoc/types
Subpath catalogue
Root @tjoc/types
Status Environment PaginationParams PaginatedResponse<T> ApiResponse<T> ErrorResponse User AuthResponse FirebaseToken
Jobs @tjoc/types/jobs
Role User ClientProfile PricingTier Job JobSource SalaryPeriod Application ApplicationStatus Assessment AssessmentTemplate AssessmentRubric BillingEvent Payout ExternalApplicationClick IntegritySignals
WiseCV @tjoc/types/wisecv
CV TailoredCV JobDescription RubricScore BillingTier
star.club @tjoc/types/star-club
Profile Goal Milestone Mentor
WiseMarket @tjoc/types/wisemarket
Seller Listing TrustScore TrustEvent Inquiry Review KycRecord WMAuditEventType
TypeScript
// Import only the product surface you need
import type {
  Job,
  ClientProfile,
  PricingTier,
  Application,
  BillingEvent,
} from '@tjoc/types/jobs';

// Tier-priced billing — type-checked end to end
function quoteListingFee(profile: ClientProfile): number {
  const tier: PricingTier = profile.pricingTier ?? 'bronze';
  // ...rest of pricing logic
}

// One re-export keeps consumers on a single import path
export type { Job, Application, BillingEvent };
02 — Identity

@tjoc/auth

v8.0.1 ● npm Cognito JWT Framework-agnostic

Cognito-first JWT verification and auth primitives. A framework-agnostic core (verifyJwt + JwksClient + typed errors) with adapters per environment: @tjoc/auth/cognito for AWS User Pools, @tjoc/auth/lambda for Lambda authorizers, @tjoc/auth/browser for browser sign-in flows, @tjoc/auth/testing for mocked verifiers in unit tests.

$ npm install @tjoc/auth
Features
  • Framework-agnostic core — verify a JWT against any JWKS
  • JWKS client with in-memory caching and configurable TTL
  • Cognito adapter exposes createCognitoVerifier for User Pools
  • Browser adapter wraps Cognito sign-in / refresh / sign-out for SPAs
  • Lambda adapter ships an authorizer-shaped middleware
  • Typed errors: TokenExpiredError, IssuerMismatchError, KidNotFoundError
  • Test utilities for mock verifiers and synthetic JWKS
API surface
ExportFrom
verifyJwt@tjoc/auth — framework-agnostic verifier
createJwksClient@tjoc/auth — cached JWKS fetcher
extractPrincipal@tjoc/auth — pulls a typed Principal out of verified claims
createCognitoVerifier@tjoc/auth/cognito — Cognito User Pool verifier
createCognitoClient@tjoc/auth/browser — browser-side sign in / refresh / sign out
lambda adapter@tjoc/auth/lambda — Lambda authorizer shape
test verifier@tjoc/auth/testing — mock JWKS + signed tokens
Quick start
TypeScript
// Backend (Lambda / Hono / Express) — Cognito-first
import { createCognitoVerifier } from '@tjoc/auth/cognito';

const verify = createCognitoVerifier({
  region:     process.env.AWS_REGION!,
  userPoolId: process.env.COGNITO_USER_POOL_ID!,
  clientId:   process.env.COGNITO_CLIENT_ID!,
  tokenUse:   'access',
});

// Pass the bearer token, get a typed Principal back
const principal = await verify(req.headers.authorization);
// principal.sub, principal.email, principal.role, ...

// Browser — sign in / refresh / sign out
import { createCognitoClient } from '@tjoc/auth/browser';

const auth = createCognitoClient({
  userPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID,
  clientId:   import.meta.env.VITE_COGNITO_CLIENT_ID,
});

const { accessToken, refreshToken } = await auth.signIn({
  email: 'jane@acme.co',
  password: '...',
});
03 — Audit trail

@tjoc/audit

v1.0.1 ● npm DynamoDB Drizzle Sink-pluggable

A stable AuditEvent shape plus pluggable AuditSink implementations — in-memory for tests, DynamoDB for serverless products (@tjoc/audit/dynamodb), and a Drizzle adapter for SQL-backed products. Each portfolio product writes to its own audit-events table; this package just ships the contract and the sinks.

$ npm install @tjoc/audit
Features
  • Stable AuditEvent shape: actor, action, resource, metadata, occurredAt
  • Normalisation guarantees: ID and timestamp filled in if omitted
  • InMemoryAuditSink for unit tests — no infrastructure required
  • DynamoDbAuditSink at @tjoc/audit/dynamodb for serverless products
  • createDrizzleAuditSink for SQL-backed products
  • Drop a new sink in by implementing the AuditSink interface
  • Fail-safe by design: emit errors are logged, never thrown back to the request path
API surface
ExportFrom
AuditEvent@tjoc/audit — type
AuditSink@tjoc/audit — interface every sink implements
NormalisedAuditEvent@tjoc/audit — post-normalise shape
InMemoryAuditSink@tjoc/audit — in-process sink for tests
createDrizzleAuditSink@tjoc/audit — Drizzle insert adapter
DynamoDbAuditSink@tjoc/audit/dynamodb — PAY_PER_REQUEST table sink
Quick start
TypeScript
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDbAuditSink } from '@tjoc/audit/dynamodb';
import type { AuditEvent } from '@tjoc/audit';

const sink = new DynamoDbAuditSink({
  client: new DynamoDBClient({}),
  tableName: process.env.AUDIT_EVENTS_TABLE!,
});

// Emit anywhere a state change happens
export async function audit(event: AuditEvent): Promise<void> {
  await sink.emit(event).catch((err) => {
    // sinks never throw upstream — just log
    console.error('audit emit failed', err);
  });
}

// Usage at a handler
await audit({
  actorId:      session.userId,
  actorType:    session.role,
  action:       'job.published',
  resourceType: 'job',
  resourceId:   jobId,
  metadata:     { title, tier: profile.pricingTier },
});
04 — Outbound comms

@tjoc/notifications

v4.0.0 ● npm Resend Email + Push + In-app List-Unsubscribe

Email, push, and in-app notification primitives. The email sender wraps Resend with per-recipient List-Unsubscribe, suppression callbacks, and an EmailEventSink for delivery / bounce / complaint logging. Plain template rendering (no registry) plus a set of pre-built user-action templates for welcome / verify / reset / password-changed. Push surface lives at @tjoc/notifications/push.

$ npm install @tjoc/notifications
Features
  • createEmailSender wraps Resend with consistent error semantics
  • Per-recipient List-Unsubscribe header threading
  • Suppression check hook — caller decides if an address is suppressed before send
  • Pluggable EmailEventSink for delivery / bounce / complaint logging
  • Stateless renderTemplate — no template registry, no surprise globals
  • Pre-built user-action templates (welcome, verify, reset, password-changed)
  • In-app Notification + NotificationRepository contracts for product-side persistence
  • Push module at @tjoc/notifications/push for mobile + web push
API surface
ExportFrom
createEmailSender@tjoc/notifications — Resend-backed sender
renderTemplate@tjoc/notifications — stateless template render
userActionTemplates@tjoc/notifications — pre-built welcome / verify / reset templates
EmailEventSink@tjoc/notifications — type for delivery / bounce logging
Notification, NotificationRepository@tjoc/notifications — in-app contracts
push surface@tjoc/notifications/push — mobile + web push
Quick start
TypeScript
import {
  createEmailSender,
  renderTemplate,
  userActionTemplates,
} from '@tjoc/notifications';

const sender = createEmailSender({
  apiKey: process.env.RESEND_API_KEY!,
  from: 'noreply@tjoc.dev',
  eventSink: {
    async log(event) {
      // event.status: 'success' | 'failure' | 'suppressed'
      await logEmailEvent(event);
    },
  },
});

// Render a pre-built welcome template
const rendered = renderTemplate(userActionTemplates.welcome, {
  name: 'Jane',
  productName: 'TJOC Jobs',
  loginUrl: 'https://jobs.tjoc.dev/login',
});

await sender.send({
  to: 'jane@acme.co',
  subject: rendered.subject,
  html: rendered.html,
  listUnsubscribeUrl: `https://api.jobs.tjoc.dev/unsubscribe?token=...`,
  // Caller-owned suppression check
  suppressionCheck: async (email) => isSuppressed(email),
});
05 — Analytics

@tjoc/analytics

v2.2.0 ● npm PostHog Provider-pluggable Fail-safe

Provider-pluggable analytics service. Ships with a PostHog implementation (PostHogProvider) and a no-op fallback (NoopProvider) auto-selected outside production. Every method is wrapped in try/catch and logs on failure — analytics never throws into the request path. v2.2.0 adds alias() for collapsing pre-signup and post-signup distinct IDs onto one PostHog Person.

$ npm install @tjoc/analytics
Features
  • Single AnalyticsService wrapper around any AnalyticsProvider
  • PostHog provider out of the box — flushAt 20, flushInterval 10s
  • NoopProvider auto-selected in non-production environments
  • Server-side capture with distinctId from event properties
  • identify seeds person properties on conversion
  • alias bridges anonymous distinct IDs to authenticated ones (v2.2.0)
  • Feature flag and experiment APIs: isFeatureEnabled, getExperimentVariant
  • Fail-safe: every call wrapped in try/catch + console.error
API surface
ExportMethod / role
AnalyticsServiceService wrapper — trackEvent trackPageView identify alias shutdown
AnalyticsServiceFlags — isFeatureEnabled getExperimentVariant reloadFeatureFlags
AnalyticsProviderInterface implemented by every provider
PostHogProviderProduction implementation, wraps posthog-node
NoopProviderAuto-used when NODE_ENV !== 'production'
AnalyticsConfigType — apiKey, apiHost, debug
Quick start
TypeScript
import { AnalyticsService } from '@tjoc/analytics';

const analytics = new AnalyticsService({
  apiKey:  process.env.POSTHOG_API_KEY!,
  apiHost: 'https://eu.i.posthog.com',
});

// Capture an event keyed on a distinct ID
await analytics.trackEvent('lead_submitted', {
  userId: leadId,
  expectedHiresIn90Days: '2-5',
  hiringTimeline: 'next_quarter',
});

// Identify on conversion to seed person properties
await analytics.identify(clientUserId, {
  email:       'jane@acme.co',
  companyName: 'Acme Co.',
  fromLeadId:  leadId,
});

// v2.2.0: collapse the lead's anonymous distinct ID
// onto the new client's authenticated distinct ID
await analytics.alias(leadId, clientUserId);

// Feature flags + experiments
const enabled = await analytics.isFeatureEnabled('new-onboarding', clientUserId);
const variant = await analytics.getExperimentVariant('cta-copy', clientUserId);
06 — AI primitives

@tjoc/ai-core

v3.0.1 ● npm Anthropic OpenAI Gemini Streaming

A single AIService interface backed by Anthropic, OpenAI, or Gemini. Swap providers without changing application code. FallbackAIService chains a primary and a fallback so a provider outage degrades quietly. Prompt utilities for formatting, sanitisation, and validation. Streaming and sync completion are both first-class.

$ npm install @tjoc/ai-core
Features
  • Single AIService interface — complete + stream
  • AnthropicService, OpenAIService, GeminiService ship in-package
  • FallbackAIService chains two providers with automatic failover
  • Typed AIMessage, AIRequestOptions, AIResponse
  • Streaming support via AsyncIterable<string>
  • formatPrompt, sanitizePrompt, validatePrompt utilities
  • Implement your own AIService backend in minutes
API surface
ExportDescription
AIServiceInterface — complete stream
AnthropicServiceAnthropic Claude backend (default in WiseCV + WiseMarket)
OpenAIServiceOpenAI GPT backend
GeminiServiceGoogle Gemini backend
FallbackAIServiceChains primary + fallback
formatPromptInject variables into a prompt template
sanitizePromptStrip unsafe content before sending
validatePromptAssert length + content rules
Quick start
TypeScript
import {
  AnthropicService,
  FallbackAIService,
  OpenAIService,
  formatPrompt,
  sanitizePrompt,
} from '@tjoc/ai-core';

// Primary: Claude — Fallback: GPT
const ai = new FallbackAIService(
  new AnthropicService({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  new OpenAIService({ apiKey: process.env.OPENAI_API_KEY! }),
);

const TAILOR_TEMPLATE = `Tailor this CV for the role.
Role: {{role}}
CV: {{cv}}`;

const prompt = formatPrompt(TAILOR_TEMPLATE, {
  role: jobDescription,
  cv:   sanitizePrompt(cvText),
});

// Single completion — JSON schema lock done in the system message
const { text } = await ai.complete(prompt, {
  model:       'claude-haiku-4',
  system:      'Output JSON only.',
  maxTokens:   2048,
  temperature: 0.2,
});

// Or stream tokens as they arrive
for await (const chunk of ai.stream(prompt)) {
  process.stdout.write(chunk);
}