@varshylinc/onboarding-consent-engine 0.1.0 → 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 (83) hide show
  1. package/README.md +27 -0
  2. package/dist/client/actions.d.ts +33 -0
  3. package/dist/client/actions.d.ts.map +1 -0
  4. package/dist/client/actions.js +47 -0
  5. package/dist/client/actions.js.map +1 -0
  6. package/dist/client/components/ConsentBlock.d.ts +13 -0
  7. package/dist/client/components/ConsentBlock.d.ts.map +1 -0
  8. package/dist/client/components/ConsentBlock.js +14 -0
  9. package/dist/client/components/ConsentBlock.js.map +1 -0
  10. package/dist/client/components/ConsentCheckbox.d.ts +11 -0
  11. package/dist/client/components/ConsentCheckbox.d.ts.map +1 -0
  12. package/dist/client/components/ConsentCheckbox.js +5 -0
  13. package/dist/client/components/ConsentCheckbox.js.map +1 -0
  14. package/dist/client/components/ConsentUpdateModal.d.ts +12 -0
  15. package/dist/client/components/ConsentUpdateModal.d.ts.map +1 -0
  16. package/dist/client/components/ConsentUpdateModal.js +9 -0
  17. package/dist/client/components/ConsentUpdateModal.js.map +1 -0
  18. package/dist/client/components/EmptyState.d.ts +8 -0
  19. package/dist/client/components/EmptyState.d.ts.map +1 -0
  20. package/dist/client/components/EmptyState.js +5 -0
  21. package/dist/client/components/EmptyState.js.map +1 -0
  22. package/dist/client/components/SignupConsentBlock.d.ts +15 -0
  23. package/dist/client/components/SignupConsentBlock.d.ts.map +1 -0
  24. package/dist/client/components/SignupConsentBlock.js +11 -0
  25. package/dist/client/components/SignupConsentBlock.js.map +1 -0
  26. package/dist/client/components/WelcomeScreen.d.ts +15 -0
  27. package/dist/client/components/WelcomeScreen.d.ts.map +1 -0
  28. package/dist/client/components/WelcomeScreen.js +7 -0
  29. package/dist/client/components/WelcomeScreen.js.map +1 -0
  30. package/{src/client/components/index.ts → dist/client/components/index.d.ts} +3 -0
  31. package/dist/client/components/index.d.ts.map +1 -0
  32. package/dist/client/components/index.js +7 -0
  33. package/dist/client/components/index.js.map +1 -0
  34. package/dist/client/index.d.ts +6 -0
  35. package/dist/client/index.d.ts.map +1 -0
  36. package/dist/client/index.js +3 -0
  37. package/dist/client/index.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/shared/signupConsent.d.ts +27 -0
  43. package/dist/shared/signupConsent.d.ts.map +1 -0
  44. package/dist/shared/signupConsent.js +29 -0
  45. package/dist/shared/signupConsent.js.map +1 -0
  46. package/package.json +11 -6
  47. package/.eslintrc.cjs +0 -18
  48. package/CHANGELOG.md +0 -11
  49. package/MODULE.md +0 -130
  50. package/src/client/components/ConsentBlock.tsx +0 -73
  51. package/src/client/components/ConsentCheckbox.tsx +0 -53
  52. package/src/client/components/ConsentUpdateModal.tsx +0 -65
  53. package/src/client/components/EmptyState.tsx +0 -38
  54. package/src/client/components/WelcomeScreen.tsx +0 -64
  55. package/src/client/index.ts +0 -19
  56. package/src/index.ts +0 -20
  57. package/src/server/index.ts +0 -143
  58. package/src/server/lib/getAuditTrail.ts +0 -20
  59. package/src/server/lib/getCurrentConsents.ts +0 -25
  60. package/src/server/lib/getUserLatestConsents.ts +0 -27
  61. package/src/server/lib/hasUserConsented.ts +0 -18
  62. package/src/server/lib/index.ts +0 -7
  63. package/src/server/lib/needsConsentUpdate.ts +0 -28
  64. package/src/server/lib/recordConsent.ts +0 -13
  65. package/src/server/lib/recordSignupConsents.ts +0 -32
  66. package/src/server/migrations/0001_create_oce_schema_migrations.sql +0 -7
  67. package/src/server/migrations/0002_create_oce_consent_definitions.sql +0 -12
  68. package/src/server/migrations/0003_create_oce_user_consents.sql +0 -14
  69. package/src/server/migrations/0004_create_oce_consent_version_log.sql +0 -12
  70. package/src/server/templates/applyProductName.ts +0 -10
  71. package/src/server/templates/index.ts +0 -3
  72. package/src/server/templates/standardConsents.ts +0 -37
  73. package/src/shared/types.ts +0 -85
  74. package/tests/integration/integration.test.ts +0 -162
  75. package/tests/setup/global-setup.ts +0 -16
  76. package/tests/unit/applyProductName.test.ts +0 -20
  77. package/tests/unit/getAuditTrail.test.ts +0 -33
  78. package/tests/unit/hasUserConsented.test.ts +0 -24
  79. package/tests/unit/needsConsentUpdate.test.ts +0 -24
  80. package/tests/unit/recordConsent.test.ts +0 -41
  81. package/tsconfig.client.json +0 -15
  82. package/tsconfig.json +0 -19
  83. package/vitest.config.ts +0 -9
@@ -1,18 +0,0 @@
1
- import type { Pool } from 'pg';
2
-
3
- export async function hasUserConsented(
4
- pool: Pool,
5
- userId: string,
6
- key: string,
7
- ): Promise<boolean> {
8
- const result = await pool.query<{ granted: boolean }>(
9
- `SELECT uc.granted
10
- FROM oce_user_consents uc
11
- JOIN oce_consent_definitions cd ON cd.id = uc.definition_id
12
- WHERE uc.user_id = $1 AND cd.key = $2
13
- ORDER BY uc.consented_at DESC
14
- LIMIT 1`,
15
- [userId, key],
16
- );
17
- return result.rows.length > 0 && result.rows[0].granted === true;
18
- }
@@ -1,7 +0,0 @@
1
- export { recordConsent } from './recordConsent.js';
2
- export { recordSignupConsents } from './recordSignupConsents.js';
3
- export { hasUserConsented } from './hasUserConsented.js';
4
- export { needsConsentUpdate } from './needsConsentUpdate.js';
5
- export { getCurrentConsents } from './getCurrentConsents.js';
6
- export { getAuditTrail } from './getAuditTrail.js';
7
- export { getUserLatestConsents } from './getUserLatestConsents.js';
@@ -1,28 +0,0 @@
1
- import type { Pool } from 'pg';
2
- import type { ConsentDefinition } from '../../shared/types.js';
3
-
4
- /**
5
- * Returns required consent definitions where the user has not yet granted
6
- * the current version. Used to gate the app and show ConsentUpdateModal.
7
- */
8
- export async function needsConsentUpdate(
9
- pool: Pool,
10
- userId: string,
11
- ): Promise<ConsentDefinition[]> {
12
- const result = await pool.query<ConsentDefinition>(
13
- `SELECT cd.*
14
- FROM oce_consent_definitions cd
15
- WHERE cd.required = true
16
- AND NOT EXISTS (
17
- SELECT 1
18
- FROM oce_user_consents uc
19
- WHERE uc.user_id = $1
20
- AND uc.definition_id = cd.id
21
- AND uc.version = cd.version
22
- AND uc.granted = true
23
- )
24
- ORDER BY cd.key`,
25
- [userId],
26
- );
27
- return result.rows;
28
- }
@@ -1,13 +0,0 @@
1
- import type { Pool } from 'pg';
2
- import type { RecordConsentInput, UserConsent } from '../../shared/types.js';
3
-
4
- export async function recordConsent(pool: Pool, input: RecordConsentInput): Promise<UserConsent> {
5
- const { userId, definitionId, version, granted, ipAddress, userAgent } = input;
6
- const result = await pool.query<UserConsent>(
7
- `INSERT INTO oce_user_consents (user_id, definition_id, version, granted, ip_address, user_agent)
8
- VALUES ($1, $2, $3, $4, $5, $6)
9
- RETURNING *`,
10
- [userId, definitionId, version, granted, ipAddress ?? null, userAgent ?? null],
11
- );
12
- return result.rows[0];
13
- }
@@ -1,32 +0,0 @@
1
- import type { Pool } from 'pg';
2
- import type { ConsentDefinition, RecordSignupConsentsInput, UserConsent } from '../../shared/types.js';
3
- import { recordConsent } from './recordConsent.js';
4
-
5
- export async function recordSignupConsents(
6
- pool: Pool,
7
- input: RecordSignupConsentsInput,
8
- ): Promise<UserConsent[]> {
9
- const results: UserConsent[] = [];
10
-
11
- for (const c of input.consents) {
12
- const defResult = await pool.query<ConsentDefinition>(
13
- 'SELECT * FROM oce_consent_definitions WHERE key = $1',
14
- [c.key],
15
- );
16
- if (defResult.rows.length === 0) {
17
- throw new Error(`Unknown consent key: ${c.key}`);
18
- }
19
- const def = defResult.rows[0];
20
- const consent = await recordConsent(pool, {
21
- userId: input.userId,
22
- definitionId: def.id,
23
- version: def.version,
24
- granted: c.granted,
25
- ipAddress: input.ipAddress,
26
- userAgent: input.userAgent,
27
- });
28
- results.push(consent);
29
- }
30
-
31
- return results;
32
- }
@@ -1,7 +0,0 @@
1
- -- Migration ledger for @varshylinc/onboarding-consent-engine
2
- CREATE TABLE IF NOT EXISTS oce_schema_migrations (
3
- id SERIAL PRIMARY KEY,
4
- migration VARCHAR(255) NOT NULL UNIQUE,
5
- applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6
- );
7
- CREATE INDEX IF NOT EXISTS idx_oce_schema_migrations_migration ON oce_schema_migrations (migration);
@@ -1,12 +0,0 @@
1
- -- Consent definitions: one row per consent type, versioned
2
- CREATE TABLE IF NOT EXISTS oce_consent_definitions (
3
- id SERIAL PRIMARY KEY,
4
- key VARCHAR(128) NOT NULL UNIQUE,
5
- version INTEGER NOT NULL DEFAULT 1,
6
- required BOOLEAN NOT NULL DEFAULT false,
7
- display_text TEXT NOT NULL,
8
- legal_url TEXT,
9
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
11
- );
12
- CREATE INDEX IF NOT EXISTS idx_oce_consent_definitions_key ON oce_consent_definitions (key);
@@ -1,14 +0,0 @@
1
- -- Immutable append-only consent records per user
2
- -- user_id is TEXT — no FK, format determined by consuming product
3
- CREATE TABLE IF NOT EXISTS oce_user_consents (
4
- id SERIAL PRIMARY KEY,
5
- user_id TEXT NOT NULL,
6
- definition_id INTEGER NOT NULL REFERENCES oce_consent_definitions(id),
7
- version INTEGER NOT NULL,
8
- granted BOOLEAN NOT NULL,
9
- ip_address TEXT,
10
- user_agent TEXT,
11
- consented_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
12
- );
13
- CREATE INDEX IF NOT EXISTS idx_oce_user_consents_user_id ON oce_user_consents (user_id);
14
- CREATE INDEX IF NOT EXISTS idx_oce_user_consents_definition_id ON oce_user_consents (definition_id);
@@ -1,12 +0,0 @@
1
- -- Audit log of definition text changes (tracks consent version bumps)
2
- CREATE TABLE IF NOT EXISTS oce_consent_version_log (
3
- id SERIAL PRIMARY KEY,
4
- definition_id INTEGER NOT NULL REFERENCES oce_consent_definitions(id),
5
- old_version INTEGER NOT NULL,
6
- new_version INTEGER NOT NULL,
7
- old_text TEXT NOT NULL,
8
- new_text TEXT NOT NULL,
9
- changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10
- changed_by TEXT
11
- );
12
- CREATE INDEX IF NOT EXISTS idx_oce_consent_version_log_definition_id ON oce_consent_version_log (definition_id);
@@ -1,10 +0,0 @@
1
- /**
2
- * Substitute {{PRODUCT_NAME}} in a consent display_text template.
3
- *
4
- * IMPORTANT: call this at SEED TIME only (inside seedStandardConsents).
5
- * Components must display display_text from the DB verbatim and must never
6
- * interpolate productName at render time.
7
- */
8
- export function applyProductName(template: string, productName: string): string {
9
- return template.replace(/\{\{PRODUCT_NAME\}\}/g, productName);
10
- }
@@ -1,3 +0,0 @@
1
- export { STANDARD_CONSENTS } from './standardConsents.js';
2
- export type { StandardConsentKey } from './standardConsents.js';
3
- export { applyProductName } from './applyProductName.js';
@@ -1,37 +0,0 @@
1
- /**
2
- * Canonical consent set for all Varshyl products.
3
- *
4
- * display_text contains the placeholder {{PRODUCT_NAME}}.
5
- * Call applyProductName() at SEED TIME only — components must display
6
- * display_text from the DB verbatim and never interpolate at render time.
7
- */
8
- export const STANDARD_CONSENTS = [
9
- {
10
- key: 'terms_of_service',
11
- required: true,
12
- display_text: 'I agree to the {{PRODUCT_NAME}} Terms of Service.',
13
- legal_url: null,
14
- },
15
- {
16
- key: 'privacy_policy',
17
- required: true,
18
- display_text: 'I agree to the {{PRODUCT_NAME}} Privacy Policy.',
19
- legal_url: null,
20
- },
21
- {
22
- key: 'marketing_emails',
23
- required: false,
24
- display_text:
25
- 'I would like to receive product updates and marketing emails from {{PRODUCT_NAME}}.',
26
- legal_url: null,
27
- },
28
- {
29
- key: 'ai_training',
30
- required: false,
31
- display_text:
32
- 'I consent to {{PRODUCT_NAME}} using anonymized data from my sessions to improve AI models.',
33
- legal_url: null,
34
- },
35
- ] as const;
36
-
37
- export type StandardConsentKey = (typeof STANDARD_CONSENTS)[number]['key'];
@@ -1,85 +0,0 @@
1
- export interface ConsentDefinition {
2
- id: number;
3
- key: string;
4
- version: number;
5
- required: boolean;
6
- display_text: string;
7
- legal_url: string | null;
8
- created_at: Date;
9
- updated_at: Date;
10
- }
11
-
12
- export interface UserConsent {
13
- id: number;
14
- user_id: string;
15
- definition_id: number;
16
- version: number;
17
- granted: boolean;
18
- ip_address: string | null;
19
- user_agent: string | null;
20
- consented_at: Date;
21
- }
22
-
23
- export interface ConsentVersionLog {
24
- id: number;
25
- definition_id: number;
26
- old_version: number;
27
- new_version: number;
28
- old_text: string;
29
- new_text: string;
30
- changed_at: Date;
31
- changed_by: string | null;
32
- }
33
-
34
- export interface RecordConsentInput {
35
- userId: string;
36
- definitionId: number;
37
- version: number;
38
- granted: boolean;
39
- ipAddress?: string;
40
- userAgent?: string;
41
- }
42
-
43
- export interface RecordSignupConsentsInput {
44
- userId: string;
45
- consents: Array<{ key: string; granted: boolean }>;
46
- ipAddress?: string;
47
- userAgent?: string;
48
- }
49
-
50
- export interface ConsentStatus {
51
- key: string;
52
- version: number;
53
- granted: boolean;
54
- consented_at: Date;
55
- }
56
-
57
- export interface AuditEntry {
58
- id: number;
59
- user_id: string;
60
- key: string;
61
- version: number;
62
- granted: boolean;
63
- ip_address: string | null;
64
- user_agent: string | null;
65
- consented_at: Date;
66
- }
67
-
68
- export interface ConsentModuleAdapter {
69
- onConsentRecorded?: (userId: string, key: string, granted: boolean) => void | Promise<void>;
70
- }
71
-
72
- export interface ConsentModuleConfig {
73
- pool: import('pg').Pool;
74
- adapter?: ConsentModuleAdapter;
75
- }
76
-
77
- export interface ConsentModule {
78
- recordConsent(input: RecordConsentInput): Promise<UserConsent>;
79
- recordSignupConsents(input: RecordSignupConsentsInput): Promise<UserConsent[]>;
80
- hasUserConsented(userId: string, key: string): Promise<boolean>;
81
- needsConsentUpdate(userId: string): Promise<ConsentDefinition[]>;
82
- getCurrentConsents(userId: string): Promise<ConsentStatus[]>;
83
- getAuditTrail(userId: string, limit?: number): Promise<AuditEntry[]>;
84
- getUserLatestConsents(userIds: string[]): Promise<Map<string, ConsentStatus[]>>;
85
- }
@@ -1,162 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
- import { Pool } from 'pg';
3
- import {
4
- runMigrations,
5
- seedStandardConsents,
6
- createConsentModule,
7
- } from '../../src/server/index.js';
8
-
9
- const describeWithDb = process.env.DATABASE_URL ? describe : describe.skip;
10
-
11
- let pool: Pool;
12
- let consent: ReturnType<typeof createConsentModule>;
13
-
14
- describeWithDb('@varshylinc/onboarding-consent-engine — integration', () => {
15
- beforeAll(async () => {
16
- pool = new Pool({ connectionString: process.env.DATABASE_URL });
17
- // Migrations already applied by global-setup; seed again is idempotent
18
- await seedStandardConsents(pool, 'TestProduct');
19
- consent = createConsentModule({ pool });
20
- });
21
-
22
- afterAll(async () => {
23
- await pool.end();
24
- });
25
-
26
- it('runMigrations is idempotent — second call skips all', async () => {
27
- const { applied, skipped } = await runMigrations(pool);
28
- expect(applied).toHaveLength(0);
29
- expect(skipped).toHaveLength(4);
30
- });
31
-
32
- it('oce_consent_definitions has 4 standard rows after seed', async () => {
33
- const result = await pool.query('SELECT COUNT(*) FROM oce_consent_definitions');
34
- expect(Number(result.rows[0].count)).toBeGreaterThanOrEqual(4);
35
- });
36
-
37
- it('recordSignupConsents inserts records for all provided keys', async () => {
38
- const userId = `test-user-${Date.now()}`;
39
- const records = await consent.recordSignupConsents({
40
- userId,
41
- consents: [
42
- { key: 'terms_of_service', granted: true },
43
- { key: 'privacy_policy', granted: true },
44
- { key: 'marketing_emails', granted: false },
45
- ],
46
- ipAddress: '127.0.0.1',
47
- userAgent: 'vitest',
48
- });
49
- expect(records).toHaveLength(3);
50
- expect(records.every((r) => r.user_id === userId)).toBe(true);
51
- });
52
-
53
- it('hasUserConsented returns true for granted key', async () => {
54
- const userId = `huc-${Date.now()}`;
55
- await consent.recordSignupConsents({
56
- userId,
57
- consents: [{ key: 'privacy_policy', granted: true }],
58
- });
59
- expect(await consent.hasUserConsented(userId, 'privacy_policy')).toBe(true);
60
- });
61
-
62
- it('hasUserConsented returns false for un-consented key', async () => {
63
- const userId = `huc2-${Date.now()}`;
64
- expect(await consent.hasUserConsented(userId, 'terms_of_service')).toBe(false);
65
- });
66
-
67
- it('needsConsentUpdate returns required definitions for new user', async () => {
68
- const userId = `ncu-${Date.now()}`;
69
- const pending = await consent.needsConsentUpdate(userId);
70
- // terms_of_service and privacy_policy are required
71
- expect(pending.length).toBeGreaterThanOrEqual(2);
72
- expect(pending.every((d) => d.required)).toBe(true);
73
- });
74
-
75
- it('needsConsentUpdate returns empty after user consents to all required', async () => {
76
- const userId = `ncu2-${Date.now()}`;
77
- await consent.recordSignupConsents({
78
- userId,
79
- consents: [
80
- { key: 'terms_of_service', granted: true },
81
- { key: 'privacy_policy', granted: true },
82
- ],
83
- });
84
- const pending = await consent.needsConsentUpdate(userId);
85
- expect(pending).toHaveLength(0);
86
- });
87
-
88
- it('getAuditTrail returns all events newest-first', async () => {
89
- const userId = `audit-${Date.now()}`;
90
- await consent.recordSignupConsents({
91
- userId,
92
- consents: [
93
- { key: 'terms_of_service', granted: true },
94
- { key: 'marketing_emails', granted: false },
95
- ],
96
- });
97
- const trail = await consent.getAuditTrail(userId);
98
- expect(trail.length).toBeGreaterThanOrEqual(2);
99
- expect(trail[0].user_id).toBe(userId);
100
- });
101
-
102
- it('getCurrentConsents returns latest state per key', async () => {
103
- const userId = `cc-${Date.now()}`;
104
- await consent.recordSignupConsents({
105
- userId,
106
- consents: [
107
- { key: 'terms_of_service', granted: true },
108
- { key: 'privacy_policy', granted: true },
109
- ],
110
- });
111
- const statuses = await consent.getCurrentConsents(userId);
112
- expect(statuses.length).toBeGreaterThanOrEqual(2);
113
- expect(statuses.every((s) => s.granted === true)).toBe(true);
114
- });
115
-
116
- it('getUserLatestConsents returns a map keyed by user_id', async () => {
117
- const uid1 = `bulk-a-${Date.now()}`;
118
- const uid2 = `bulk-b-${Date.now()}`;
119
- await Promise.all([
120
- consent.recordSignupConsents({
121
- userId: uid1,
122
- consents: [{ key: 'terms_of_service', granted: true }],
123
- }),
124
- consent.recordSignupConsents({
125
- userId: uid2,
126
- consents: [{ key: 'privacy_policy', granted: true }],
127
- }),
128
- ]);
129
- const map = await consent.getUserLatestConsents([uid1, uid2]);
130
- expect(map.has(uid1)).toBe(true);
131
- expect(map.has(uid2)).toBe(true);
132
- expect(map.get(uid1)!.some((s) => s.key === 'terms_of_service')).toBe(true);
133
- });
134
-
135
- it('recordSignupConsents throws for unknown consent key', async () => {
136
- await expect(
137
- consent.recordSignupConsents({
138
- userId: 'err-user',
139
- consents: [{ key: 'nonexistent_key', granted: true }],
140
- }),
141
- ).rejects.toThrow('Unknown consent key: nonexistent_key');
142
- });
143
-
144
- it('onConsentRecorded adapter hook is called', async () => {
145
- const calls: Array<{ userId: string; key: string; granted: boolean }> = [];
146
- const tracked = createConsentModule({
147
- pool,
148
- adapter: {
149
- onConsentRecorded(userId, key, granted) {
150
- calls.push({ userId, key, granted });
151
- },
152
- },
153
- });
154
- const userId = `hook-${Date.now()}`;
155
- await tracked.recordSignupConsents({
156
- userId,
157
- consents: [{ key: 'terms_of_service', granted: true }],
158
- });
159
- expect(calls).toHaveLength(1);
160
- expect(calls[0]).toMatchObject({ userId, key: 'terms_of_service', granted: true });
161
- });
162
- });
@@ -1,16 +0,0 @@
1
- import { Pool } from 'pg';
2
-
3
- export async function setup(): Promise<void> {
4
- const databaseUrl = process.env.DATABASE_URL;
5
- if (!databaseUrl) return;
6
- const { runMigrations, seedStandardConsents } = await import(
7
- '../../src/server/index.js'
8
- );
9
- const pool = new Pool({ connectionString: databaseUrl });
10
- try {
11
- await runMigrations(pool);
12
- await seedStandardConsents(pool, 'TestProduct');
13
- } finally {
14
- await pool.end();
15
- }
16
- }
@@ -1,20 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { applyProductName } from '../../src/server/templates/applyProductName.js';
3
-
4
- describe('applyProductName', () => {
5
- it('replaces {{PRODUCT_NAME}} placeholder', () => {
6
- expect(applyProductName('I agree to the {{PRODUCT_NAME}} Terms.', 'ConstructInv')).toBe(
7
- 'I agree to the ConstructInv Terms.',
8
- );
9
- });
10
-
11
- it('replaces multiple occurrences', () => {
12
- expect(
13
- applyProductName('{{PRODUCT_NAME}} is cool. Use {{PRODUCT_NAME}}.', 'DailyLog'),
14
- ).toBe('DailyLog is cool. Use DailyLog.');
15
- });
16
-
17
- it('leaves strings without placeholder unchanged', () => {
18
- expect(applyProductName('No placeholder here.', 'X')).toBe('No placeholder here.');
19
- });
20
- });
@@ -1,33 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { getAuditTrail } from '../../src/server/lib/getAuditTrail.js';
3
- import type { Pool } from 'pg';
4
-
5
- function makePool(rows: object[]): Pool {
6
- return { query: vi.fn().mockResolvedValue({ rows }) } as unknown as Pool;
7
- }
8
-
9
- describe('getAuditTrail', () => {
10
- it('returns audit entries for a user', async () => {
11
- const entries = [
12
- { id: 1, user_id: 'u1', key: 'terms_of_service', version: 1, granted: true },
13
- { id: 2, user_id: 'u1', key: 'privacy_policy', version: 1, granted: true },
14
- ];
15
- const pool = makePool(entries);
16
- const result = await getAuditTrail(pool, 'u1');
17
- expect(result).toEqual(entries);
18
- });
19
-
20
- it('passes limit=50 by default', async () => {
21
- const pool = makePool([]);
22
- await getAuditTrail(pool, 'u1');
23
- const [, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0];
24
- expect(params[1]).toBe(50);
25
- });
26
-
27
- it('passes custom limit', async () => {
28
- const pool = makePool([]);
29
- await getAuditTrail(pool, 'u1', 10);
30
- const [, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0];
31
- expect(params[1]).toBe(10);
32
- });
33
- });
@@ -1,24 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { hasUserConsented } from '../../src/server/lib/hasUserConsented.js';
3
- import type { Pool } from 'pg';
4
-
5
- function makePool(rows: object[]): Pool {
6
- return { query: vi.fn().mockResolvedValue({ rows }) } as unknown as Pool;
7
- }
8
-
9
- describe('hasUserConsented', () => {
10
- it('returns true when latest record is granted=true', async () => {
11
- const pool = makePool([{ granted: true }]);
12
- expect(await hasUserConsented(pool, 'u1', 'terms_of_service')).toBe(true);
13
- });
14
-
15
- it('returns false when latest record is granted=false', async () => {
16
- const pool = makePool([{ granted: false }]);
17
- expect(await hasUserConsented(pool, 'u1', 'terms_of_service')).toBe(false);
18
- });
19
-
20
- it('returns false when no record exists', async () => {
21
- const pool = makePool([]);
22
- expect(await hasUserConsented(pool, 'u1', 'terms_of_service')).toBe(false);
23
- });
24
- });
@@ -1,24 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { needsConsentUpdate } from '../../src/server/lib/needsConsentUpdate.js';
3
- import type { Pool } from 'pg';
4
-
5
- function makePool(rows: object[]): Pool {
6
- return { query: vi.fn().mockResolvedValue({ rows }) } as unknown as Pool;
7
- }
8
-
9
- describe('needsConsentUpdate', () => {
10
- it('returns definitions that still need consent', async () => {
11
- const missing = [
12
- { id: 1, key: 'terms_of_service', version: 2, required: true, display_text: 'ToS' },
13
- ];
14
- const pool = makePool(missing);
15
- const result = await needsConsentUpdate(pool, 'u1');
16
- expect(result).toEqual(missing);
17
- });
18
-
19
- it('returns empty array when user has all current consents', async () => {
20
- const pool = makePool([]);
21
- const result = await needsConsentUpdate(pool, 'u1');
22
- expect(result).toHaveLength(0);
23
- });
24
- });
@@ -1,41 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { recordConsent } from '../../src/server/lib/recordConsent.js';
3
- import type { Pool } from 'pg';
4
-
5
- function makePool(rows: object[]): Pool {
6
- return { query: vi.fn().mockResolvedValue({ rows }) } as unknown as Pool;
7
- }
8
-
9
- describe('recordConsent', () => {
10
- it('inserts a consent record and returns it', async () => {
11
- const expected = {
12
- id: 1,
13
- user_id: '42',
14
- definition_id: 3,
15
- version: 1,
16
- granted: true,
17
- ip_address: '127.0.0.1',
18
- user_agent: 'vitest',
19
- consented_at: new Date(),
20
- };
21
- const pool = makePool([expected]);
22
- const result = await recordConsent(pool, {
23
- userId: '42',
24
- definitionId: 3,
25
- version: 1,
26
- granted: true,
27
- ipAddress: '127.0.0.1',
28
- userAgent: 'vitest',
29
- });
30
- expect(result).toEqual(expected);
31
- expect(pool.query).toHaveBeenCalledOnce();
32
- });
33
-
34
- it('passes null for missing ipAddress and userAgent', async () => {
35
- const pool = makePool([{ id: 1 }]);
36
- await recordConsent(pool, { userId: 'u1', definitionId: 1, version: 1, granted: false });
37
- const [, params] = (pool.query as ReturnType<typeof vi.fn>).mock.calls[0];
38
- expect(params[4]).toBeNull();
39
- expect(params[5]).toBeNull();
40
- });
41
- });
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
- "jsx": "react-jsx",
8
- "strict": true,
9
- "noEmit": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true
12
- },
13
- "include": ["src/client/**/*", "src/shared/**/*"],
14
- "exclude": ["node_modules", "dist"]
15
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "lib": ["ES2022"],
7
- "outDir": "./dist",
8
- "rootDir": "./src",
9
- "declaration": true,
10
- "declarationMap": true,
11
- "sourceMap": true,
12
- "strict": true,
13
- "esModuleInterop": true,
14
- "skipLibCheck": true,
15
- "resolveJsonModule": true
16
- },
17
- "include": ["src/index.ts", "src/server/**/*", "src/shared/**/*"],
18
- "exclude": ["node_modules", "dist", "src/client/**/*"]
19
- }
package/vitest.config.ts DELETED
@@ -1,9 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: 'node',
6
- globalSetup: 'tests/setup/global-setup.ts',
7
- include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
8
- },
9
- });