@serve.zone/dcrouter 32.1.1 → 32.1.2
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.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +2 -2
- package/dist_ts/config/classes.gateway-client-manager.js +1 -1
- package/dist_ts/db/documents/classes.cached.email.d.ts +0 -1
- package/dist_ts/db/documents/classes.cached.email.js +45 -48
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +61 -50
- package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +9 -2
- package/dist_ts/db/documents/classes.gateway-client.doc.js +9 -11
- package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +4 -3
- package/dist_ts/db/documents/classes.ip-intelligence.doc.js +27 -31
- package/dist_ts/db/documents/classes.ops-config-event.doc.d.ts +0 -2
- package/dist_ts/db/documents/classes.ops-config-event.doc.js +70 -68
- package/dist_ts/db/documents/classes.smartmta-storage.doc.d.ts +14 -0
- package/dist_ts/db/documents/classes.smartmta-storage.doc.js +17 -3
- package/dist_ts/db/documents/classes.webpush-admission.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.webpush-admission.doc.js +14 -5
- package/dist_ts/db/documents/classes.webpush-binding.doc.d.ts +1 -1
- package/dist_ts/db/documents/classes.webpush-binding.doc.js +45 -7
- package/dist_ts/db/documents/classes.webpush-spool.doc.d.ts +0 -1
- package/dist_ts/db/documents/classes.webpush-spool.doc.js +33 -5
- package/dist_ts/dns/manager.dns.js +7 -3
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +15 -3
- package/dist_ts/email/classes.smartmta-storage-manager.js +62 -36
- package/dist_ts/security/classes.security-policy-manager.js +11 -9
- package/dist_ts/webpush/classes.webpush-manager.d.ts +5 -1
- package/dist_ts/webpush/classes.webpush-manager.js +162 -232
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +3 -3
- package/readme.md +1 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +1 -1
- package/ts/config/classes.gateway-client-manager.ts +4 -2
- package/ts/db/documents/classes.cached.email.ts +51 -66
- package/ts/db/documents/classes.email-traffic-bucket.doc.ts +65 -59
- package/ts/db/documents/classes.gateway-client.doc.ts +38 -18
- package/ts/db/documents/classes.ip-intelligence.doc.ts +37 -40
- package/ts/db/documents/classes.ops-config-event.doc.ts +80 -77
- package/ts/db/documents/classes.smartmta-storage.doc.ts +16 -1
- package/ts/db/documents/classes.webpush-admission.doc.ts +14 -5
- package/ts/db/documents/classes.webpush-binding.doc.ts +53 -7
- package/ts/db/documents/classes.webpush-spool.doc.ts +39 -5
- package/ts/dns/manager.dns.ts +6 -2
- package/ts/email/classes.smartmta-storage-manager.ts +66 -50
- package/ts/security/classes.security-policy-manager.ts +10 -8
- package/ts/webpush/classes.webpush-manager.ts +175 -280
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -9,6 +9,20 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
11
11
|
|
|
12
|
+
const PRUNE_BATCH_SIZE = 500;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* "Not acknowledged yet" as a typed predicate: the field is absent (how the ORM stores
|
|
16
|
+
* an unset optional field) or explicitly null (how older documents may carry it). This
|
|
17
|
+
* is exactly what the previous `{ acknowledgedAt: null }` selector matched.
|
|
18
|
+
*/
|
|
19
|
+
const unacknowledgedBranches = (): Array<{
|
|
20
|
+
acknowledgedAt: { $exists: false } | { $type: 'null' };
|
|
21
|
+
}> => [
|
|
22
|
+
{ acknowledgedAt: { $exists: false } },
|
|
23
|
+
{ acknowledgedAt: { $type: 'null' } },
|
|
24
|
+
];
|
|
25
|
+
|
|
12
26
|
export interface IOpsConfigEventFilter {
|
|
13
27
|
acknowledged?: boolean;
|
|
14
28
|
severity?: TOpsConfigEventSeverity;
|
|
@@ -87,21 +101,6 @@ export class OpsConfigEventDoc extends plugins.smartdata.SmartDataDbDoc<OpsConfi
|
|
|
87
101
|
return savable;
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
private static async getNativeCollection() {
|
|
91
|
-
const smartdataCollection = (OpsConfigEventDoc as typeof OpsConfigEventDoc & {
|
|
92
|
-
collection: plugins.smartdata.SmartdataCollection<OpsConfigEventDoc>;
|
|
93
|
-
}).collection;
|
|
94
|
-
await smartdataCollection.init();
|
|
95
|
-
const probe = new OpsConfigEventDoc();
|
|
96
|
-
await smartdataCollection.markUniqueIndexes(probe.uniqueIndexes || []);
|
|
97
|
-
await smartdataCollection.createRegularIndexes(probe.regularIndexes || []);
|
|
98
|
-
return smartdataCollection.mongoDbCollection;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private static hydrate(rawArg: Record<string, unknown>): OpsConfigEventDoc {
|
|
102
|
-
return OpsConfigEventDoc.createInstanceFromMongoDbNativeDoc(rawArg as any);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
104
|
public static async upsertOpenEvent(optionsArg: {
|
|
106
105
|
id: string;
|
|
107
106
|
openDedupeKey: string;
|
|
@@ -112,9 +111,13 @@ export class OpsConfigEventDoc extends plugins.smartdata.SmartDataDbDoc<OpsConfi
|
|
|
112
111
|
context: IOpsConfigEventContext;
|
|
113
112
|
createdAt: number;
|
|
114
113
|
}): Promise<{ event: OpsConfigEventDoc; created: boolean }> {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
// Deduplication is arbitrated by the sparse unique `openDedupeKey` index: the filter
|
|
115
|
+
// anchors on that key, so two writers racing the same open occurrence either update
|
|
116
|
+
// it or lose the insert branch to the index. `$setOnInsert` seeds the `@unI()`
|
|
117
|
+
// identity on the insert branch only — the client refuses an identity seed without
|
|
118
|
+
// `upsert: true` and refuses a seed that contradicts an identity anchor in the
|
|
119
|
+
// filter, so the created event is addressable by `id` and immutable afterwards.
|
|
120
|
+
const result = await OpsConfigEventDoc.atomicUpdate<OpsConfigEventDoc>(
|
|
118
121
|
{ openDedupeKey: optionsArg.openDedupeKey },
|
|
119
122
|
{
|
|
120
123
|
$set: {
|
|
@@ -122,80 +125,72 @@ export class OpsConfigEventDoc extends plugins.smartdata.SmartDataDbDoc<OpsConfi
|
|
|
122
125
|
detail: optionsArg.detail,
|
|
123
126
|
context: optionsArg.context,
|
|
124
127
|
createdAt: optionsArg.createdAt,
|
|
125
|
-
_updatedAt: timestamp,
|
|
126
128
|
},
|
|
127
129
|
$setOnInsert: {
|
|
128
130
|
id: optionsArg.id,
|
|
129
131
|
category: optionsArg.category,
|
|
130
132
|
title: optionsArg.title,
|
|
131
133
|
openDedupeKey: optionsArg.openDedupeKey,
|
|
132
|
-
_createdAt: timestamp,
|
|
133
134
|
},
|
|
134
135
|
},
|
|
135
136
|
{ upsert: true },
|
|
136
137
|
);
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
};
|
|
138
|
+
const event = await OpsConfigEventDoc.getInstance<OpsConfigEventDoc>({
|
|
139
|
+
openDedupeKey: optionsArg.openDedupeKey,
|
|
140
|
+
});
|
|
141
|
+
if (!event) throw new Error('Failed to read the recorded platform configuration event');
|
|
142
|
+
return { event, created: result.upsertedId !== null };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
public static async findFiltered(
|
|
146
146
|
filter: IOpsConfigEventFilter = {},
|
|
147
147
|
): Promise<{ events: OpsConfigEventDoc[]; total: number; nextCursor?: IOpsConfigEventCursor }> {
|
|
148
|
-
const selector:
|
|
148
|
+
const selector: plugins.smartdata.MongoFilter<OpsConfigEventDoc> = {};
|
|
149
149
|
if (filter.acknowledged === true) selector.acknowledgedAt = { $gt: 0 };
|
|
150
|
-
if (filter.acknowledged === false) selector
|
|
150
|
+
if (filter.acknowledged === false) Object.assign(selector, { $or: unacknowledgedBranches() });
|
|
151
151
|
if (filter.severity) selector.severity = filter.severity;
|
|
152
152
|
if (filter.category) selector.category = filter.category;
|
|
153
153
|
const requestedLimit = Number.isSafeInteger(filter.limit) && filter.limit! > 0
|
|
154
154
|
? filter.limit!
|
|
155
155
|
: 100;
|
|
156
156
|
const limit = Math.min(requestedLimit, 200);
|
|
157
|
-
const countSelector = structuredClone(selector);
|
|
158
157
|
if (filter.cursor) {
|
|
159
158
|
if (!Number.isSafeInteger(filter.cursor.createdAt) || filter.cursor.createdAt < 0
|
|
160
159
|
|| typeof filter.cursor.id !== 'string' || !filter.cursor.id.trim()) {
|
|
161
160
|
throw new Error('Invalid platform configuration event cursor');
|
|
162
161
|
}
|
|
163
|
-
selector.$or = [
|
|
164
|
-
{ createdAt: { $lt: filter.cursor.createdAt } },
|
|
165
|
-
{ createdAt: filter.cursor.createdAt, id: { $lt: filter.cursor.id } },
|
|
166
|
-
];
|
|
167
162
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
163
|
+
// Seek pagination over (createdAt, id) with the unique id as tiebreaker, plus the
|
|
164
|
+
// unpaged total for the same filter.
|
|
165
|
+
const page = await OpsConfigEventDoc.getPagedInstances<OpsConfigEventDoc>({
|
|
166
|
+
filter: selector,
|
|
167
|
+
sortField: 'createdAt',
|
|
168
|
+
uniqueField: 'id',
|
|
169
|
+
sortDirection: 'desc',
|
|
170
|
+
limit,
|
|
171
|
+
withTotal: true,
|
|
172
|
+
...(filter.cursor
|
|
173
|
+
? { cursor: { sortValue: filter.cursor.createdAt, uniqueValue: filter.cursor.id } }
|
|
174
|
+
: {}),
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
events: page.documents,
|
|
178
|
+
total: page.total ?? 0,
|
|
179
|
+
...(page.nextCursor
|
|
180
|
+
? {
|
|
185
181
|
nextCursor: {
|
|
186
|
-
createdAt: Number(
|
|
187
|
-
id: String(
|
|
182
|
+
createdAt: Number(page.nextCursor.sortValue),
|
|
183
|
+
id: String(page.nextCursor.uniqueValue),
|
|
188
184
|
},
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
await cursor.close();
|
|
193
|
-
}
|
|
185
|
+
}
|
|
186
|
+
: {}),
|
|
187
|
+
};
|
|
194
188
|
}
|
|
195
189
|
|
|
196
190
|
public static async countUnacknowledged(): Promise<number> {
|
|
197
|
-
|
|
198
|
-
|
|
191
|
+
return await OpsConfigEventDoc.getCount<OpsConfigEventDoc>({
|
|
192
|
+
$or: unacknowledgedBranches(),
|
|
193
|
+
});
|
|
199
194
|
}
|
|
200
195
|
|
|
201
196
|
/** Acknowledge the given event ids; returns how many were newly acknowledged. */
|
|
@@ -208,16 +203,23 @@ export class OpsConfigEventDoc extends plugins.smartdata.SmartDataDbDoc<OpsConfi
|
|
|
208
203
|
}
|
|
209
204
|
const uniqueIds = Array.from(new Set(ids));
|
|
210
205
|
if (uniqueIds.length === 0) return 0;
|
|
211
|
-
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
212
206
|
const now = Date.now();
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
// The per-id `$or` is the equality anchor a plural atomic write requires; the second
|
|
208
|
+
// branch keeps the "only newly acknowledged rows count" guard.
|
|
209
|
+
const result = await OpsConfigEventDoc.atomicUpdateMany<OpsConfigEventDoc>(
|
|
210
|
+
{
|
|
211
|
+
$and: [
|
|
212
|
+
{ $or: uniqueIds.map((idArg) => ({ id: idArg })) },
|
|
213
|
+
{ $or: unacknowledgedBranches() },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
215
216
|
{
|
|
216
217
|
$set: {
|
|
217
218
|
acknowledgedBy: userId,
|
|
218
219
|
acknowledgedAt: now,
|
|
219
|
-
_updatedAt: new Date(now).toISOString(),
|
|
220
220
|
},
|
|
221
|
+
// Presence of openDedupeKey is the open-event uniqueness authority, so
|
|
222
|
+
// acknowledging must release it.
|
|
221
223
|
$unset: { openDedupeKey: '' },
|
|
222
224
|
},
|
|
223
225
|
);
|
|
@@ -228,27 +230,28 @@ export class OpsConfigEventDoc extends plugins.smartdata.SmartDataDbDoc<OpsConfi
|
|
|
228
230
|
public static async pruneAcknowledged(maxAgeMs: number): Promise<number> {
|
|
229
231
|
const cutoff = Date.now() - maxAgeMs;
|
|
230
232
|
let pruned = 0;
|
|
231
|
-
const collection = await OpsConfigEventDoc.getNativeCollection();
|
|
232
233
|
while (true) {
|
|
233
|
-
const cursor =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
234
|
+
const cursor = await OpsConfigEventDoc.getCursor<OpsConfigEventDoc>(
|
|
235
|
+
{ acknowledgedAt: { $lt: cutoff } },
|
|
236
|
+
{
|
|
237
|
+
projection: { id: 1 },
|
|
238
|
+
sort: { acknowledgedAt: 1, id: 1 },
|
|
239
|
+
limit: PRUNE_BATCH_SIZE,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
let rows: OpsConfigEventDoc[];
|
|
241
243
|
try {
|
|
242
|
-
rows = await cursor.toArray()
|
|
244
|
+
rows = await cursor.toArray();
|
|
243
245
|
} finally {
|
|
244
246
|
await cursor.close();
|
|
245
247
|
}
|
|
246
248
|
if (rows.length === 0) break;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
// Anchored on the scanned identities, with the retention guard re-evaluated
|
|
250
|
+
// server-side so an event acknowledged after the scan is never pruned early.
|
|
251
|
+
const result = await OpsConfigEventDoc.atomicDeleteMany<OpsConfigEventDoc>({
|
|
252
|
+
$or: rows.map((rowArg) => ({ id: rowArg.id })),
|
|
249
253
|
acknowledgedAt: { $lt: cutoff },
|
|
250
|
-
};
|
|
251
|
-
const result = await collection.deleteMany(deleteFilter);
|
|
254
|
+
});
|
|
252
255
|
if (result.deletedCount === 0) break;
|
|
253
256
|
pruned += result.deletedCount;
|
|
254
257
|
}
|
|
@@ -3,11 +3,26 @@ import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
|
3
3
|
|
|
4
4
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* SmartMTA's durable key/value space.
|
|
8
|
+
*
|
|
9
|
+
* Every document is content-addressed: `id` is `sha256(normalizedKey)` and owns the stored
|
|
10
|
+
* `_id`, so the primary key — not a secondary index — is the uniqueness authority that makes
|
|
11
|
+
* a write a single race-free upsert. That is what `identityAsDocumentId: 'id'` declares, and
|
|
12
|
+
* it holds for every row dcrouter ever wrote here, because the manager has always derived
|
|
13
|
+
* both `_id` and `id` from the same hash.
|
|
14
|
+
*
|
|
15
|
+
* The declaration is irreversible: from here on reads are answered through `_id`, so a row
|
|
16
|
+
* whose `_id` is not its identity would no longer resolve, and the client refuses a unique
|
|
17
|
+
* index on `id` because the primary key is the only uniqueness authority a document-id
|
|
18
|
+
* identity may have.
|
|
19
|
+
*/
|
|
20
|
+
@plugins.smartdata.Collection(() => getDb(), { identityAsDocumentId: 'id' })
|
|
7
21
|
export class SmartMtaStorageDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
8
22
|
SmartMtaStorageDoc,
|
|
9
23
|
SmartMtaStorageDoc
|
|
10
24
|
> {
|
|
25
|
+
@plugins.smartdata.unI()
|
|
11
26
|
@plugins.smartdata.svDb()
|
|
12
27
|
public id!: string;
|
|
13
28
|
|
|
@@ -9,6 +9,20 @@ const getDb = () => DcRouterDb.getInstance().getDb();
|
|
|
9
9
|
* A credential rotation must not reset these counters, so the stable binding
|
|
10
10
|
* id and lifecycle generation are the authority rather than a credential id.
|
|
11
11
|
*/
|
|
12
|
+
// Both indexes are declared under the exact names the released Web Push schema migration
|
|
13
|
+
// created them with (`ts_migrations/index.ts` via `ts/webpush/webpush-indexes.ts`), so model
|
|
14
|
+
// initialization verifies the existing topology instead of installing a second, differently
|
|
15
|
+
// named index over the same key.
|
|
16
|
+
@plugins.smartdata.namedIndex({
|
|
17
|
+
name: 'web_push_admission_binding_unique',
|
|
18
|
+
key: { bindingId: 1, lifecycleGeneration: 1 },
|
|
19
|
+
options: { unique: true },
|
|
20
|
+
})
|
|
21
|
+
@plugins.smartdata.namedIndex({
|
|
22
|
+
name: 'web_push_admission_ttl',
|
|
23
|
+
key: { purgeAt: 1 },
|
|
24
|
+
options: { expireAfterSeconds: 0 },
|
|
25
|
+
})
|
|
12
26
|
@plugins.smartdata.Collection(() => getDb())
|
|
13
27
|
export class WebPushAdmissionDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
14
28
|
WebPushAdmissionDoc,
|
|
@@ -40,9 +54,4 @@ export class WebPushAdmissionDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
40
54
|
|
|
41
55
|
@plugins.smartdata.svDb()
|
|
42
56
|
public purgeAt!: Date;
|
|
43
|
-
|
|
44
|
-
public static getNativeCollection():
|
|
45
|
-
plugins.smartdata.SmartdataCollection<WebPushAdmissionDoc>['mongoDbCollection'] {
|
|
46
|
-
return getDb().mongoDb.collection('WebPushAdmissionDoc');
|
|
47
|
-
}
|
|
48
57
|
}
|
|
@@ -47,6 +47,53 @@ export interface IWebPushStoredControllerFence {
|
|
|
47
47
|
|
|
48
48
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
49
49
|
|
|
50
|
+
// Every index is declared under the exact name the released Web Push schema migration created
|
|
51
|
+
// it with (`ts_migrations/index.ts` via `ts/webpush/webpush-indexes.ts`), so model initialization
|
|
52
|
+
// verifies the existing topology instead of installing a second, differently named index over the
|
|
53
|
+
// same key. `test/test.webpush-persisted-bytes.node.ts` keeps both declarations in lockstep.
|
|
54
|
+
@plugins.smartdata.namedIndex({
|
|
55
|
+
name: 'web_push_binding_id_unique',
|
|
56
|
+
key: { id: 1 },
|
|
57
|
+
options: { unique: true },
|
|
58
|
+
})
|
|
59
|
+
@plugins.smartdata.namedIndex({
|
|
60
|
+
name: 'web_push_binding_owner_unique',
|
|
61
|
+
key: { ownerGatewayClientType: 1, ownerGatewayClientId: 1, ownerAppInstanceId: 1 },
|
|
62
|
+
options: { unique: true },
|
|
63
|
+
})
|
|
64
|
+
@plugins.smartdata.namedIndex({
|
|
65
|
+
name: 'web_push_binding_credential_unique',
|
|
66
|
+
key: { 'credential.id': 1 },
|
|
67
|
+
options: { unique: true },
|
|
68
|
+
})
|
|
69
|
+
@plugins.smartdata.namedIndex({
|
|
70
|
+
name: 'web_push_binding_previous_credential_lookup',
|
|
71
|
+
key: { 'previousCredentials.id': 1 },
|
|
72
|
+
})
|
|
73
|
+
@plugins.smartdata.namedIndex({
|
|
74
|
+
name: 'web_push_binding_previous_credential_cleanup',
|
|
75
|
+
key: { 'previousCredentials.validUntil': 1 },
|
|
76
|
+
})
|
|
77
|
+
@plugins.smartdata.namedIndex({
|
|
78
|
+
name: 'web_push_binding_rotation_recovery_cleanup',
|
|
79
|
+
key: { 'credentialRotationRecovery.expiresAt': 1 },
|
|
80
|
+
})
|
|
81
|
+
@plugins.smartdata.namedIndex({
|
|
82
|
+
name: 'web_push_binding_vapid_status_cleanup',
|
|
83
|
+
key: { 'vapidKeys.status': 1 },
|
|
84
|
+
})
|
|
85
|
+
@plugins.smartdata.namedIndex({
|
|
86
|
+
name: 'web_push_binding_vapid_retire_after_cleanup',
|
|
87
|
+
key: { 'vapidKeys.retireAfter': 1 },
|
|
88
|
+
})
|
|
89
|
+
@plugins.smartdata.namedIndex({
|
|
90
|
+
name: 'web_push_binding_disabled_cleanup',
|
|
91
|
+
key: { enabled: 1, lifecycleGeneration: 1 },
|
|
92
|
+
})
|
|
93
|
+
@plugins.smartdata.namedIndex({
|
|
94
|
+
name: 'web_push_binding_deleted_cleanup',
|
|
95
|
+
key: { deletedAt: 1, lifecycleGeneration: 1 },
|
|
96
|
+
})
|
|
50
97
|
@plugins.smartdata.Collection(() => getDb())
|
|
51
98
|
export class WebPushBindingDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
52
99
|
WebPushBindingDoc,
|
|
@@ -70,7 +117,8 @@ export class WebPushBindingDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
70
117
|
@plugins.smartdata.svDb()
|
|
71
118
|
public status!: plugins.servezoneInterfaces.data.TWebPushBindingStatus;
|
|
72
119
|
|
|
73
|
-
|
|
120
|
+
/** `credential.id` is the compare-and-set anchor of credential rotation and a unique index. */
|
|
121
|
+
@plugins.smartdata.svDb({ atomicPaths: ['id'] })
|
|
74
122
|
public credential!: IWebPushStoredCredential;
|
|
75
123
|
|
|
76
124
|
/** At most one still-valid overlap credential; expired entries are scrubbed. */
|
|
@@ -78,7 +126,10 @@ export class WebPushBindingDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
78
126
|
public previousCredentials: IWebPushStoredCredential[] = [];
|
|
79
127
|
|
|
80
128
|
/** MUST BE ABSENT outside the bounded idempotent credential-rotation replay window. */
|
|
81
|
-
@plugins.smartdata.svDb(
|
|
129
|
+
@plugins.smartdata.svDb({
|
|
130
|
+
// Expiry cleanup fences on the exact recovery record it observed.
|
|
131
|
+
atomicPaths: ['requestedCredentialId', 'resultingCredentialId', 'expiresAt'],
|
|
132
|
+
})
|
|
82
133
|
public credentialRotationRecovery?: IWebPushCredentialRotationRecovery;
|
|
83
134
|
|
|
84
135
|
@plugins.smartdata.svDb()
|
|
@@ -107,11 +158,6 @@ export class WebPushBindingDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
107
158
|
@plugins.smartdata.svDb()
|
|
108
159
|
public deletedAt?: number;
|
|
109
160
|
|
|
110
|
-
public static getNativeCollection():
|
|
111
|
-
plugins.smartdata.SmartdataCollection<WebPushBindingDoc>['mongoDbCollection'] {
|
|
112
|
-
return getDb().mongoDb.collection('WebPushBindingDoc');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
161
|
public override async createSavableObject(): Promise<WebPushBindingDoc> {
|
|
116
162
|
const savable = await super.createSavableObject();
|
|
117
163
|
if (savable.credential.secretHash === undefined) {
|
|
@@ -4,6 +4,45 @@ import type { IWebPushEncryptedEnvelope } from '../../webpush/classes.webpush-cr
|
|
|
4
4
|
|
|
5
5
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
6
6
|
|
|
7
|
+
// Every index is declared under the exact name the released Web Push schema migration created
|
|
8
|
+
// it with (`ts_migrations/index.ts` via `ts/webpush/webpush-indexes.ts`), so model initialization
|
|
9
|
+
// verifies the existing topology instead of installing a second, differently named index over the
|
|
10
|
+
// same key. `test/test.webpush-persisted-bytes.node.ts` keeps both declarations in lockstep.
|
|
11
|
+
@plugins.smartdata.namedIndex({
|
|
12
|
+
name: 'web_push_spool_id_unique',
|
|
13
|
+
key: { id: 1 },
|
|
14
|
+
options: { unique: true },
|
|
15
|
+
})
|
|
16
|
+
@plugins.smartdata.namedIndex({
|
|
17
|
+
name: 'web_push_spool_idempotency_unique',
|
|
18
|
+
key: { bindingId: 1, lifecycleGeneration: 1, credentialId: 1, idempotencyKeyDigest: 1 },
|
|
19
|
+
options: { unique: true },
|
|
20
|
+
})
|
|
21
|
+
@plugins.smartdata.namedIndex({
|
|
22
|
+
name: 'web_push_spool_delivery_scan',
|
|
23
|
+
key: { state: 1, nextAttemptAt: 1, leaseExpiresAt: 1 },
|
|
24
|
+
})
|
|
25
|
+
@plugins.smartdata.namedIndex({
|
|
26
|
+
name: 'web_push_spool_cancellation_scan',
|
|
27
|
+
key: { bindingId: 1, lifecycleGeneration: 1, subscriptionId: 1, state: 1 },
|
|
28
|
+
})
|
|
29
|
+
@plugins.smartdata.namedIndex({
|
|
30
|
+
name: 'web_push_spool_cancelled_lease_scan',
|
|
31
|
+
key: { state: 1, cancelRequestedAt: 1, leaseExpiresAt: 1, id: 1 },
|
|
32
|
+
})
|
|
33
|
+
@plugins.smartdata.namedIndex({
|
|
34
|
+
name: 'web_push_spool_lifecycle_drain_scan',
|
|
35
|
+
key: { bindingId: 1, lifecycleGeneration: 1, state: 1, leaseExpiresAt: 1 },
|
|
36
|
+
})
|
|
37
|
+
@plugins.smartdata.namedIndex({
|
|
38
|
+
name: 'web_push_spool_vapid_retirement_scan',
|
|
39
|
+
key: { bindingId: 1, lifecycleGeneration: 1, vapidKeyId: 1, state: 1 },
|
|
40
|
+
})
|
|
41
|
+
@plugins.smartdata.namedIndex({
|
|
42
|
+
name: 'web_push_spool_terminal_ttl',
|
|
43
|
+
key: { purgeAt: 1 },
|
|
44
|
+
options: { expireAfterSeconds: 0 },
|
|
45
|
+
})
|
|
7
46
|
@plugins.smartdata.Collection(() => getDb())
|
|
8
47
|
export class WebPushSpoolDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
9
48
|
WebPushSpoolDoc,
|
|
@@ -106,11 +145,6 @@ export class WebPushSpoolDoc extends plugins.smartdata.SmartDataDbDoc<
|
|
|
106
145
|
@plugins.smartdata.svDb()
|
|
107
146
|
public purgeAt?: Date;
|
|
108
147
|
|
|
109
|
-
public static getNativeCollection():
|
|
110
|
-
plugins.smartdata.SmartdataCollection<WebPushSpoolDoc>['mongoDbCollection'] {
|
|
111
|
-
return getDb().mongoDb.collection('WebPushSpoolDoc');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
148
|
public override async createSavableObject(): Promise<WebPushSpoolDoc> {
|
|
115
149
|
const savable = await super.createSavableObject();
|
|
116
150
|
for (const field of [
|
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -1501,7 +1501,9 @@ export class DnsManager {
|
|
|
1501
1501
|
rec.providerRecordId = undefined;
|
|
1502
1502
|
rec.updatedAt = now;
|
|
1503
1503
|
await rec.save({ session });
|
|
1504
|
-
|
|
1504
|
+
// save() persists set fields only, so the provider link is removed with an
|
|
1505
|
+
// explicit atomic $unset inside the same transaction.
|
|
1506
|
+
await DnsRecordDoc.atomicUpdate<DnsRecordDoc>(
|
|
1505
1507
|
{ id: rec.id },
|
|
1506
1508
|
{ $unset: { providerRecordId: '' } },
|
|
1507
1509
|
{ session },
|
|
@@ -1520,7 +1522,9 @@ export class DnsManager {
|
|
|
1520
1522
|
domain.updatedAt = now;
|
|
1521
1523
|
domain.authoritative = ownership.verified;
|
|
1522
1524
|
await domain.save({ session });
|
|
1523
|
-
|
|
1525
|
+
// Same reason as above: the provider-managed fields must disappear from the
|
|
1526
|
+
// stored document, which only an atomic $unset expresses.
|
|
1527
|
+
await DomainDoc.atomicUpdate<DomainDoc>(
|
|
1524
1528
|
{ id: domain.id },
|
|
1525
1529
|
{
|
|
1526
1530
|
$unset: {
|