@kernhq/module-hr 0.16.0 → 0.17.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 (49) hide show
  1. package/dist/contract/index.d.ts +1 -0
  2. package/dist/contract/index.d.ts.map +1 -1
  3. package/dist/contract/index.js +1 -0
  4. package/dist/contract/index.js.map +1 -1
  5. package/dist/contract/models.d.ts +1 -0
  6. package/dist/contract/models.d.ts.map +1 -1
  7. package/dist/contract/models.js +15 -0
  8. package/dist/contract/models.js.map +1 -1
  9. package/dist/contract/permissions.d.ts +8 -0
  10. package/dist/contract/permissions.d.ts.map +1 -1
  11. package/dist/contract/permissions.js +20 -0
  12. package/dist/contract/permissions.js.map +1 -1
  13. package/dist/contract/privacy.d.ts +860 -0
  14. package/dist/contract/privacy.d.ts.map +1 -0
  15. package/dist/contract/privacy.js +412 -0
  16. package/dist/contract/privacy.js.map +1 -0
  17. package/dist/contract/router.d.ts +815 -0
  18. package/dist/contract/router.d.ts.map +1 -1
  19. package/dist/contract/router.js +105 -0
  20. package/dist/contract/router.js.map +1 -1
  21. package/dist/server/router.d.ts +876 -0
  22. package/dist/server/router.d.ts.map +1 -1
  23. package/dist/server/router.js +312 -2
  24. package/dist/server/router.js.map +1 -1
  25. package/dist/server/schema.d.ts +397 -1
  26. package/dist/server/schema.d.ts.map +1 -1
  27. package/dist/server/schema.js +109 -0
  28. package/dist/server/schema.js.map +1 -1
  29. package/dist/server/services/audit.d.ts +249 -0
  30. package/dist/server/services/audit.d.ts.map +1 -0
  31. package/dist/server/services/audit.js +230 -0
  32. package/dist/server/services/audit.js.map +1 -0
  33. package/dist/server/services/people.d.ts +53 -1
  34. package/dist/server/services/people.d.ts.map +1 -1
  35. package/dist/server/services/people.js +74 -1
  36. package/dist/server/services/people.js.map +1 -1
  37. package/dist/server/services/privacy.d.ts +514 -0
  38. package/dist/server/services/privacy.d.ts.map +1 -0
  39. package/dist/server/services/privacy.js +972 -0
  40. package/dist/server/services/privacy.js.map +1 -0
  41. package/migrations/0011_privacy.sql +77 -0
  42. package/migrations/meta/0011_snapshot.json +4450 -0
  43. package/migrations/meta/_journal.json +7 -0
  44. package/package.json +1 -1
  45. package/src/contract/index.ts +1 -0
  46. package/src/contract/models.ts +15 -0
  47. package/src/contract/permissions.ts +22 -0
  48. package/src/contract/privacy.ts +452 -0
  49. package/src/contract/router.ts +121 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privacy.d.ts","sourceRoot":"","sources":["../../src/contract/privacy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,412 @@
1
+ import { Timestamp, WorkspaceId } from '@kernhq/contracts';
2
+ import { z } from 'zod';
3
+ import { ApprovalDecision, ApprovalRequest, ApprovalStep, Delegation } from './approvals.js';
4
+ import { AttendanceDay, Punch, Regularization } from './attendance.js';
5
+ import { LeaveLedgerEntry, LeaveRequest, LeaveType } from './leave.js';
6
+ import { Employment, IsoDate, OfficeAssignment, Person, PersonDocument, PersonSensitive } from './models.js';
7
+ import { ResolvedPolicy } from './policies.js';
8
+ /**
9
+ * Subject access, erasure and retention — the three things a data-protection law asks a personnel
10
+ * system for, and the one part of HR whose whole job is to say what it did.
11
+ *
12
+ * Three positions hold this file together, and each of them is a decision the rest of the module
13
+ * already made somewhere else:
14
+ *
15
+ * - **Erasure is redaction, never deletion.** `leave_ledger` is append-only because a balance is the
16
+ * sum of it and nothing else; `punches` is append-only because an attendance record somebody can
17
+ * quietly rewrite is worth nothing in the dispute it exists for; `employments` is effective-dated
18
+ * so March is still answerable in June. Deleting an erased person's rows would break all three and
19
+ * would answer a payroll audit with "she left, so we deleted her file". So erasure clears the
20
+ * columns that identify a person and leaves every record that a wage, an entitlement or an
21
+ * authorisation was computed from. Afterwards the ledger still has somebody to attribute a day
22
+ * to, and nobody has a name.
23
+ * - **It says what it kept, per class, and why.** The `kept` half of the report is not a courtesy:
24
+ * an erasure that silently retains is the same failure as an export that silently omits, and the
25
+ * only difference is which of the two the subject finds out about later.
26
+ * - **Nothing here is a capability.** A workspace that could switch privacy off would be a workspace
27
+ * that stopped honouring subject requests, which fails the capability registry's own second rule —
28
+ * a switch must be reversible without destroying data, and this one would not even be a switch
29
+ * anybody should be offered. One permission key, `hr.privacy.manage`, gates all four procedures,
30
+ * and it ships in the same change as them.
31
+ */
32
+ const ws = { workspaceId: WorkspaceId };
33
+ // =====================================================================================
34
+ // retention
35
+ // =====================================================================================
36
+ /**
37
+ * A horizon in days, or null.
38
+ *
39
+ * **Null is the shipped value for every class and it means "keep indefinitely".** "Seven years" is a
40
+ * fact about one country and one document class, not about the world, so no number here is a
41
+ * default: the suggested figures live in the help text on the settings screen, beside the sentence
42
+ * saying Kern gives no legal advice. The workspace owner sets them, and where their legal entities
43
+ * differ they set them per entity — which this shape cannot yet express, and says so below.
44
+ */
45
+ const RetentionDays = z.number().int().min(1).max(36_500).nullable().default(null);
46
+ /**
47
+ * The classes retention is expressed in.
48
+ *
49
+ * They follow the redact/retain split rather than the table list, because that is the split a
50
+ * retention decision is actually made in: "how long do we keep the evidence behind a day sheet" is
51
+ * one question whatever tables it lands in.
52
+ */
53
+ export const RetentionClass = z.enum([
54
+ /** Where a punch happened, on what device, and what was typed on it. */
55
+ 'punchDetail',
56
+ /** The punch rows themselves — the evidence behind a day sheet. */
57
+ 'punches',
58
+ /** The derived day sheet: hours, overtime, lateness. This is pay. */
59
+ 'attendanceDays',
60
+ /** Ledger movements and the requests behind them. Also pay. */
61
+ 'leave',
62
+ /** The audit trail. At the horizon the values go and the rows stay — see `privacy.erase`. */
63
+ 'personHistory',
64
+ /** Contracts, payslips, identity scans. */
65
+ 'personDocuments',
66
+ /** People who have left, and have not been erased. */
67
+ 'terminatedPeople',
68
+ /** Who read somebody's identity, birth date or bank details. Itself personal data. */
69
+ 'sensitiveAccessLog',
70
+ ]);
71
+ /**
72
+ * How long HR keeps each class, per workspace.
73
+ *
74
+ * A table rather than module settings, and the reason is mechanical: `core.settings.setModule`
75
+ * validates the merged blob against the module's declared zod schema and a zod object strips what it
76
+ * does not declare — so a retention key stored beside `country` and `employeeNumberPrefix` would be
77
+ * deleted by the next unrelated settings write, silently, from a screen that never mentions
78
+ * retention. Extending `HrSettings` instead is the other honest route and is a change to a shared
79
+ * shape; this one keeps the numbers where nothing can quietly drop them.
80
+ *
81
+ * Workspace-wide for now. The honest end state is per legal entity — a Dutch entity and a Turkish
82
+ * one have different obligations, which is exactly why `periods` is already per legal entity — and
83
+ * the row carries a jsonb config so that move is an added column rather than a new table.
84
+ */
85
+ export const HrRetention = z.object({
86
+ punchDetail: RetentionDays,
87
+ punches: RetentionDays,
88
+ attendanceDays: RetentionDays,
89
+ leave: RetentionDays,
90
+ personHistory: RetentionDays,
91
+ personDocuments: RetentionDays,
92
+ terminatedPeople: RetentionDays,
93
+ sensitiveAccessLog: RetentionDays,
94
+ });
95
+ /** One class, its horizon, and what has already passed it. */
96
+ export const RetentionClassState = z.object({
97
+ class: RetentionClass,
98
+ /** Null means "kept indefinitely", which is what every class ships as. */
99
+ days: z.number().int().nullable(),
100
+ /**
101
+ * How many rows are already older than the horizon — the dry run, asked for with `withCounts`.
102
+ *
103
+ * Null when no horizon is set (nothing to be past) or when the caller did not ask for counts. A
104
+ * retention screen that cannot say what a sweep would touch before it runs is a data-loss button
105
+ * with no confirmation, so this is the number that belongs beside every horizon on it.
106
+ */
107
+ dueNow: z.number().int().nullable(),
108
+ });
109
+ export const RetentionSettings = z.object({
110
+ ...ws,
111
+ classes: z.array(RetentionClassState),
112
+ updatedAt: Timestamp.nullable(),
113
+ updatedBy: z.uuid().nullable(),
114
+ /**
115
+ * Whether anything in HR deletes on these horizons yet. **It is false, and saying so is the
116
+ * point.**
117
+ *
118
+ * The horizons are read in two places today: here, to count what has passed one, and by
119
+ * `privacy.erase`, to say under which horizon each surviving class was kept. No timed sweep acts
120
+ * on them. An unattended job that prunes personnel records is the one act in this module that
121
+ * cannot be undone by re-running anything, so it ships off, with a dry run and a per-run report
122
+ * naming every person it touched — and until it exists this field must not claim otherwise.
123
+ */
124
+ sweepEnabled: z.literal(false),
125
+ });
126
+ // =====================================================================================
127
+ // erasure
128
+ // =====================================================================================
129
+ /** The units erasure reports in. One per group of columns that survive or go together. */
130
+ export const ErasureClass = z.enum([
131
+ 'identity',
132
+ 'sensitive',
133
+ 'history',
134
+ 'documents',
135
+ 'punches',
136
+ 'attendance',
137
+ 'leaveRequests',
138
+ 'leaveLedger',
139
+ 'employment',
140
+ 'officeAssignments',
141
+ 'approvals',
142
+ 'approvalDecisions',
143
+ 'regularizations',
144
+ 'delegations',
145
+ 'headship',
146
+ ]);
147
+ /** A class the erasure cleared, and exactly which columns it cleared. */
148
+ export const ErasureRedaction = z.object({
149
+ class: ErasureClass,
150
+ table: z.string(),
151
+ /** Rows that still had something to clear. Zero on a replay, which is what makes it replayable. */
152
+ rows: z.number().int(),
153
+ columns: z.array(z.string()),
154
+ });
155
+ /**
156
+ * Why a record survived an erasure.
157
+ *
158
+ * An enum rather than a sentence: a reason composed on the server is English for every reader, and
159
+ * this one is read by the person who asked to be forgotten. The client renders it.
160
+ */
161
+ export const RetentionBasis = z.enum([
162
+ /** A wage, an entitlement or the hours behind one. Statutory wherever Kern ships a country pack. */
163
+ 'payRecord',
164
+ /** What changed, when, and who authorised it. Erasing the trail and erasing the data differ. */
165
+ 'auditTrail',
166
+ /** The workspace set a horizon for this class and it has not passed. */
167
+ 'retentionHorizon',
168
+ /** HR cannot remove it: the bytes belong to another module. See `caveats`. */
169
+ 'notRemovable',
170
+ /** It is somebody else's record. Erasing A is not authority to rewrite what A did to B's row. */
171
+ 'anotherPersonsRecord',
172
+ ]);
173
+ export const ErasureRetained = z.object({
174
+ class: ErasureClass,
175
+ table: z.string(),
176
+ rows: z.number().int(),
177
+ basis: RetentionBasis,
178
+ /** The horizon in force for the retention class covering this, when the workspace set one. */
179
+ retentionDays: z.number().int().nullable(),
180
+ });
181
+ /**
182
+ * Things the response has to state in words, as keys the client renders.
183
+ *
184
+ * Each one is a promise this module would otherwise be making and not keeping.
185
+ */
186
+ export const ErasureCaveat = z.enum([
187
+ /**
188
+ * `person_documents` rows and their files both survive. `core.files.get` is the only file
189
+ * procedure a module can reach — there is no `files.delete` — so an erasure claiming to remove
190
+ * employee documents would null a row and leave the passport scan in the bucket. The rows stay
191
+ * because they are also the only remaining record of which files those are.
192
+ */
193
+ 'documentFilesRemain',
194
+ /** The profile photo pointer was cleared; the object behind it is in `filesRemaining`. */
195
+ 'photoFileOrphaned',
196
+ /** Sick notes attached to leave requests, same reason and same list. */
197
+ 'leaveDocumentFilesRemain',
198
+ /** The national identity number was kept, because the caller asked for it to be. */
199
+ 'nationalIdKeptForAudit',
200
+ /**
201
+ * History rows where this person was the **actor** on somebody else's record keep their values.
202
+ * Erasing A is not authority to rewrite the trail of what A did to B.
203
+ */
204
+ 'actorHistoryKept',
205
+ /** At least one attendance day or punch sits in a locked period and was left exactly as it is. */
206
+ 'lockedPeriodUntouched',
207
+ ]);
208
+ /**
209
+ * What an erasure did, or would do.
210
+ *
211
+ * The same shape either way, because a preview computed differently from the thing it previews is a
212
+ * preview that eventually lies — the rule `accrual.preview` is written under.
213
+ */
214
+ export const ErasureReport = z.object({
215
+ ...ws,
216
+ personId: z.uuid(),
217
+ /** True when nothing was written. See `privacy.erase` for why this is what the default gives you. */
218
+ dryRun: z.boolean(),
219
+ /**
220
+ * When this person was erased. Null on a dry run of a person who has not been.
221
+ *
222
+ * On a replay it carries the **first** erasure's timestamp: the second run finds nothing left to
223
+ * clear, reports zero rows everywhere, and does not restamp the tombstone.
224
+ */
225
+ erasedAt: Timestamp.nullable(),
226
+ /** The pseudonym the directory shows now. Never a name, never empty — `displayName` is not null. */
227
+ displayName: z.string(),
228
+ redacted: z.array(ErasureRedaction),
229
+ kept: z.array(ErasureRetained),
230
+ caveats: z.array(ErasureCaveat),
231
+ /**
232
+ * File objects in core's storage that this erasure could not delete, and which are recorded on the
233
+ * person row so a later release can finish the job rather than losing them.
234
+ */
235
+ filesRemaining: z.array(z.uuid()),
236
+ });
237
+ // =====================================================================================
238
+ // the sensitive read log
239
+ // =====================================================================================
240
+ /**
241
+ * How a sensitive record was reached.
242
+ *
243
+ * Derived from the principal, never taken from the caller — a client-supplied value is one the
244
+ * reader controls, and this log exists to say what happened rather than what the reader claimed.
245
+ * `export` is the exception and is not reachable from a request: it is passed by the one server-side
246
+ * path that takes a bulk copy of a record, which is `privacy.subjectAccess`.
247
+ *
248
+ * There is no `mcp` and no `service`, and both omissions are the same rule rather than an oversight.
249
+ * Core's MCP proxy forwards a plain bearer token, so an assistant's read arrives as an ordinary
250
+ * `kind: 'user'` principal that nothing here can honestly tell from a browser; and a service
251
+ * principal carries no user id at all, so the read is refused before a row is built. A value nothing
252
+ * can ever write is the same lie as a permission key nothing asks about. See `services/audit.ts`.
253
+ */
254
+ export const SensitiveAccessVia = z.enum(['ui', 'api', 'export']);
255
+ /**
256
+ * One read of somebody's identity, birth date or bank details.
257
+ *
258
+ * Written in the same transaction as the read, in HR's own schema. `mod_core.activity_events` also
259
+ * gets a best-effort copy so the workspace's single audit console shows that HR sensitive data was
260
+ * touched — but it cannot be the record, for two reasons: it is read behind `core.audit.view`,
261
+ * which owners and admins hold by default and `hr.person.view_sensitive` deliberately nobody does,
262
+ * so publishing the read there inverts the model the sensitive split exists to create; and the
263
+ * cross-service call is allowed to fail, which is right for a notification and wrong for evidence.
264
+ * A subject-access response that answers "who looked at my data" out of a log with silent holes is
265
+ * worse than one that says it cannot answer.
266
+ */
267
+ export const SensitiveAccess = z.object({
268
+ id: z.uuid(),
269
+ /** Whose record was read. */
270
+ personId: z.uuid(),
271
+ /**
272
+ * The account the disclosure is recorded against. Never null: a reader nobody can be named for is
273
+ * refused before the record is loaded, because "somebody read your bank details" is not an answer
274
+ * to give a subject.
275
+ */
276
+ actorUserId: z.uuid(),
277
+ /** The reader's own HR record, when they have one. Plenty of accounts are not employees. */
278
+ actorPersonId: z.uuid().nullable(),
279
+ /**
280
+ * Only the fields that actually came back with a value.
281
+ *
282
+ * Logging "read the record" when three of the four were empty overstates what happened, and this
283
+ * log is itself read by the subject.
284
+ */
285
+ fields: z.array(z.string()),
286
+ /** Why, when the caller said. Shown to the subject in their own bundle — nowhere else. */
287
+ purpose: z.string().max(500).nullable(),
288
+ via: SensitiveAccessVia,
289
+ at: Timestamp,
290
+ });
291
+ // =====================================================================================
292
+ // the subject-access bundle
293
+ // =====================================================================================
294
+ /** `person_history`, with the values. Redacting it in a subject's own bundle defeats the purpose. */
295
+ export const PersonHistoryEntry = z.object({
296
+ id: z.uuid(),
297
+ field: z.string(),
298
+ from: z.unknown(),
299
+ to: z.unknown(),
300
+ at: Timestamp,
301
+ actorId: z.uuid().nullable(),
302
+ source: z.string(),
303
+ });
304
+ /** A request exploded into days — what the balance was actually charged for. */
305
+ export const LeaveRequestDay = z.object({
306
+ id: z.uuid(),
307
+ requestId: z.uuid(),
308
+ date: IsoDate,
309
+ fraction: z.number(),
310
+ counted: z.boolean(),
311
+ status: z.string(),
312
+ });
313
+ /** A section that hit the row cap. Named, with its numbers, so nothing is short by surprise. */
314
+ export const BundleTruncation = z.object({
315
+ section: z.string(),
316
+ returned: z.number().int(),
317
+ cap: z.number().int(),
318
+ });
319
+ /** Something this bundle does not contain, and why. */
320
+ export const BundleExclusion = z.object({
321
+ section: z.string(),
322
+ reason: z.enum([
323
+ /**
324
+ * The bytes of a file. HR holds the metadata and the id; the object lives in core's storage and
325
+ * a module can only ask for a download URL for one file at a time.
326
+ */
327
+ 'fileContentsNotExportable',
328
+ /** The caller asked for the fast path. */
329
+ 'notRequested',
330
+ ]),
331
+ });
332
+ export const SubjectAccessManifest = z.object({
333
+ ...ws,
334
+ personId: z.uuid(),
335
+ generatedAt: Timestamp,
336
+ generatedBy: z.uuid().nullable(),
337
+ /** From `packageVersion(import.meta.url)`, never a literal — the literals drifted for months. */
338
+ moduleVersion: z.string(),
339
+ truncated: z.array(BundleTruncation),
340
+ excluded: z.array(BundleExclusion),
341
+ });
342
+ /**
343
+ * Everything HR holds about one person, including the parts they cannot reach through the product.
344
+ *
345
+ * Structured JSON rather than a zip of files, and one person at a time. Both are deliberate:
346
+ *
347
+ * - **The rows are not the problem; documents are.** Five years of four punches a day is about nine
348
+ * thousand rows and two to three megabytes — large for a response and bounded. A contract PDF,
349
+ * three payslips a year and an identity scan has no upper bound at all, which is why the file
350
+ * bytes are named in `manifest.excluded` instead of being streamed through a request handler.
351
+ * Every section is capped and every cap that bites is named in `manifest.truncated`.
352
+ * - **There is no workspace-wide variant, and there will not be.** Five hundred people is two and a
353
+ * half million rows with decrypted bank details in flight. `personId` is required and there is no
354
+ * filter.
355
+ *
356
+ * `sensitive` is decrypted here, which makes this the most dangerous object the module produces —
357
+ * so producing it writes a `sensitive_access_log` row with `via: 'export'` before it is returned. An
358
+ * export is a bulk read of that record, not a smaller one.
359
+ */
360
+ export const SubjectAccessBundle = z.object({
361
+ manifest: SubjectAccessManifest,
362
+ /**
363
+ * The whole row, not the directory card.
364
+ *
365
+ * `forViewer` narrows `personalEmail`, `phone`, `hiredOn` and `terminatedOn` for a reader who may
366
+ * not see somebody's personnel record. Those four are the subject's own; they are not narrowed
367
+ * here.
368
+ */
369
+ person: Person,
370
+ sensitive: PersonSensitive,
371
+ employment: z.array(Employment),
372
+ offices: z.array(OfficeAssignment),
373
+ history: z.array(PersonHistoryEntry),
374
+ /** Metadata and file ids. The bytes are named in `manifest.excluded`. */
375
+ documents: z.array(PersonDocument),
376
+ leave: z.object({
377
+ types: z.array(LeaveType),
378
+ requests: z.array(LeaveRequest),
379
+ days: z.array(LeaveRequestDay),
380
+ ledger: z.array(LeaveLedgerEntry),
381
+ /** The running balance down the ledger, so "why is my balance this number" is answered here. */
382
+ closingBalanceMinutes: z.number().int(),
383
+ }),
384
+ attendance: z.object({
385
+ punches: z.array(Punch),
386
+ days: z.array(AttendanceDay),
387
+ }),
388
+ regularizations: z.array(Regularization),
389
+ approvals: z.object({
390
+ /** Requests this person raised. */
391
+ raised: z.array(ApprovalRequest),
392
+ /** Steps they were named on as an approver, with the decisions filed against them. */
393
+ approverOn: z.array(ApprovalStep),
394
+ /** Their own decisions, wherever they were filed. */
395
+ decisions: z.array(ApprovalDecision),
396
+ }),
397
+ /** Both directions: what they handed over, and what was handed to them. */
398
+ delegations: z.object({
399
+ given: z.array(Delegation),
400
+ received: z.array(Delegation),
401
+ }),
402
+ /**
403
+ * The accrual, overtime and rounding policy resolved for this person today, with the rung that
404
+ * answered. Included whatever the `leave_accrual` capability is set to: it is the subject's own
405
+ * data, and "why does she accrue differently from her team" is the commonest follow-up a subject
406
+ * access request produces.
407
+ */
408
+ policiesInForce: z.array(ResolvedPolicy),
409
+ /** Who read their identity, birth date or bank details. A bundle without it is incomplete. */
410
+ accessLog: z.array(SensitiveAccess),
411
+ });
412
+ //# sourceMappingURL=privacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"privacy.js","sourceRoot":"","sources":["../../src/contract/privacy.ts"],"names":[],"mappings":"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"}