@futdevpro/nts-dynamo 1.15.250 → 1.15.253

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 (33) hide show
  1. package/build/_collections/progress-handle.interface.d.ts +20 -0
  2. package/build/_collections/progress-handle.interface.d.ts.map +1 -0
  3. package/build/_collections/progress-handle.interface.js +3 -0
  4. package/build/_collections/progress-handle.interface.js.map +1 -0
  5. package/build/_collections/progress-line.util.d.ts +67 -0
  6. package/build/_collections/progress-line.util.d.ts.map +1 -0
  7. package/build/_collections/progress-line.util.js +379 -0
  8. package/build/_collections/progress-line.util.js.map +1 -0
  9. package/build/_collections/progress-options.interface.d.ts +21 -0
  10. package/build/_collections/progress-options.interface.d.ts.map +1 -0
  11. package/build/_collections/progress-options.interface.js +3 -0
  12. package/build/_collections/progress-options.interface.js.map +1 -0
  13. package/build/_modules/privacy-lifecycle/_enums/privacy-rights-notification-state.type-enum.d.ts +15 -0
  14. package/build/_modules/privacy-lifecycle/_enums/privacy-rights-notification-state.type-enum.d.ts.map +1 -0
  15. package/build/_modules/privacy-lifecycle/_enums/privacy-rights-notification-state.type-enum.js +20 -0
  16. package/build/_modules/privacy-lifecycle/_enums/privacy-rights-notification-state.type-enum.js.map +1 -0
  17. package/build/_modules/privacy-lifecycle/_models/privacy-rights-notification.interface.d.ts +63 -0
  18. package/build/_modules/privacy-lifecycle/_models/privacy-rights-notification.interface.d.ts.map +1 -0
  19. package/build/_modules/privacy-lifecycle/_models/privacy-rights-notification.interface.js +3 -0
  20. package/build/_modules/privacy-lifecycle/_models/privacy-rights-notification.interface.js.map +1 -0
  21. package/build/_modules/privacy-lifecycle/index.d.ts +3 -0
  22. package/build/_modules/privacy-lifecycle/index.d.ts.map +1 -1
  23. package/build/_modules/privacy-lifecycle/index.js +6 -1
  24. package/build/_modules/privacy-lifecycle/index.js.map +1 -1
  25. package/build/_modules/privacy-lifecycle/privacy-rights-notification.mongo-store.d.ts +72 -0
  26. package/build/_modules/privacy-lifecycle/privacy-rights-notification.mongo-store.d.ts.map +1 -0
  27. package/build/_modules/privacy-lifecycle/privacy-rights-notification.mongo-store.js +681 -0
  28. package/build/_modules/privacy-lifecycle/privacy-rights-notification.mongo-store.js.map +1 -0
  29. package/build/index.d.ts +3 -0
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js +3 -0
  32. package/build/index.js.map +1 -1
  33. package/package.json +1 -1
@@ -0,0 +1,681 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DyNTS_PrivacyRightsNotification_MongoStore = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_util_1 = require("node:util");
6
+ const mongoose_1 = require("mongoose");
7
+ const email_submission_outcome_enum_1 = require("../../_enums/email-submission-outcome.enum");
8
+ const privacy_rights_decision_type_enum_1 = require("./_enums/privacy-rights-decision.type-enum");
9
+ const privacy_rights_notification_state_type_enum_1 = require("./_enums/privacy-rights-notification-state.type-enum");
10
+ const privacy_request_type_enum_1 = require("./_enums/privacy-request.type-enum");
11
+ /**
12
+ * Persists a content-free notification intent in the rights transaction and fences later SMTP submission.
13
+ * External delivery never runs inside the Mongo transaction. An expired claim becomes manual review, never resend.
14
+ */
15
+ class DyNTS_PrivacyRightsNotification_MongoStore {
16
+ settings;
17
+ ownedErrors = new WeakSet();
18
+ isReady = false;
19
+ /** Requires explicit timing and a closed server-owned template allowlist. */
20
+ constructor(settings) {
21
+ try {
22
+ if (!settings?.connection || typeof settings.connection.startSession !== 'function'
23
+ || !this.positive(settings.maxOperationTimeMs, 2_147_483_647)
24
+ || !this.positive(settings.claimTimeMs, 86_400_000)
25
+ || !Array.isArray(settings.templateCodes) || settings.templateCodes.length < 1
26
+ || settings.templateCodes.length > 64 || new Set(settings.templateCodes).size !== settings.templateCodes.length
27
+ || settings.templateCodes.some((code) => !this.code(code))) {
28
+ throw new Error();
29
+ }
30
+ this.settings = Object.freeze({ ...settings, templateCodes: Object.freeze([...settings.templateCodes]) });
31
+ }
32
+ catch {
33
+ throw this.error('SETTINGS_INVALID');
34
+ }
35
+ }
36
+ /** Creates and validates the strict outbox schema before any request is admitted. */
37
+ async ensureReady() {
38
+ if (this.isReady) {
39
+ return;
40
+ }
41
+ try {
42
+ try {
43
+ await this.settings.connection.db.createCollection('dynts_privacy_rights_notifications', {
44
+ validator: this.validator(), validationLevel: 'strict', validationAction: 'error',
45
+ maxTimeMS: this.settings.maxOperationTimeMs,
46
+ });
47
+ }
48
+ catch (error) {
49
+ if (!(error instanceof mongoose_1.mongo.MongoServerError) || error.code !== 48) {
50
+ throw error;
51
+ }
52
+ }
53
+ const information = await this.settings.connection.db.listCollections({ name: 'dynts_privacy_rights_notifications' }, { nameOnly: false, maxTimeMS: this.settings.maxOperationTimeMs }).next();
54
+ if (!(0, node_util_1.isDeepStrictEqual)(information?.options?.validator, this.validator())
55
+ || information?.options?.validationLevel !== 'strict'
56
+ || information?.options?.validationAction !== 'error') {
57
+ throw this.error('SCHEMA_MIGRATION_REQUIRED');
58
+ }
59
+ await this.collection().createIndex({ requestId: 1, subjectId: 1, organizationId: 1 }, { unique: true, name: 'uq_dynts_rights_notification_authority', maxTimeMS: this.settings.maxOperationTimeMs });
60
+ this.isReady = true;
61
+ }
62
+ catch (error) {
63
+ if (error instanceof Error && this.ownedErrors.has(error)) {
64
+ throw error;
65
+ }
66
+ throw this.error('NOT_READY');
67
+ }
68
+ }
69
+ /** Schedules exactly once in the same strong transaction as the product mutation and execution journal. */
70
+ async schedule(set) {
71
+ if (!this.exactRecord(set, ['execution', 'templateCode', 'session'])) {
72
+ throw this.error('SCHEDULE_INVALID');
73
+ }
74
+ return this.inTransaction(set?.session, async () => {
75
+ const execution = this.execution(set?.execution);
76
+ const templateCode = this.template(set?.templateCode);
77
+ const authority = this.authority(execution);
78
+ const key = this.key(authority);
79
+ const existing = await this.collection().findOne({ _id: key }, this.transactionOptions(set.session));
80
+ if (existing) {
81
+ this.assertRecord(existing, authority);
82
+ if (existing.executionReference !== this.executionReference(execution)
83
+ || existing.executionCompletedAt !== execution.completedAt || existing.templateCode !== templateCode) {
84
+ throw this.error('SCHEDULE_CONFLICT');
85
+ }
86
+ return this.view(existing, false);
87
+ }
88
+ const record = {
89
+ _id: key, contractVersion: 'dynts-privacy-rights-notification/1', ...authority,
90
+ executionReference: this.executionReference(execution), executionCompletedAt: execution.completedAt,
91
+ templateCode: templateCode, state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.pending,
92
+ attemptCount: 0, revision: 0, claimTokenHash: '', claimedAt: '', claimExpiresAt: '',
93
+ channel: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.none, outcome: '', submissionReference: '',
94
+ acceptedCount: 0, rejectedCount: 0, pendingCount: 0,
95
+ completedAt: '',
96
+ };
97
+ await this.collection().insertOne(record, this.transactionOptions(set.session));
98
+ return this.view(record, true);
99
+ });
100
+ }
101
+ /** Claims one pending notification or converts an abandoned external-send claim to manual review. */
102
+ async claim(authorityValue) {
103
+ return this.protect('CLAIM_FAILED', () => this.claimInternal(authorityValue));
104
+ }
105
+ async claimInternal(authorityValue) {
106
+ this.assertReady();
107
+ if (!this.exactRecord(authorityValue, ['requestId', 'subjectId', 'organizationId'])) {
108
+ throw this.error('AUTHORITY_INVALID');
109
+ }
110
+ const authority = this.authority(authorityValue);
111
+ const now = await this.databaseTime();
112
+ let record = await this.collection().findOne({ _id: this.key(authority) }, this.options());
113
+ if (!record) {
114
+ throw this.error('NOT_SCHEDULED');
115
+ }
116
+ this.assertRecord(record, authority);
117
+ if (record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching) {
118
+ if (Date.parse(record.claimExpiresAt) <= now.getTime()) {
119
+ record = await this.expireClaim(record, now);
120
+ this.assertRecord(record, authority);
121
+ }
122
+ return this.claimResult(record, false, '');
123
+ }
124
+ if (record.state !== privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.pending) {
125
+ return this.claimResult(record, false, '');
126
+ }
127
+ const claimToken = (0, node_crypto_1.randomBytes)(32).toString('hex');
128
+ const claimed = await this.collection().findOneAndUpdate({ _id: record._id, state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.pending, revision: record.revision }, { $set: {
129
+ state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching,
130
+ claimTokenHash: this.tokenHash(claimToken), claimedAt: now.toISOString(),
131
+ claimExpiresAt: new Date(now.getTime() + this.settings.claimTimeMs).toISOString(),
132
+ }, $inc: { attemptCount: 1, revision: 1 } }, { ...this.options(), returnDocument: 'after' });
133
+ if (!claimed) {
134
+ record = await this.collection().findOne({ _id: record._id }, this.options());
135
+ if (!record) {
136
+ throw this.error('CLAIM_RACE_UNRESOLVED');
137
+ }
138
+ this.assertRecord(record, authority);
139
+ return this.claimResult(record, false, '');
140
+ }
141
+ this.assertRecord(claimed, authority);
142
+ return this.claimResult(claimed, true, claimToken);
143
+ }
144
+ /** Persists only the content-free email receipt and supports exact lost-response replay. */
145
+ async complete(value) {
146
+ return this.protect('COMPLETION_FAILED', () => this.completeInternal(value));
147
+ }
148
+ async completeInternal(value) {
149
+ this.assertReady();
150
+ const input = this.completion(value);
151
+ const authority = this.authority(input);
152
+ const now = await this.databaseTime();
153
+ const record = await this.requireRecord(authority);
154
+ this.assertClaimToken(record, input.claimToken);
155
+ if (this.terminal(record.state)) {
156
+ if (!this.sameSubmission(record, input.submission)) {
157
+ throw this.error('COMPLETION_CONFLICT');
158
+ }
159
+ return this.view(record, false);
160
+ }
161
+ if (record.state !== privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching) {
162
+ throw this.error('CLAIM_REQUIRED');
163
+ }
164
+ const state = input.submission.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.accepted
165
+ ? privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.submitted
166
+ : input.submission.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.rejected
167
+ ? privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.rejected
168
+ : privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview;
169
+ const updated = await this.collection().findOneAndUpdate({ _id: record._id, state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching,
170
+ revision: record.revision, claimTokenHash: record.claimTokenHash }, { $set: {
171
+ state: state, channel: this.notificationChannel(input.submission.channel),
172
+ outcome: input.submission.outcome,
173
+ submissionReference: input.submission.submissionReference,
174
+ acceptedCount: input.submission.acceptedCount, rejectedCount: input.submission.rejectedCount,
175
+ pendingCount: input.submission.pendingCount, completedAt: now.toISOString(),
176
+ }, $inc: { revision: 1 } }, { ...this.options(), returnDocument: 'after' });
177
+ if (!updated) {
178
+ throw this.error('COMPLETION_RACE');
179
+ }
180
+ this.assertRecord(updated, authority);
181
+ return this.view(updated, true);
182
+ }
183
+ /** Records an attempted-but-unconfirmed send without persisting any raw transport failure. */
184
+ async markIndeterminate(value) {
185
+ return this.protect('INDETERMINATE_FAILED', () => this.markIndeterminateInternal(value));
186
+ }
187
+ async markIndeterminateInternal(value) {
188
+ this.assertReady();
189
+ const input = this.indeterminate(value);
190
+ const authority = this.authority(input);
191
+ const record = await this.requireRecord(authority);
192
+ this.assertClaimToken(record, input.claimToken);
193
+ if (this.terminal(record.state)) {
194
+ if (record.state !== privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview
195
+ || record.outcome !== email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.indeterminate) {
196
+ throw this.error('INDETERMINATE_CONFLICT');
197
+ }
198
+ return this.view(record, false);
199
+ }
200
+ if (record.state !== privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching) {
201
+ throw this.error('CLAIM_REQUIRED');
202
+ }
203
+ const now = await this.databaseTime();
204
+ const updated = await this.collection().findOneAndUpdate({ _id: record._id, state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching,
205
+ revision: record.revision, claimTokenHash: record.claimTokenHash }, { $set: {
206
+ state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview,
207
+ channel: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.none,
208
+ outcome: email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.indeterminate, submissionReference: '',
209
+ acceptedCount: 0, rejectedCount: 0, pendingCount: 0, completedAt: now.toISOString(),
210
+ }, $inc: { revision: 1 } }, { ...this.options(), returnDocument: 'after' });
211
+ if (!updated) {
212
+ throw this.error('INDETERMINATE_RACE');
213
+ }
214
+ this.assertRecord(updated, authority);
215
+ return this.view(updated, true);
216
+ }
217
+ /** Returns content-free current state; claim capability and message data never leave the worker boundary. */
218
+ async read(authorityValue) {
219
+ return this.protect('READ_FAILED', () => this.readInternal(authorityValue));
220
+ }
221
+ async readInternal(authorityValue) {
222
+ this.assertReady();
223
+ if (!this.exactRecord(authorityValue, ['requestId', 'subjectId', 'organizationId'])) {
224
+ throw this.error('AUTHORITY_INVALID');
225
+ }
226
+ const authority = this.authority(authorityValue);
227
+ const record = await this.collection().findOne({ _id: this.key(authority) }, this.options());
228
+ if (!record) {
229
+ return null;
230
+ }
231
+ this.assertRecord(record, authority);
232
+ return this.view(record, false);
233
+ }
234
+ async expireClaim(record, now) {
235
+ const updated = await this.collection().findOneAndUpdate({ _id: record._id, state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching,
236
+ revision: record.revision, claimExpiresAt: record.claimExpiresAt }, { $set: {
237
+ state: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview,
238
+ channel: privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.none,
239
+ outcome: email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.indeterminate, submissionReference: '',
240
+ acceptedCount: 0, rejectedCount: 0, pendingCount: 0, completedAt: now.toISOString(),
241
+ }, $inc: { revision: 1 } }, { ...this.options(), returnDocument: 'after' });
242
+ if (!updated) {
243
+ const current = await this.collection().findOne({ _id: record._id }, this.options());
244
+ if (!current) {
245
+ throw this.error('EXPIRED_CLAIM_RACE');
246
+ }
247
+ return current;
248
+ }
249
+ return updated;
250
+ }
251
+ execution(value) {
252
+ if (!this.exactRecord(value, [
253
+ 'contractVersion', 'requestId', 'subjectId', 'organizationId', 'requestType', 'policyVersion',
254
+ 'expectedRequestRevision', 'rectifications', 'purposes', 'completedAt', 'created',
255
+ ])) {
256
+ throw this.error('EXECUTION_INVALID');
257
+ }
258
+ const rectifications = this.rectifications(value.rectifications);
259
+ const purposes = this.purposes(value.purposes);
260
+ const completedAtValue = typeof value.completedAt === 'string' ? value.completedAt : '';
261
+ const completedAt = Date.parse(completedAtValue);
262
+ const requestType = this.rightsRequestType(value.requestType);
263
+ const rectificationRequest = requestType === privacy_request_type_enum_1.DyNTS_PrivacyRequest_Type.rectification;
264
+ const purposeRequest = requestType === privacy_request_type_enum_1.DyNTS_PrivacyRequest_Type.restriction
265
+ || requestType === privacy_request_type_enum_1.DyNTS_PrivacyRequest_Type.objection;
266
+ if (value.contractVersion !== 'dynts-privacy-rights-execution-receipt/1'
267
+ || !this.code(value.requestId) || !this.code(value.subjectId) || !this.code(value.organizationId)
268
+ || !this.code(value.policyVersion) || !Number.isSafeInteger(value.expectedRequestRevision)
269
+ || Number(value.expectedRequestRevision) < 0 || typeof value.created !== 'boolean'
270
+ || !rectifications || !purposes || !Number.isFinite(completedAt) || !requestType
271
+ || (rectificationRequest && (rectifications.length < 1 || purposes.length !== 0))
272
+ || (purposeRequest && (purposes.length < 1 || rectifications.length !== 0))
273
+ || rectifications.some((decision) => (!this.rectificationSemantics(decision)))
274
+ || purposes.some((decision) => (!this.purposeSemantics(decision, completedAt)))) {
275
+ throw this.error('EXECUTION_INVALID');
276
+ }
277
+ return Object.freeze({
278
+ contractVersion: 'dynts-privacy-rights-execution-receipt/1',
279
+ requestId: value.requestId, subjectId: value.subjectId, organizationId: value.organizationId,
280
+ requestType: requestType, policyVersion: value.policyVersion,
281
+ expectedRequestRevision: Number(value.expectedRequestRevision), rectifications: rectifications,
282
+ purposes: purposes, completedAt: completedAtValue, created: value.created,
283
+ });
284
+ }
285
+ completion(value) {
286
+ if (!this.exactRecord(value, [
287
+ 'contractVersion', 'requestId', 'subjectId', 'organizationId', 'claimToken', 'submission',
288
+ ])) {
289
+ throw this.error('COMPLETION_INVALID');
290
+ }
291
+ const submission = this.submission(value.submission);
292
+ if (value.contractVersion !== 'dynts-privacy-rights-notification-completion/1'
293
+ || !this.code(value.requestId) || !this.code(value.subjectId) || !this.code(value.organizationId)
294
+ || !this.token(value.claimToken) || !submission) {
295
+ throw this.error('COMPLETION_INVALID');
296
+ }
297
+ return Object.freeze({
298
+ contractVersion: 'dynts-privacy-rights-notification-completion/1',
299
+ requestId: value.requestId, subjectId: value.subjectId, organizationId: value.organizationId,
300
+ claimToken: value.claimToken, submission: submission,
301
+ });
302
+ }
303
+ indeterminate(value) {
304
+ if (!this.exactRecord(value, [
305
+ 'contractVersion', 'requestId', 'subjectId', 'organizationId', 'claimToken',
306
+ ])) {
307
+ throw this.error('INDETERMINATE_INVALID');
308
+ }
309
+ if (value.contractVersion !== 'dynts-privacy-rights-notification-indeterminate/1'
310
+ || !this.code(value.requestId) || !this.code(value.subjectId) || !this.code(value.organizationId)
311
+ || !this.token(value.claimToken)) {
312
+ throw this.error('INDETERMINATE_INVALID');
313
+ }
314
+ return Object.freeze({
315
+ contractVersion: 'dynts-privacy-rights-notification-indeterminate/1',
316
+ requestId: value.requestId, subjectId: value.subjectId, organizationId: value.organizationId,
317
+ claimToken: value.claimToken,
318
+ });
319
+ }
320
+ submission(value) {
321
+ if (!this.exactRecord(value, [
322
+ 'contractVersion', 'channel', 'outcome', 'submissionReference',
323
+ 'acceptedCount', 'rejectedCount', 'pendingCount',
324
+ ])) {
325
+ return null;
326
+ }
327
+ const channel = this.emailChannel(value.channel);
328
+ const outcome = this.emailOutcome(value.outcome)
329
+ ? value.outcome : null;
330
+ const submissionReference = typeof value.submissionReference === 'string'
331
+ ? value.submissionReference : '';
332
+ const acceptedCount = this.count(value.acceptedCount) ? value.acceptedCount : -1;
333
+ const rejectedCount = this.count(value.rejectedCount) ? value.rejectedCount : -1;
334
+ const pendingCount = this.count(value.pendingCount) ? value.pendingCount : -1;
335
+ const valid = value.contractVersion === 'dynts-email-submission-receipt/1'
336
+ && !!channel && !!outcome
337
+ && (submissionReference === '' || /^sha256:[a-f0-9]{64}$/u.test(submissionReference))
338
+ && acceptedCount >= 0 && rejectedCount >= 0 && pendingCount >= 0
339
+ && !(outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.accepted
340
+ && channel === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.smtp
341
+ && (acceptedCount < 1 || rejectedCount !== 0 || pendingCount !== 0 || submissionReference === ''));
342
+ return valid ? Object.freeze({
343
+ contractVersion: 'dynts-email-submission-receipt/1', channel: channel,
344
+ outcome: outcome,
345
+ submissionReference: submissionReference, acceptedCount: acceptedCount,
346
+ rejectedCount: rejectedCount, pendingCount: pendingCount,
347
+ }) : null;
348
+ }
349
+ rectifications(value) {
350
+ if (!Array.isArray(value) || value.length > 64) {
351
+ return null;
352
+ }
353
+ const decisions = [];
354
+ for (const entry of value) {
355
+ if (!this.exactRecord(entry, [
356
+ 'fieldCode', 'decision', 'expectedRevision', 'resultingRevision', 'reasonCode',
357
+ ]) || !this.code(entry.fieldCode) || !this.rectificationDecision(entry.decision)
358
+ || !this.count(entry.expectedRevision) || !this.count(entry.resultingRevision)
359
+ || typeof entry.reasonCode !== 'string' || entry.reasonCode.length > 128) {
360
+ return null;
361
+ }
362
+ decisions.push(Object.freeze({
363
+ fieldCode: entry.fieldCode,
364
+ decision: entry.decision,
365
+ expectedRevision: entry.expectedRevision, resultingRevision: entry.resultingRevision,
366
+ reasonCode: entry.reasonCode,
367
+ }));
368
+ }
369
+ return Object.freeze(decisions);
370
+ }
371
+ purposes(value) {
372
+ if (!Array.isArray(value) || value.length > 64) {
373
+ return null;
374
+ }
375
+ const decisions = [];
376
+ for (const entry of value) {
377
+ if (!this.exactRecord(entry, ['purposeCode', 'decision', 'reasonCode', 'exceptionExpiresAt'])
378
+ || !this.code(entry.purposeCode) || !this.purposeDecision(entry.decision)
379
+ || typeof entry.reasonCode !== 'string' || entry.reasonCode.length > 128
380
+ || typeof entry.exceptionExpiresAt !== 'string' || entry.exceptionExpiresAt.length > 24) {
381
+ return null;
382
+ }
383
+ decisions.push(Object.freeze({
384
+ purposeCode: entry.purposeCode, decision: entry.decision,
385
+ reasonCode: entry.reasonCode, exceptionExpiresAt: entry.exceptionExpiresAt,
386
+ }));
387
+ }
388
+ return Object.freeze(decisions);
389
+ }
390
+ async requireRecord(authority) {
391
+ const record = await this.collection().findOne({ _id: this.key(authority) }, this.options());
392
+ if (!record) {
393
+ throw this.error('NOT_SCHEDULED');
394
+ }
395
+ this.assertRecord(record, authority);
396
+ return record;
397
+ }
398
+ assertRecord(record, authority) {
399
+ if (!this.exactRecord(record, [
400
+ '_id', 'contractVersion', 'requestId', 'subjectId', 'organizationId', 'executionReference',
401
+ 'executionCompletedAt', 'templateCode', 'state', 'attemptCount', 'revision', 'claimTokenHash',
402
+ 'claimedAt', 'claimExpiresAt', 'channel', 'outcome', 'submissionReference', 'acceptedCount',
403
+ 'rejectedCount', 'pendingCount', 'completedAt',
404
+ ]) || record._id !== this.key(authority) || record.contractVersion !== 'dynts-privacy-rights-notification/1'
405
+ || record.requestId !== authority.requestId || record.subjectId !== authority.subjectId
406
+ || record.organizationId !== authority.organizationId || !/^sha256:[a-f0-9]{64}$/u.test(record.executionReference)
407
+ || !Number.isFinite(Date.parse(record.executionCompletedAt)) || !this.code(record.templateCode)
408
+ || !this.settings.templateCodes.includes(record.templateCode)
409
+ || !Object.values(privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type).includes(record.state)
410
+ || !this.count(record.attemptCount) || !this.count(record.revision)
411
+ || !this.count(record.acceptedCount) || !this.count(record.rejectedCount) || !this.count(record.pendingCount)
412
+ || !this.recordStateValid(record)) {
413
+ throw this.error('RECORD_CORRUPT');
414
+ }
415
+ }
416
+ recordStateValid(record) {
417
+ if (record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.pending) {
418
+ return record.attemptCount === 0 && record.revision === 0 && record.claimTokenHash === ''
419
+ && record.claimedAt === '' && record.claimExpiresAt === ''
420
+ && record.channel === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.none && record.outcome === ''
421
+ && record.submissionReference === '' && record.acceptedCount === 0 && record.rejectedCount === 0
422
+ && record.pendingCount === 0 && record.completedAt === '';
423
+ }
424
+ const claimValid = /^sha256:[a-f0-9]{64}$/u.test(record.claimTokenHash)
425
+ && Number.isFinite(Date.parse(record.claimedAt)) && Number.isFinite(Date.parse(record.claimExpiresAt))
426
+ && Date.parse(record.claimExpiresAt) > Date.parse(record.claimedAt) && record.attemptCount === 1
427
+ && record.revision >= 1;
428
+ if (record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.dispatching) {
429
+ return claimValid && record.revision === 1
430
+ && record.channel === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.none && record.outcome === ''
431
+ && record.submissionReference === '' && record.completedAt === '';
432
+ }
433
+ if (!claimValid || record.revision !== 2 || !Number.isFinite(Date.parse(record.completedAt))) {
434
+ return false;
435
+ }
436
+ if (record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview
437
+ && record.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.indeterminate
438
+ && record.channel === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.none) {
439
+ return record.submissionReference === '' && record.acceptedCount === 0
440
+ && record.rejectedCount === 0 && record.pendingCount === 0;
441
+ }
442
+ const submission = this.submission({
443
+ contractVersion: 'dynts-email-submission-receipt/1', channel: record.channel, outcome: record.outcome,
444
+ submissionReference: record.submissionReference, acceptedCount: record.acceptedCount,
445
+ rejectedCount: record.rejectedCount, pendingCount: record.pendingCount,
446
+ });
447
+ return !!submission && ((record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.submitted
448
+ && record.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.accepted)
449
+ || (record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.rejected
450
+ && record.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.rejected)
451
+ || (record.state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview
452
+ && (record.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.partial
453
+ || record.outcome === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.indeterminate)));
454
+ }
455
+ sameSubmission(record, submission) {
456
+ return record.channel === submission.channel && record.outcome === submission.outcome
457
+ && record.submissionReference === submission.submissionReference
458
+ && record.acceptedCount === submission.acceptedCount && record.rejectedCount === submission.rejectedCount
459
+ && record.pendingCount === submission.pendingCount;
460
+ }
461
+ view(record, created) {
462
+ return Object.freeze({
463
+ contractVersion: record.contractVersion, requestId: record.requestId, subjectId: record.subjectId,
464
+ organizationId: record.organizationId, executionReference: record.executionReference,
465
+ executionCompletedAt: record.executionCompletedAt, templateCode: record.templateCode, state: record.state,
466
+ attemptCount: record.attemptCount, channel: record.channel, outcome: record.outcome,
467
+ submissionReference: record.submissionReference, acceptedCount: record.acceptedCount,
468
+ rejectedCount: record.rejectedCount, pendingCount: record.pendingCount,
469
+ completedAt: record.completedAt, created: created,
470
+ });
471
+ }
472
+ claimResult(record, claimed, claimToken) {
473
+ return Object.freeze({
474
+ contractVersion: 'dynts-privacy-rights-notification-claim/1', claimed: claimed,
475
+ claimToken: claimToken, notification: this.view(record, false),
476
+ });
477
+ }
478
+ authority(value) {
479
+ if (!value || typeof value !== 'object') {
480
+ throw this.error('AUTHORITY_INVALID');
481
+ }
482
+ const descriptors = Object.getOwnPropertyDescriptors(value);
483
+ const requestId = descriptors.requestId?.value;
484
+ const subjectId = descriptors.subjectId?.value;
485
+ const organizationId = descriptors.organizationId?.value;
486
+ if (!this.code(requestId) || !this.code(subjectId) || !this.code(organizationId)) {
487
+ throw this.error('AUTHORITY_INVALID');
488
+ }
489
+ return Object.freeze({ requestId: requestId, subjectId: subjectId, organizationId: organizationId });
490
+ }
491
+ template(value) {
492
+ if (typeof value !== 'string' || !this.settings.templateCodes.includes(value)) {
493
+ throw this.error('TEMPLATE_INVALID');
494
+ }
495
+ return value;
496
+ }
497
+ async inTransaction(session, task) {
498
+ const concern = session?.transaction?.options?.readConcern;
499
+ if (!this.isReady || !session?.inTransaction() || session.transaction.options.writeConcern?.w !== 'majority'
500
+ || (typeof concern === 'string' ? concern : concern?.level) !== 'snapshot') {
501
+ throw this.error('OWNING_TRANSACTION_REQUIRED');
502
+ }
503
+ try {
504
+ await this.collection().findOne({ _id: '' }, this.transactionOptions(session));
505
+ return await task();
506
+ }
507
+ catch (error) {
508
+ if (error instanceof mongoose_1.mongo.MongoError && error.hasErrorLabel('TransientTransactionError')) {
509
+ const retryable = new mongoose_1.mongo.MongoError('DyNTS|PRIVACY_RIGHTS_NOTIFICATION|TRANSACTION_FAILED');
510
+ retryable.addErrorLabel('TransientTransactionError');
511
+ throw retryable;
512
+ }
513
+ if (error instanceof Error && this.ownedErrors.has(error)) {
514
+ throw error;
515
+ }
516
+ throw this.error('SCHEDULE_FAILED');
517
+ }
518
+ }
519
+ async protect(code, task) {
520
+ try {
521
+ return await task();
522
+ }
523
+ catch (error) {
524
+ if (error instanceof Error && this.ownedErrors.has(error)) {
525
+ throw error;
526
+ }
527
+ throw this.error(code);
528
+ }
529
+ }
530
+ assertClaimToken(record, token) {
531
+ if (!this.token(token) || record.claimTokenHash !== this.tokenHash(token)) {
532
+ throw this.error('CLAIM_TOKEN_INVALID');
533
+ }
534
+ }
535
+ executionReference(execution) {
536
+ return `sha256:${(0, node_crypto_1.createHash)('sha256').update(JSON.stringify({
537
+ contractVersion: execution.contractVersion, requestId: execution.requestId, subjectId: execution.subjectId,
538
+ organizationId: execution.organizationId, requestType: execution.requestType,
539
+ policyVersion: execution.policyVersion, expectedRequestRevision: execution.expectedRequestRevision,
540
+ rectifications: execution.rectifications, purposes: execution.purposes, completedAt: execution.completedAt,
541
+ })).digest('hex')}`;
542
+ }
543
+ tokenHash(token) {
544
+ return `sha256:${(0, node_crypto_1.createHash)('sha256').update(token).digest('hex')}`;
545
+ }
546
+ key(authority) {
547
+ return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([
548
+ 'dynts-privacy-rights-notification/1', authority.requestId, authority.subjectId, authority.organizationId,
549
+ ])).digest('hex');
550
+ }
551
+ async databaseTime() {
552
+ const result = await this.settings.connection.db.command({ hello: 1, maxTimeMS: this.settings.maxOperationTimeMs }, { timeoutMS: this.settings.maxOperationTimeMs });
553
+ const now = result.localTime;
554
+ if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
555
+ throw this.error('CLOCK_UNAVAILABLE');
556
+ }
557
+ return now;
558
+ }
559
+ terminal(state) {
560
+ return state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.submitted
561
+ || state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.rejected
562
+ || state === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type.manualReview;
563
+ }
564
+ emailOutcome(value) {
565
+ return value === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.accepted
566
+ || value === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.partial
567
+ || value === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.rejected
568
+ || value === email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum.indeterminate;
569
+ }
570
+ emailChannel(value) {
571
+ if (value === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.smtp
572
+ || value === privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.testSink) {
573
+ return value;
574
+ }
575
+ return null;
576
+ }
577
+ notificationChannel(value) {
578
+ return value === 'smtp' ? privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.smtp
579
+ : privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type.testSink;
580
+ }
581
+ rightsRequestType(value) {
582
+ if (value === privacy_request_type_enum_1.DyNTS_PrivacyRequest_Type.rectification || value === privacy_request_type_enum_1.DyNTS_PrivacyRequest_Type.restriction
583
+ || value === privacy_request_type_enum_1.DyNTS_PrivacyRequest_Type.objection) {
584
+ return value;
585
+ }
586
+ return null;
587
+ }
588
+ rectificationDecision(value) {
589
+ return value === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsRectificationDecision_Type.updated
590
+ || value === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsRectificationDecision_Type.correctionRequired
591
+ || value === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsRectificationDecision_Type.refused;
592
+ }
593
+ purposeDecision(value) {
594
+ return value === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsPurposeDecision_Type.stopped
595
+ || value === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsPurposeDecision_Type.retainedWithException
596
+ || value === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsPurposeDecision_Type.rejected;
597
+ }
598
+ rectificationSemantics(decision) {
599
+ if (decision.decision === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsRectificationDecision_Type.updated) {
600
+ return decision.resultingRevision > decision.expectedRevision && decision.reasonCode === '';
601
+ }
602
+ return this.code(decision.reasonCode);
603
+ }
604
+ purposeSemantics(decision, completedAt) {
605
+ if (decision.decision === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsPurposeDecision_Type.stopped) {
606
+ return decision.reasonCode === '' && decision.exceptionExpiresAt === '';
607
+ }
608
+ if (decision.decision === privacy_rights_decision_type_enum_1.DyNTS_PrivacyRightsPurposeDecision_Type.rejected) {
609
+ return this.code(decision.reasonCode) && decision.exceptionExpiresAt === '';
610
+ }
611
+ const exceptionExpiresAt = Date.parse(decision.exceptionExpiresAt);
612
+ return this.code(decision.reasonCode) && Number.isFinite(exceptionExpiresAt)
613
+ && exceptionExpiresAt > completedAt;
614
+ }
615
+ exactRecord(value, expectedKeys) {
616
+ if (typeof value !== 'object' || value === null) {
617
+ return false;
618
+ }
619
+ const keys = Reflect.ownKeys(value);
620
+ return keys.length === expectedKeys.length && expectedKeys.every((key) => {
621
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
622
+ return !!descriptor && 'value' in descriptor;
623
+ });
624
+ }
625
+ assertReady() {
626
+ if (!this.isReady) {
627
+ throw this.error('NOT_READY');
628
+ }
629
+ }
630
+ positive(value, maximum) {
631
+ return Number.isSafeInteger(value) && Number(value) > 0 && Number(value) <= maximum;
632
+ }
633
+ count(value) {
634
+ return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= 1_000_000;
635
+ }
636
+ code(value) {
637
+ return typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/u.test(value);
638
+ }
639
+ token(value) {
640
+ return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value);
641
+ }
642
+ options() {
643
+ return { maxTimeMS: this.settings.maxOperationTimeMs };
644
+ }
645
+ transactionOptions(session) {
646
+ return { session: session, maxTimeMS: this.settings.maxOperationTimeMs };
647
+ }
648
+ collection() {
649
+ return this.settings.connection.db.collection('dynts_privacy_rights_notifications');
650
+ }
651
+ error(code) {
652
+ const error = new Error(`DyNTS|PRIVACY_RIGHTS_NOTIFICATION|${code}`);
653
+ this.ownedErrors.add(error);
654
+ return error;
655
+ }
656
+ validator() {
657
+ const code = { bsonType: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$' };
658
+ const count = {
659
+ bsonType: ['int', 'long', 'double'], minimum: 0, maximum: Number.MAX_SAFE_INTEGER,
660
+ };
661
+ const properties = {
662
+ _id: { bsonType: 'string', pattern: '^[a-f0-9]{64}$' },
663
+ contractVersion: { enum: ['dynts-privacy-rights-notification/1'] },
664
+ requestId: code, subjectId: code, organizationId: code,
665
+ executionReference: { bsonType: 'string', pattern: '^sha256:[a-f0-9]{64}$' },
666
+ executionCompletedAt: { bsonType: 'string', maxLength: 24 }, templateCode: code,
667
+ state: { enum: Object.values(privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationState_Type) },
668
+ attemptCount: count, revision: count,
669
+ claimTokenHash: { bsonType: 'string', maxLength: 71 }, claimedAt: { bsonType: 'string', maxLength: 24 },
670
+ claimExpiresAt: { bsonType: 'string', maxLength: 24 },
671
+ channel: { enum: Object.values(privacy_rights_notification_state_type_enum_1.DyNTS_PrivacyRightsNotificationChannel_Type) },
672
+ outcome: { enum: ['', ...Object.values(email_submission_outcome_enum_1.DyNTS_EmailSubmissionOutcome_Enum)] },
673
+ submissionReference: { bsonType: 'string', maxLength: 71 }, acceptedCount: count,
674
+ rejectedCount: count, pendingCount: count, completedAt: { bsonType: 'string', maxLength: 24 },
675
+ };
676
+ return { $jsonSchema: { bsonType: 'object', additionalProperties: false,
677
+ required: Object.keys(properties), properties: properties } };
678
+ }
679
+ }
680
+ exports.DyNTS_PrivacyRightsNotification_MongoStore = DyNTS_PrivacyRightsNotification_MongoStore;
681
+ //# sourceMappingURL=privacy-rights-notification.mongo-store.js.map