@directive-run/knowledge 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/ai/ai-adapters.md +250 -0
  4. package/ai/ai-agents-streaming.md +269 -0
  5. package/ai/ai-budget-resilience.md +235 -0
  6. package/ai/ai-communication.md +281 -0
  7. package/ai/ai-debug-observability.md +243 -0
  8. package/ai/ai-guardrails-memory.md +332 -0
  9. package/ai/ai-mcp-rag.md +288 -0
  10. package/ai/ai-multi-agent.md +274 -0
  11. package/ai/ai-orchestrator.md +227 -0
  12. package/ai/ai-security.md +293 -0
  13. package/ai/ai-tasks.md +261 -0
  14. package/ai/ai-testing-evals.md +378 -0
  15. package/api-skeleton.md +5 -0
  16. package/core/anti-patterns.md +382 -0
  17. package/core/constraints.md +263 -0
  18. package/core/core-patterns.md +228 -0
  19. package/core/error-boundaries.md +322 -0
  20. package/core/multi-module.md +315 -0
  21. package/core/naming.md +283 -0
  22. package/core/plugins.md +344 -0
  23. package/core/react-adapter.md +262 -0
  24. package/core/resolvers.md +357 -0
  25. package/core/schema-types.md +262 -0
  26. package/core/system-api.md +271 -0
  27. package/core/testing.md +257 -0
  28. package/core/time-travel.md +238 -0
  29. package/dist/index.cjs +111 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +10 -0
  32. package/dist/index.d.ts +10 -0
  33. package/dist/index.js +102 -0
  34. package/dist/index.js.map +1 -0
  35. package/examples/ab-testing.ts +385 -0
  36. package/examples/ai-checkpoint.ts +509 -0
  37. package/examples/ai-guardrails.ts +319 -0
  38. package/examples/ai-orchestrator.ts +589 -0
  39. package/examples/async-chains.ts +287 -0
  40. package/examples/auth-flow.ts +371 -0
  41. package/examples/batch-resolver.ts +341 -0
  42. package/examples/checkers.ts +589 -0
  43. package/examples/contact-form.ts +176 -0
  44. package/examples/counter.ts +393 -0
  45. package/examples/dashboard-loader.ts +512 -0
  46. package/examples/debounce-constraints.ts +105 -0
  47. package/examples/dynamic-modules.ts +293 -0
  48. package/examples/error-boundaries.ts +430 -0
  49. package/examples/feature-flags.ts +220 -0
  50. package/examples/form-wizard.ts +347 -0
  51. package/examples/fraud-analysis.ts +663 -0
  52. package/examples/goal-heist.ts +341 -0
  53. package/examples/multi-module.ts +57 -0
  54. package/examples/newsletter.ts +241 -0
  55. package/examples/notifications.ts +210 -0
  56. package/examples/optimistic-updates.ts +317 -0
  57. package/examples/pagination.ts +260 -0
  58. package/examples/permissions.ts +337 -0
  59. package/examples/provider-routing.ts +403 -0
  60. package/examples/server.ts +316 -0
  61. package/examples/shopping-cart.ts +422 -0
  62. package/examples/sudoku.ts +630 -0
  63. package/examples/theme-locale.ts +204 -0
  64. package/examples/time-machine.ts +225 -0
  65. package/examples/topic-guard.ts +306 -0
  66. package/examples/url-sync.ts +333 -0
  67. package/examples/websocket.ts +404 -0
  68. package/package.json +65 -0
@@ -0,0 +1,403 @@
1
+ // Example: provider-routing
2
+ // Source: examples/provider-routing/src/main.ts
3
+ // Extracted for AI rules — DOM wiring stripped
4
+
5
+ /**
6
+ * Smart Provider Router — Constraint-Based Provider Routing & Fallback
7
+ *
8
+ * 3 mock providers (OpenAI, Anthropic, Ollama). Constraint router selects based
9
+ * on cost, error rates, circuit state. Provider fallback chain.
10
+ */
11
+
12
+ import {
13
+ type ModuleSchema,
14
+ createModule,
15
+ createSystem,
16
+ t,
17
+ } from "@directive-run/core";
18
+ import {
19
+ type CircuitState,
20
+ createCircuitBreaker,
21
+ devtoolsPlugin,
22
+ } from "@directive-run/core/plugins";
23
+
24
+ // ============================================================================
25
+ // Types
26
+ // ============================================================================
27
+
28
+ interface ProviderStats {
29
+ name: string;
30
+ callCount: number;
31
+ errorCount: number;
32
+ totalCost: number;
33
+ avgLatencyMs: number;
34
+ circuitState: CircuitState;
35
+ }
36
+
37
+ interface TimelineEntry {
38
+ time: number;
39
+ event: string;
40
+ detail: string;
41
+ type: "route" | "error" | "fallback" | "circuit" | "info" | "success";
42
+ }
43
+
44
+ // ============================================================================
45
+ // Mock Providers
46
+ // ============================================================================
47
+
48
+ const PROVIDERS = {
49
+ openai: { name: "OpenAI", costPer1k: 0.03, baseLatency: 200 },
50
+ anthropic: { name: "Anthropic", costPer1k: 0.025, baseLatency: 250 },
51
+ ollama: { name: "Ollama", costPer1k: 0.001, baseLatency: 400 },
52
+ };
53
+
54
+ const providerErrors: Record<string, boolean> = {
55
+ openai: false,
56
+ anthropic: false,
57
+ ollama: false,
58
+ };
59
+
60
+ const circuitBreakers = {
61
+ openai: createCircuitBreaker({
62
+ name: "openai",
63
+ failureThreshold: 3,
64
+ recoveryTimeMs: 5000,
65
+ halfOpenMaxRequests: 2,
66
+ onStateChange: (from, to) =>
67
+ }),
68
+ anthropic: createCircuitBreaker({
69
+ name: "anthropic",
70
+ failureThreshold: 3,
71
+ recoveryTimeMs: 5000,
72
+ halfOpenMaxRequests: 2,
73
+ onStateChange: (from, to) =>
74
+ }),
75
+ ollama: createCircuitBreaker({
76
+ name: "ollama",
77
+ failureThreshold: 3,
78
+ recoveryTimeMs: 5000,
79
+ halfOpenMaxRequests: 2,
80
+ onStateChange: (from, to) =>
81
+ }),
82
+ };
83
+
84
+ // ============================================================================
85
+ // Timeline
86
+ // ============================================================================
87
+
88
+ const timeline: TimelineEntry[] = [];
89
+
90
+ function addTimeline(
91
+ event: string,
92
+ detail: string,
93
+ type: TimelineEntry["type"],
94
+ ) {
95
+ timeline.unshift({ time: Date.now(), event, detail, type });
96
+ if (timeline.length > 50) {
97
+ timeline.length = 50;
98
+ }
99
+ }
100
+
101
+ // ============================================================================
102
+ // Schema
103
+ // ============================================================================
104
+
105
+ const schema = {
106
+ facts: {
107
+ openaiStats: t.object<ProviderStats>(),
108
+ anthropicStats: t.object<ProviderStats>(),
109
+ ollamaStats: t.object<ProviderStats>(),
110
+ budgetRemaining: t.number(),
111
+ budgetTotal: t.number(),
112
+ preferCheapest: t.boolean(),
113
+ lastProvider: t.string(),
114
+ totalRequests: t.number(),
115
+ lastError: t.string(),
116
+ },
117
+ derivations: {
118
+ openaiCircuit: t.string<CircuitState>(),
119
+ anthropicCircuit: t.string<CircuitState>(),
120
+ ollamaCircuit: t.string<CircuitState>(),
121
+ cheapestAvailable: t.string(),
122
+ allDown: t.boolean(),
123
+ },
124
+ events: {
125
+ toggleProviderError: { provider: t.string() },
126
+ setBudget: { value: t.number() },
127
+ togglePreferCheapest: {},
128
+ resetStats: {},
129
+ },
130
+ requirements: {},
131
+ } satisfies ModuleSchema;
132
+
133
+ // ============================================================================
134
+ // Module
135
+ // ============================================================================
136
+
137
+ function defaultStats(name: string): ProviderStats {
138
+ return {
139
+ name,
140
+ callCount: 0,
141
+ errorCount: 0,
142
+ totalCost: 0,
143
+ avgLatencyMs: 0,
144
+ circuitState: "CLOSED",
145
+ };
146
+ }
147
+
148
+ const routerModule = createModule("router", {
149
+ schema,
150
+
151
+ init: (facts) => {
152
+ facts.openaiStats = defaultStats("OpenAI");
153
+ facts.anthropicStats = defaultStats("Anthropic");
154
+ facts.ollamaStats = defaultStats("Ollama");
155
+ facts.budgetRemaining = 1.0;
156
+ facts.budgetTotal = 1.0;
157
+ facts.preferCheapest = false;
158
+ facts.lastProvider = "";
159
+ facts.totalRequests = 0;
160
+ facts.lastError = "";
161
+ },
162
+
163
+ derive: {
164
+ openaiCircuit: () => circuitBreakers.openai.getState(),
165
+ anthropicCircuit: () => circuitBreakers.anthropic.getState(),
166
+ ollamaCircuit: () => circuitBreakers.ollama.getState(),
167
+ cheapestAvailable: () => {
168
+ const available: { name: string; cost: number }[] = [];
169
+ for (const [id, config] of Object.entries(PROVIDERS)) {
170
+ const breaker = circuitBreakers[id as keyof typeof circuitBreakers];
171
+ if (breaker.isAllowed()) {
172
+ available.push({ name: id, cost: config.costPer1k });
173
+ }
174
+ }
175
+ available.sort((a, b) => a.cost - b.cost);
176
+
177
+ return available.length > 0 ? available[0]!.name : "none";
178
+ },
179
+ allDown: () =>
180
+ !circuitBreakers.openai.isAllowed() &&
181
+ !circuitBreakers.anthropic.isAllowed() &&
182
+ !circuitBreakers.ollama.isAllowed(),
183
+ },
184
+
185
+ events: {
186
+ toggleProviderError: (_facts, { provider }) => {
187
+ providerErrors[provider] = !providerErrors[provider];
188
+ },
189
+ setBudget: (facts, { value }) => {
190
+ facts.budgetRemaining = value;
191
+ facts.budgetTotal = value;
192
+ },
193
+ togglePreferCheapest: (facts) => {
194
+ facts.preferCheapest = !facts.preferCheapest;
195
+ },
196
+ resetStats: (facts) => {
197
+ facts.openaiStats = defaultStats("OpenAI");
198
+ facts.anthropicStats = defaultStats("Anthropic");
199
+ facts.ollamaStats = defaultStats("Ollama");
200
+ facts.budgetRemaining = facts.budgetTotal;
201
+ facts.lastProvider = "";
202
+ facts.totalRequests = 0;
203
+ facts.lastError = "";
204
+ providerErrors.openai = false;
205
+ providerErrors.anthropic = false;
206
+ providerErrors.ollama = false;
207
+ circuitBreakers.openai.reset();
208
+ circuitBreakers.anthropic.reset();
209
+ circuitBreakers.ollama.reset();
210
+ timeline.length = 0;
211
+ },
212
+ },
213
+ });
214
+
215
+ // ============================================================================
216
+ // System
217
+ // ============================================================================
218
+
219
+ const system = createSystem({
220
+ module: routerModule,
221
+ debug: { runHistory: true },
222
+ plugins: [devtoolsPlugin({ name: "provider-routing" })],
223
+ });
224
+ system.start();
225
+
226
+ // ============================================================================
227
+ // Routing Logic
228
+ // ============================================================================
229
+
230
+ function selectProvider(): string | null {
231
+ const budget = system.facts.budgetRemaining as number;
232
+ const preferCheapest = system.facts.preferCheapest as boolean;
233
+
234
+ // Collect available providers (circuit breaker allows + within budget)
235
+ const available: { id: string; cost: number }[] = [];
236
+ for (const [id, config] of Object.entries(PROVIDERS)) {
237
+ const breaker = circuitBreakers[id as keyof typeof circuitBreakers];
238
+ if (breaker.isAllowed() && budget >= config.costPer1k) {
239
+ available.push({ id, cost: config.costPer1k });
240
+ }
241
+ }
242
+
243
+ if (available.length === 0) {
244
+ return null;
245
+ }
246
+
247
+ if (preferCheapest) {
248
+ available.sort((a, b) => a.cost - b.cost);
249
+
250
+ return available[0]!.id;
251
+ }
252
+
253
+ // Default: prefer openai > anthropic > ollama
254
+ const priority = ["openai", "anthropic", "ollama"];
255
+ for (const id of priority) {
256
+ if (available.find((a) => a.id === id)) {
257
+ return id;
258
+ }
259
+ }
260
+
261
+ return available[0]!.id;
262
+ }
263
+
264
+ async function executeProvider(providerId: string): Promise<boolean> {
265
+ const breaker = circuitBreakers[providerId as keyof typeof circuitBreakers];
266
+ const config = PROVIDERS[providerId as keyof typeof PROVIDERS]!;
267
+ const statsKey = `${providerId}Stats` as
268
+ | "openaiStats"
269
+ | "anthropicStats"
270
+ | "ollamaStats";
271
+
272
+ try {
273
+ await breaker.execute(async () => {
274
+ await new Promise((resolve) =>
275
+ setTimeout(resolve, config.baseLatency + Math.random() * 100),
276
+ );
277
+
278
+ if (providerErrors[providerId]) {
279
+ throw new Error(`${config.name}: simulated error`);
280
+ }
281
+ });
282
+
283
+ const stats = system.facts[statsKey] as ProviderStats;
284
+ const cost = config.costPer1k;
285
+ const latency = config.baseLatency + Math.random() * 100;
286
+ system.facts[statsKey] = {
287
+ ...stats,
288
+ callCount: stats.callCount + 1,
289
+ totalCost: Math.round((stats.totalCost + cost) * 1000) / 1000,
290
+ avgLatencyMs: Math.round(
291
+ (stats.avgLatencyMs * stats.callCount + latency) /
292
+ (stats.callCount + 1),
293
+ ),
294
+ circuitState: breaker.getState(),
295
+ };
296
+ system.facts.budgetRemaining =
297
+ Math.round(((system.facts.budgetRemaining as number) - cost) * 1000) /
298
+ 1000;
299
+ system.facts.lastError = "";
300
+
301
+ return true;
302
+ } catch (err) {
303
+ const stats = system.facts[statsKey] as ProviderStats;
304
+ system.facts[statsKey] = {
305
+ ...stats,
306
+ errorCount: stats.errorCount + 1,
307
+ circuitState: breaker.getState(),
308
+ };
309
+ system.facts.lastError = err instanceof Error ? err.message : String(err);
310
+
311
+ return false;
312
+ }
313
+ }
314
+
315
+ async function sendRequest() {
316
+ system.facts.totalRequests = (system.facts.totalRequests as number) + 1;
317
+
318
+ const providerId = selectProvider();
319
+ if (!providerId) {
320
+ system.facts.lastError = "All providers unavailable or over budget";
321
+
322
+ return;
323
+ }
324
+
325
+ system.facts.lastProvider = providerId;
326
+
327
+ const success = await executeProvider(providerId);
328
+ if (success) {
329
+ return;
330
+ }
331
+
332
+ // Primary failed — try fallback
333
+ const fallbackId = selectProvider();
334
+ if (fallbackId && fallbackId !== providerId) {
335
+ system.facts.lastProvider = fallbackId;
336
+ await executeProvider(fallbackId);
337
+ }
338
+ }
339
+
340
+ // ============================================================================
341
+ // DOM References
342
+ // ============================================================================
343
+
344
+
345
+ // ============================================================================
346
+ // Render
347
+ // ============================================================================
348
+
349
+ function escapeHtml(text: string): string {
350
+
351
+ return div.innerHTML;
352
+ }
353
+
354
+ function circuitBadge(state: CircuitState): string {
355
+ const cls =
356
+ state === "CLOSED" ? "closed" : state === "OPEN" ? "open" : "half-open";
357
+
358
+ return `<span class="pr-circuit-badge ${cls}">${state}</span>`;
359
+ }
360
+
361
+ stats: ProviderStats,
362
+ state: CircuitState,
363
+ ): void {
364
+ ${circuitBadge(state)}
365
+ <span style="font-size:0.55rem;color:var(--brand-text-dim)">${stats.callCount} calls, ${stats.errorCount} err, $${stats.totalCost}</span>
366
+ `;
367
+ }
368
+
369
+
370
+ // ============================================================================
371
+ // Subscribe
372
+ // ============================================================================
373
+
374
+ const allKeys = [
375
+ ...Object.keys(schema.facts),
376
+ ...Object.keys(schema.derivations),
377
+ ];
378
+ system.subscribe(allKeys, render);
379
+
380
+ setInterval(render, 1000);
381
+
382
+ // ============================================================================
383
+ // Controls
384
+ // ============================================================================
385
+
386
+
387
+ for (const id of ["openai", "anthropic", "ollama"]) {
388
+ system.events.toggleProviderError({ provider: id });
389
+ });
390
+ }
391
+
392
+ "input",
393
+ (e) => {
394
+ system.events.setBudget({ value });
395
+ },
396
+ );
397
+
398
+
399
+ // ============================================================================
400
+ // Initial Render
401
+ // ============================================================================
402
+
403
+ render();
@@ -0,0 +1,316 @@
1
+ // Example: server
2
+ // Source: examples/server/src/server.ts
3
+ // Pure module file — no DOM wiring
4
+
5
+ /**
6
+ * Directive Server Example
7
+ *
8
+ * An Express API demonstrating Directive's server-side features:
9
+ * - Distributable snapshots with TTL
10
+ * - Signed snapshot verification (HMAC-SHA256)
11
+ * - Cryptographic audit trail
12
+ * - GDPR/CCPA compliance tooling
13
+ *
14
+ * Run: npx tsx --watch src/server.ts
15
+ */
16
+
17
+ import {
18
+ createAuditTrail,
19
+ createCompliance,
20
+ createInMemoryComplianceStorage,
21
+ } from "@directive-run/ai";
22
+ import {
23
+ createSystem,
24
+ isSnapshotExpired,
25
+ signSnapshot,
26
+ verifySnapshotSignature,
27
+ } from "@directive-run/core";
28
+ import express from "express";
29
+ import { userProfile } from "./module.js";
30
+
31
+ const app = express();
32
+ app.use(express.json());
33
+
34
+ // ============================================================================
35
+ // Shared Infrastructure
36
+ // ============================================================================
37
+
38
+ const SIGNING_SECRET =
39
+ process.env.SIGNING_SECRET ?? "dev-secret-change-in-production";
40
+
41
+ // Audit trail – shared across requests, acts as a Directive plugin
42
+ const audit = createAuditTrail({
43
+ maxEntries: 10_000,
44
+ retentionMs: 7 * 24 * 60 * 60 * 1000, // 7 days
45
+ piiMasking: {
46
+ enabled: true,
47
+ types: ["email", "name"],
48
+ redactionStyle: "masked",
49
+ },
50
+ });
51
+
52
+ // Compliance – in-memory storage for this example (use a DB adapter in production)
53
+ const compliance = createCompliance({
54
+ storage: createInMemoryComplianceStorage(),
55
+ consentPurposes: ["analytics", "marketing", "personalization"],
56
+ });
57
+
58
+ // In-memory snapshot cache (use Redis in production)
59
+ const snapshotCache = new Map<
60
+ string,
61
+ { snapshot: unknown; cachedAt: number }
62
+ >();
63
+ const CACHE_TTL_MS = 60_000; // 1 minute
64
+
65
+ // ============================================================================
66
+ // Helper: Per-Request System Factory
67
+ // ============================================================================
68
+
69
+ function createUserSystem(userId: string) {
70
+ const system = createSystem({
71
+ module: userProfile,
72
+ plugins: [audit.createPlugin()],
73
+ });
74
+
75
+ system.start();
76
+ system.events.loadUser({ userId });
77
+
78
+ return system;
79
+ }
80
+
81
+ // ============================================================================
82
+ // GET /snapshot/:userId
83
+ // Distributable Snapshots with TTL
84
+ // ============================================================================
85
+
86
+ app.get("/snapshot/:userId", async (req, res) => {
87
+ const { userId } = req.params;
88
+
89
+ // Check cache first
90
+ const cached = snapshotCache.get(userId);
91
+ if (cached && Date.now() - cached.cachedAt < CACHE_TTL_MS) {
92
+ res.json({ source: "cache", snapshot: cached.snapshot });
93
+
94
+ return;
95
+ }
96
+
97
+ // Create a per-request system, settle it, then export a distributable snapshot
98
+ const system = createUserSystem(userId);
99
+
100
+ try {
101
+ await system.settle(5000);
102
+
103
+ const snapshot = system.getDistributableSnapshot({
104
+ includeDerivations: ["effectivePlan", "canUseFeature", "isReady"],
105
+ ttlSeconds: 3600,
106
+ });
107
+
108
+ // Cache it
109
+ snapshotCache.set(userId, { snapshot, cachedAt: Date.now() });
110
+
111
+ res.json({ source: "fresh", snapshot });
112
+ } catch (error) {
113
+ res.status(500).json({ error: "Failed to settle system" });
114
+ } finally {
115
+ system.destroy();
116
+ }
117
+ });
118
+
119
+ // ============================================================================
120
+ // POST /snapshot/:userId/verify
121
+ // Signed Snapshot Verification
122
+ // ============================================================================
123
+
124
+ app.post("/snapshot/:userId/verify", async (req, res) => {
125
+ const { snapshot } = req.body;
126
+
127
+ if (!snapshot) {
128
+ res.status(400).json({ error: "Missing snapshot in request body" });
129
+
130
+ return;
131
+ }
132
+
133
+ // Sign a fresh snapshot for this user
134
+ const system = createUserSystem(req.params.userId);
135
+
136
+ try {
137
+ await system.settle(5000);
138
+
139
+ const freshSnapshot = system.getDistributableSnapshot({
140
+ includeDerivations: ["effectivePlan", "canUseFeature", "isReady"],
141
+ ttlSeconds: 3600,
142
+ });
143
+
144
+ // Sign the snapshot with HMAC-SHA256
145
+ const signed = await signSnapshot(freshSnapshot, SIGNING_SECRET);
146
+
147
+ // Verify the provided snapshot's signature
148
+ if (snapshot.signature) {
149
+ const isValid = await verifySnapshotSignature(snapshot, SIGNING_SECRET);
150
+ const isExpired = isSnapshotExpired(snapshot);
151
+
152
+ res.json({
153
+ signatureValid: isValid,
154
+ expired: isExpired,
155
+ signedSnapshot: signed,
156
+ });
157
+ } else {
158
+ // No signature on the incoming snapshot – just return a signed version
159
+ res.json({
160
+ signatureValid: null,
161
+ expired: false,
162
+ signedSnapshot: signed,
163
+ });
164
+ }
165
+ } catch (error) {
166
+ res.status(500).json({ error: "Verification failed" });
167
+ } finally {
168
+ system.destroy();
169
+ }
170
+ });
171
+
172
+ // ============================================================================
173
+ // GET /audit
174
+ // Query Audit Entries
175
+ // ============================================================================
176
+
177
+ app.get("/audit", (req, res) => {
178
+ const { eventType, since, actorId, limit } = req.query;
179
+
180
+ // biome-ignore lint/suspicious/noExplicitAny: eventType comes from query string
181
+ const entries = audit.getEntries({
182
+ eventTypes: eventType ? [eventType as any] : undefined,
183
+ since: since ? Number(since) : undefined,
184
+ actorId: actorId as string | undefined,
185
+ limit: limit ? Number(limit) : 50,
186
+ });
187
+
188
+ res.json({
189
+ count: entries.length,
190
+ entries,
191
+ });
192
+ });
193
+
194
+ // ============================================================================
195
+ // GET /audit/verify
196
+ // Verify Audit Hash Chain Integrity
197
+ // ============================================================================
198
+
199
+ app.get("/audit/verify", async (_req, res) => {
200
+ const result = await audit.verifyChain();
201
+
202
+ res.json({
203
+ chainValid: result.valid,
204
+ entriesVerified: result.entriesVerified,
205
+ brokenAt: result.brokenAt ?? null,
206
+ verifiedAt: new Date(result.verifiedAt).toISOString(),
207
+ });
208
+ });
209
+
210
+ // ============================================================================
211
+ // POST /compliance/:subjectId/export
212
+ // GDPR Article 20 – Data Export
213
+ // ============================================================================
214
+
215
+ app.post("/compliance/:subjectId/export", async (req, res) => {
216
+ const { subjectId } = req.params;
217
+
218
+ // Record consent for analytics before exporting
219
+ await compliance.consent.grant(subjectId, "analytics", {
220
+ source: "api-request",
221
+ });
222
+
223
+ const result = await compliance.exportData({
224
+ subjectId,
225
+ format: "json",
226
+ includeAudit: true,
227
+ });
228
+
229
+ if (result.success) {
230
+ res.json({
231
+ subjectId,
232
+ exportedAt: new Date(result.exportedAt).toISOString(),
233
+ expiresAt: result.expiresAt
234
+ ? new Date(result.expiresAt).toISOString()
235
+ : null,
236
+ recordCount: result.recordCount,
237
+ checksum: result.checksum,
238
+ data: JSON.parse(result.data),
239
+ });
240
+ } else {
241
+ res.status(500).json({ error: "Export failed" });
242
+ }
243
+ });
244
+
245
+ // ============================================================================
246
+ // POST /compliance/:subjectId/delete
247
+ // GDPR Article 17 – Right to Erasure
248
+ // ============================================================================
249
+
250
+ app.post("/compliance/:subjectId/delete", async (req, res) => {
251
+ const { subjectId } = req.params;
252
+ const { reason } = req.body;
253
+
254
+ const result = await compliance.deleteData({
255
+ subjectId,
256
+ scope: "all",
257
+ reason: reason ?? "GDPR Article 17 request",
258
+ });
259
+
260
+ if (result.success) {
261
+ res.json({
262
+ subjectId,
263
+ deletedAt: new Date(result.deletedAt).toISOString(),
264
+ recordsAffected: result.recordsAffected,
265
+ certificate: result.certificate,
266
+ });
267
+ } else {
268
+ res.status(500).json({ error: "Deletion failed" });
269
+ }
270
+ });
271
+
272
+ // ============================================================================
273
+ // GET /health
274
+ // Health Check
275
+ // ============================================================================
276
+
277
+ app.get("/health", (_req, res) => {
278
+ const auditStats = audit.getStats();
279
+
280
+ res.json({
281
+ status: "ok",
282
+ audit: {
283
+ totalEntries: auditStats.totalEntries,
284
+ oldestEntry: auditStats.oldestEntry
285
+ ? new Date(auditStats.oldestEntry).toISOString()
286
+ : null,
287
+ newestEntry: auditStats.newestEntry
288
+ ? new Date(auditStats.newestEntry).toISOString()
289
+ : null,
290
+ chainIntegrity: auditStats.chainIntegrity,
291
+ },
292
+ });
293
+ });
294
+
295
+ // ============================================================================
296
+ // Start
297
+ // ============================================================================
298
+
299
+ const PORT = Number(process.env.PORT ?? 3000);
300
+
301
+ app.listen(PORT, () => {
302
+ console.log(`Directive server example running on http://localhost:${PORT}`);
303
+ console.log();
304
+ console.log("Endpoints:");
305
+ console.log(
306
+ " GET /snapshot/:userId Distributable snapshot with TTL",
307
+ );
308
+ console.log(" POST /snapshot/:userId/verify Sign and verify snapshots");
309
+ console.log(" GET /audit Query audit entries");
310
+ console.log(" GET /audit/verify Verify hash chain integrity");
311
+ console.log(" POST /compliance/:subjectId/export GDPR data export");
312
+ console.log(" POST /compliance/:subjectId/delete GDPR right to erasure");
313
+ console.log(" GET /health Health check");
314
+ console.log();
315
+ console.log("Try: curl http://localhost:3000/snapshot/user-1");
316
+ });