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.
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.
// 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 };
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.
createCognitoVerifier for User PoolsTokenExpiredError, IssuerMismatchError, KidNotFoundError| Export | From |
|---|---|
| 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 |
// 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: '...',
});
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.
AuditEvent shape: actor, action, resource, metadata, occurredAtInMemoryAuditSink for unit tests — no infrastructure requiredDynamoDbAuditSink at @tjoc/audit/dynamodb for serverless productscreateDrizzleAuditSink for SQL-backed productsAuditSink interface| Export | From |
|---|---|
| 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 |
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 },
});
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.
createEmailSender wraps Resend with consistent error semanticsList-Unsubscribe header threadingEmailEventSink for delivery / bounce / complaint loggingrenderTemplate — no template registry, no surprise globalsNotification + NotificationRepository contracts for product-side persistence@tjoc/notifications/push for mobile + web push| Export | From |
|---|---|
| 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 |
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),
});
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.
AnalyticsService wrapper around any AnalyticsProviderNoopProvider auto-selected in non-production environmentsdistinctId from event propertiesidentify seeds person properties on conversionalias bridges anonymous distinct IDs to authenticated ones (v2.2.0)isFeatureEnabled, getExperimentVariant| Export | Method / role |
|---|---|
| AnalyticsService | Service wrapper — trackEvent trackPageView identify alias shutdown |
| AnalyticsService | Flags — isFeatureEnabled getExperimentVariant reloadFeatureFlags |
| AnalyticsProvider | Interface implemented by every provider |
| PostHogProvider | Production implementation, wraps posthog-node |
| NoopProvider | Auto-used when NODE_ENV !== 'production' |
| AnalyticsConfig | Type — apiKey, apiHost, debug |
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);
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.
AIService interface — complete + streamAnthropicService, OpenAIService, GeminiService ship in-packageFallbackAIService chains two providers with automatic failoverAIMessage, AIRequestOptions, AIResponseAsyncIterable<string>formatPrompt, sanitizePrompt, validatePrompt utilitiesAIService backend in minutes| Export | Description |
|---|---|
| AIService | Interface — complete stream |
| AnthropicService | Anthropic Claude backend (default in WiseCV + WiseMarket) |
| OpenAIService | OpenAI GPT backend |
| GeminiService | Google Gemini backend |
| FallbackAIService | Chains primary + fallback |
| formatPrompt | Inject variables into a prompt template |
| sanitizePrompt | Strip unsafe content before sending |
| validatePrompt | Assert length + content rules |
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);
}