@twin.org/auditable-item-stream-service 0.0.3-next.9 → 0.9.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 (39) hide show
  1. package/README.md +2 -2
  2. package/dist/es/auditableItemStreamRoutes.js +401 -73
  3. package/dist/es/auditableItemStreamRoutes.js.map +1 -1
  4. package/dist/es/auditableItemStreamService.js +304 -113
  5. package/dist/es/auditableItemStreamService.js.map +1 -1
  6. package/dist/es/entities/auditableItemStream.js +17 -3
  7. package/dist/es/entities/auditableItemStream.js.map +1 -1
  8. package/dist/es/models/IAuditableItemStreamServiceConfig.js.map +1 -1
  9. package/dist/es/models/IAuditableItemStreamServiceConstructorOptions.js.map +1 -1
  10. package/dist/es/models/IAuditableItemStreamServiceContext.js.map +1 -1
  11. package/dist/es/restEntryPoints.js +3 -0
  12. package/dist/es/restEntryPoints.js.map +1 -1
  13. package/dist/types/auditableItemStreamRoutes.d.ts +37 -5
  14. package/dist/types/auditableItemStreamService.d.ts +37 -38
  15. package/dist/types/entities/auditableItemStream.d.ts +10 -1
  16. package/dist/types/models/IAuditableItemStreamServiceConfig.d.ts +4 -0
  17. package/dist/types/models/IAuditableItemStreamServiceConstructorOptions.d.ts +4 -0
  18. package/dist/types/models/IAuditableItemStreamServiceContext.d.ts +4 -0
  19. package/dist/types/restEntryPoints.d.ts +3 -0
  20. package/docs/changelog.md +396 -92
  21. package/docs/examples.md +211 -1
  22. package/docs/open-api/spec.json +832 -169
  23. package/docs/reference/classes/AuditableItemStream.md +30 -14
  24. package/docs/reference/classes/AuditableItemStreamEntry.md +13 -13
  25. package/docs/reference/classes/AuditableItemStreamService.md +109 -84
  26. package/docs/reference/functions/auditableItemStreamClose.md +31 -0
  27. package/docs/reference/functions/auditableItemStreamDeleteEntry.md +1 -1
  28. package/docs/reference/functions/auditableItemStreamList.md +1 -1
  29. package/docs/reference/functions/auditableItemStreamListEntries.md +1 -1
  30. package/docs/reference/functions/auditableItemStreamListEntriesNoStream.md +31 -0
  31. package/docs/reference/functions/auditableItemStreamListEntryObjects.md +1 -1
  32. package/docs/reference/functions/auditableItemStreamListEntryObjectsNoStream.md +31 -0
  33. package/docs/reference/functions/auditableItemStreamRemoveProof.md +31 -0
  34. package/docs/reference/index.md +4 -0
  35. package/docs/reference/interfaces/IAuditableItemStreamServiceConfig.md +10 -2
  36. package/docs/reference/interfaces/IAuditableItemStreamServiceConstructorOptions.md +18 -10
  37. package/docs/reference/variables/restEntryPoints.md +2 -0
  38. package/locales/en.json +6 -1
  39. package/package.json +19 -18
@@ -1,13 +1,15 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
- import { AuditableItemStreamContexts, AuditableItemStreamTopics, AuditableItemStreamTypes } from "@twin.org/auditable-item-stream-models";
4
- import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
5
- import { Coerce, ComponentFactory, GeneralError, Guards, Is, NotFoundError, ObjectHelper, RandomHelper, Urn, Validation } from "@twin.org/core";
6
- import { JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
3
+ import { AuditableItemStreamContexts, AuditableItemStreamDataTypes, AuditableItemStreamMetricIds, AuditableItemStreamMetrics, AuditableItemStreamModes, AuditableItemStreamTopics, AuditableItemStreamTypes } from "@twin.org/auditable-item-stream-models";
4
+ import { ContextIdHelper, ContextIdKeys, ContextIdStore } from "@twin.org/context";
5
+ import { Coerce, ComponentFactory, GeneralError, Guards, Is, Mutex, NotFoundError, ObjectHelper, RandomHelper, Urn, Validation } from "@twin.org/core";
6
+ import { DataTypeHelper } from "@twin.org/data-core";
7
+ import { JsonLdDataTypes, JsonLdHelper, JsonLdProcessor } from "@twin.org/data-json-ld";
7
8
  import { ComparisonOperator, LogicalOperator, SortDirection } from "@twin.org/entity";
8
9
  import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
9
- import { ImmutableProofContexts } from "@twin.org/immutable-proof-models";
10
+ import { ImmutableProofContexts, ImmutableProofDataTypes } from "@twin.org/immutable-proof-models";
10
11
  import { SchemaOrgContexts, SchemaOrgDataTypes, SchemaOrgTypes } from "@twin.org/standards-schema-org";
12
+ import { MetricHelper } from "@twin.org/telemetry-models";
11
13
  /**
12
14
  * Class for performing auditable item stream operations.
13
15
  */
@@ -68,11 +70,21 @@ export class AuditableItemStreamService {
68
70
  * @internal
69
71
  */
70
72
  _eventBusComponent;
73
+ /**
74
+ * The telemetry component.
75
+ * @internal
76
+ */
77
+ _telemetryComponent;
71
78
  /**
72
79
  * The default interval for the integrity checks.
73
80
  * @internal
74
81
  */
75
82
  _defaultImmutableInterval;
83
+ /**
84
+ * The timeout in milliseconds when acquiring a mutex lock.
85
+ * @internal
86
+ */
87
+ _mutexTimeoutMs;
76
88
  /**
77
89
  * Create a new instance of AuditableItemStreamService.
78
90
  * @param options The dependencies for the auditable item stream connector.
@@ -84,9 +96,14 @@ export class AuditableItemStreamService {
84
96
  if (Is.stringValue(options?.eventBusComponentType)) {
85
97
  this._eventBusComponent = ComponentFactory.get(options.eventBusComponentType);
86
98
  }
99
+ this._telemetryComponent = ComponentFactory.getIfExists(options?.telemetryComponentType);
87
100
  this._config = options?.config ?? {};
88
101
  this._defaultImmutableInterval = this._config.defaultImmutableInterval ?? 10;
102
+ this._mutexTimeoutMs = Coerce.integer(options?.config?.mutexTimeoutMs);
89
103
  SchemaOrgDataTypes.registerRedirects();
104
+ AuditableItemStreamDataTypes.registerTypes();
105
+ JsonLdDataTypes.registerTypes();
106
+ ImmutableProofDataTypes.registerTypes();
90
107
  }
91
108
  /**
92
109
  * Returns the class name of the component.
@@ -95,49 +112,59 @@ export class AuditableItemStreamService {
95
112
  className() {
96
113
  return AuditableItemStreamService.CLASS_NAME;
97
114
  }
115
+ /**
116
+ * Register all AIS metrics with the telemetry component.
117
+ * @returns A promise that resolves when the metrics have been registered.
118
+ */
119
+ async start() {
120
+ if (Is.undefined(this._telemetryComponent)) {
121
+ return;
122
+ }
123
+ await MetricHelper.createMetrics(this._telemetryComponent, AuditableItemStreamMetrics);
124
+ }
98
125
  /**
99
126
  * Create a new stream.
100
127
  * @param stream The stream to create.
101
- * @param stream.annotationObject The object for the stream as JSON-LD.
102
- * @param stream.entries Entries to store in the stream.
103
- * @param options Options for creating the stream.
104
- * @param options.immutableInterval After how many entries do we add immutable checks, defaults to service configured value.
105
- * A value of 0 will disable integrity checks, 1 will be every item, or any other integer for an interval.
106
128
  * @returns The id of the new stream item.
107
129
  */
108
- async create(stream, options) {
130
+ async create(stream) {
109
131
  Guards.object(AuditableItemStreamService.CLASS_NAME, "stream", stream);
110
132
  const contextIds = await ContextIdStore.getContextIds();
133
+ ContextIdHelper.guard(contextIds, ContextIdKeys.Organization);
111
134
  try {
135
+ const ownerOrganizationId = contextIds[ContextIdKeys.UserOrganization] ?? contextIds[ContextIdKeys.Organization];
136
+ const id = RandomHelper.generateUuidV7("compact");
137
+ const schemaValidationFailures = [];
138
+ await DataTypeHelper.validate("stream", `${AuditableItemStreamContexts.Namespace}${AuditableItemStreamTypes.Stream}Base`, stream, schemaValidationFailures);
139
+ Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream", schemaValidationFailures);
140
+ if (stream.closed && !Is.arrayValue(stream.entries?.[SchemaOrgTypes.ItemListElement])) {
141
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "closedRequiresEntries");
142
+ }
112
143
  if (Is.object(stream.annotationObject)) {
113
144
  const validationFailures = [];
114
145
  await JsonLdHelper.validate(stream.annotationObject, validationFailures);
115
146
  Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream.annotationObject", validationFailures);
116
147
  }
117
- const id = RandomHelper.generateUuidV7("compact");
118
148
  const context = {
119
149
  now: new Date(Date.now()).toISOString(),
120
150
  contextIds,
121
151
  indexCounter: 0,
122
- immutableInterval: options?.immutableInterval ?? this._defaultImmutableInterval
152
+ immutableInterval: stream?.immutableInterval ?? this._defaultImmutableInterval,
153
+ organizationIdentity: ownerOrganizationId
123
154
  };
124
155
  const streamEntity = {
125
156
  id,
126
- organizationIdentity: contextIds?.[ContextIdKeys.Organization],
157
+ organizationIdentity: ownerOrganizationId,
127
158
  userIdentity: contextIds?.[ContextIdKeys.User],
128
159
  dateCreated: context.now,
129
160
  immutableInterval: context.immutableInterval,
161
+ closed: stream.closed,
162
+ mode: stream.mode,
130
163
  numberOfItems: 0
131
164
  };
132
- // Create the JSON-LD object we want to use for the proof
133
- // this is a subset of fixed properties from the stream object.
134
- const streamModel = this.streamEntityToJsonLd(ObjectHelper.pick(streamEntity, AuditableItemStreamService._PROOF_KEYS_STREAM));
135
- // Create the proof for the stream object
136
- if (context.immutableInterval > 0) {
137
- streamEntity.proofId = await this._immutableProofComponent.create(streamModel);
138
- }
139
- if (Is.arrayValue(stream.entries)) {
140
- for (const entry of stream.entries) {
165
+ const streamUrn = await this.createStreamProof(streamEntity, context.immutableInterval);
166
+ if (Is.arrayValue(stream.entries?.[SchemaOrgTypes.ItemListElement])) {
167
+ for (const entry of stream.entries[SchemaOrgTypes.ItemListElement]) {
141
168
  await this.setEntry(context, id, entry);
142
169
  }
143
170
  }
@@ -146,25 +173,23 @@ export class AuditableItemStreamService {
146
173
  streamEntity.annotationObject = stream.annotationObject;
147
174
  streamEntity.numberOfItems = context.indexCounter;
148
175
  await this._streamStorage.set(streamEntity);
149
- await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamCreated, { id: streamModel.id });
150
- return streamModel.id;
176
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsCreated, {
177
+ mode: streamEntity.mode ?? AuditableItemStreamModes.Default,
178
+ immutableInterval: context.immutableInterval
179
+ });
180
+ await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamCreated, { id: streamUrn });
181
+ return streamUrn;
151
182
  }
152
183
  catch (error) {
153
184
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "createFailed", undefined, error);
154
185
  }
155
186
  }
156
187
  /**
157
- * Get a stream header without the entries.
158
- * @param id The id of the stream to get.
159
- * @param options Additional options for the get operation.
160
- * @param options.includeEntries Whether to include the entries, defaults to false.
161
- * @param options.includeDeleted Whether to include deleted entries, defaults to false.
162
- * @param options.verifyStream Should the stream be verified, defaults to false.
163
- * @param options.verifyEntries Should the entries be verified, defaults to false.
164
- * @returns The stream and entries if found.
165
- * @throws NotFoundError if the stream is not found
188
+ * Close a stream.
189
+ * @param id The id of the stream to close.
190
+ * @returns A promise that resolves when the stream has been closed.
166
191
  */
167
- async get(id, options) {
192
+ async close(id) {
168
193
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "id", id);
169
194
  const urnParsed = Urn.fromValidString(id);
170
195
  if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
@@ -173,39 +198,32 @@ export class AuditableItemStreamService {
173
198
  id
174
199
  });
175
200
  }
201
+ const streamId = urnParsed.namespaceSpecific(0);
202
+ await Mutex.lock(streamId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
176
203
  try {
177
- const streamId = urnParsed.namespaceSpecific(0);
178
204
  const streamEntity = await this._streamStorage.get(streamId);
179
205
  if (Is.empty(streamEntity)) {
180
206
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", id);
181
207
  }
182
- const verifyStream = options?.verifyStream ?? false;
183
- const verifyEntries = options?.verifyEntries ?? false;
184
- const streamModel = this.streamEntityToJsonLd(streamEntity);
185
- if (options?.includeEntries) {
186
- const result = await this.findEntries(streamId, options?.includeDeleted, verifyEntries);
187
- streamModel.entries = result.entries;
188
- streamModel.cursor = result.cursor;
189
- }
190
- if (verifyStream && Is.stringValue(streamEntity.proofId)) {
191
- streamModel.verification = await this._immutableProofComponent.verify(streamEntity.proofId);
192
- }
193
- if (verifyStream || verifyEntries) {
194
- streamModel["@context"].push(ImmutableProofContexts.Context);
208
+ if (!streamEntity.closed) {
209
+ streamEntity.closed = true;
210
+ streamEntity.dateModified = new Date(Date.now()).toISOString();
211
+ await this._streamStorage.set(streamEntity);
212
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsClosed);
213
+ await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamUpdated, { id });
195
214
  }
196
- const result = await JsonLdProcessor.compact(streamModel, streamModel["@context"]);
197
- return result;
198
215
  }
199
216
  catch (error) {
200
- throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "getFailed", undefined, error);
217
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "closeFailed", undefined, error);
218
+ }
219
+ finally {
220
+ Mutex.unlock(streamId);
201
221
  }
202
222
  }
203
223
  /**
204
224
  * Update a stream.
205
- * @param stream The stream to update.
206
- * @param stream.id The id of the stream to update.
207
- * @param stream.annotationObject The object for the stream as JSON-LD.
208
- * @returns Nothing.
225
+ * @param stream The stream to update, does not update entries.
226
+ * @returns A promise that resolves when the stream has been updated.
209
227
  */
210
228
  async update(stream) {
211
229
  Guards.object(AuditableItemStreamService.CLASS_NAME, "stream", stream);
@@ -217,8 +235,12 @@ export class AuditableItemStreamService {
217
235
  id: stream.id
218
236
  });
219
237
  }
238
+ const streamId = urnParsed.namespaceSpecific(0);
239
+ await Mutex.lock(streamId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
220
240
  try {
221
- const streamId = urnParsed.namespaceSpecific(0);
241
+ const schemaValidationFailures = [];
242
+ await DataTypeHelper.validate("stream", `${AuditableItemStreamContexts.Namespace}${AuditableItemStreamTypes.Stream}`, stream, schemaValidationFailures);
243
+ Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream", schemaValidationFailures);
222
244
  const streamEntity = await this._streamStorage.get(streamId);
223
245
  if (Is.empty(streamEntity)) {
224
246
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", stream.id);
@@ -228,21 +250,99 @@ export class AuditableItemStreamService {
228
250
  await JsonLdHelper.validate(stream.annotationObject, validationFailures);
229
251
  Validation.asValidationError(AuditableItemStreamService.CLASS_NAME, "stream.annotationObject", validationFailures);
230
252
  }
253
+ let changed = false;
231
254
  if (!ObjectHelper.equal(streamEntity.annotationObject, stream.annotationObject, false)) {
232
255
  streamEntity.annotationObject = stream.annotationObject;
256
+ changed = true;
257
+ }
258
+ const contextIds = await ContextIdStore.getContextIds();
259
+ const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
260
+ if (!Is.stringValue(streamEntity.organizationIdentity) &&
261
+ Is.stringValue(ownerOrganizationId)) {
262
+ streamEntity.organizationIdentity = ownerOrganizationId;
263
+ changed = true;
264
+ }
265
+ if (!Is.stringValue(streamEntity.proofId) &&
266
+ Is.stringValue(streamEntity.organizationIdentity)) {
267
+ await this.createStreamProof(streamEntity, streamEntity.immutableInterval ?? this._defaultImmutableInterval);
268
+ if (Is.stringValue(streamEntity.proofId)) {
269
+ changed = true;
270
+ }
271
+ }
272
+ if (changed) {
233
273
  streamEntity.dateModified = new Date(Date.now()).toISOString();
234
274
  await this._streamStorage.set(streamEntity);
275
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsUpdated);
235
276
  await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamUpdated, { id: stream.id });
236
277
  }
237
278
  }
238
279
  catch (error) {
239
280
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "updatingFailed", undefined, error);
240
281
  }
282
+ finally {
283
+ Mutex.unlock(streamId);
284
+ }
285
+ }
286
+ /**
287
+ * Get a stream header without the entries.
288
+ * @param id The id of the stream to get.
289
+ * @param cursor Cursor to use for next chunk of entries.
290
+ * @param limit Limit the number of entries to return, only applicable if includeEntries is true.
291
+ * @param options Additional options for the get operation.
292
+ * @param options.includeEntries Whether to include the entries, defaults to false.
293
+ * @param options.includeDeleted Whether to include deleted entries, defaults to false.
294
+ * @param options.verifyStream Should the stream be verified, defaults to false.
295
+ * @param options.verifyEntries Should the entries be verified, defaults to false.
296
+ * @returns The stream and entries if found.
297
+ * @throws NotFoundError if the stream is not found
298
+ */
299
+ async get(id, cursor, limit, options) {
300
+ Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "id", id);
301
+ const urnParsed = Urn.fromValidString(id);
302
+ if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
303
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
304
+ namespace: AuditableItemStreamService._NAMESPACE,
305
+ id
306
+ });
307
+ }
308
+ try {
309
+ const streamId = urnParsed.namespaceSpecific(0);
310
+ const streamEntity = await this._streamStorage.get(streamId);
311
+ if (Is.empty(streamEntity)) {
312
+ throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", id);
313
+ }
314
+ const verifyStream = options?.verifyStream ?? false;
315
+ const verifyEntries = options?.verifyEntries ?? false;
316
+ const streamModel = this.streamEntityToJsonLd(streamEntity);
317
+ let returnCursor;
318
+ if (options?.includeEntries) {
319
+ const result = await this.findEntries(streamId, options?.includeDeleted, verifyEntries, undefined, undefined, undefined, limit, cursor);
320
+ streamModel.entries = {
321
+ type: SchemaOrgTypes.ItemList,
322
+ [SchemaOrgTypes.ItemListElement]: result.entries
323
+ };
324
+ returnCursor = result.cursor;
325
+ }
326
+ if (verifyStream && Is.stringValue(streamEntity.proofId)) {
327
+ streamModel.verification = await this._immutableProofComponent.verify(streamEntity.proofId);
328
+ }
329
+ if (verifyStream || verifyEntries) {
330
+ streamModel["@context"].push(ImmutableProofContexts.Context);
331
+ }
332
+ const result = await JsonLdProcessor.compact(streamModel, streamModel["@context"]);
333
+ return {
334
+ stream: result,
335
+ cursor: returnCursor
336
+ };
337
+ }
338
+ catch (error) {
339
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "getFailed", undefined, error);
340
+ }
241
341
  }
242
342
  /**
243
343
  * Delete the stream.
244
344
  * @param id The id of the stream to remove.
245
- * @returns Nothing.
345
+ * @returns A promise that resolves when the stream has been removed.
246
346
  */
247
347
  async remove(id) {
248
348
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "id", id);
@@ -253,19 +353,24 @@ export class AuditableItemStreamService {
253
353
  id
254
354
  });
255
355
  }
356
+ const streamId = urnParsed.namespaceSpecific(0);
357
+ await Mutex.lock(streamId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
256
358
  try {
257
- const streamId = urnParsed.namespaceSpecific(0);
258
359
  const streamEntity = await this._streamStorage.get(streamId);
259
360
  if (Is.empty(streamEntity)) {
260
361
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", id);
261
362
  }
262
363
  await this.internalRemoveEntries(streamEntity, false);
263
364
  await this._streamStorage.remove(streamEntity.id);
365
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.StreamsDeleted);
264
366
  await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamDeleted, { id });
265
367
  }
266
368
  catch (error) {
267
369
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removingFailed", undefined, error);
268
370
  }
371
+ finally {
372
+ Mutex.unlock(streamId);
373
+ }
269
374
  }
270
375
  /**
271
376
  * Query all the streams, will not return entries.
@@ -339,17 +444,25 @@ export class AuditableItemStreamService {
339
444
  id: streamId
340
445
  });
341
446
  }
447
+ const streamIdParts = urnParsed.namespaceSpecific(0);
448
+ await Mutex.lock(streamIdParts, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
342
449
  try {
343
- const streamIdParts = urnParsed.namespaceSpecific(0);
344
450
  const streamEntity = await this._streamStorage.get(streamIdParts);
345
451
  if (Is.empty(streamEntity)) {
346
452
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamIdParts);
347
453
  }
454
+ if (streamEntity.closed) {
455
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ClosedStreamRejections, { operation: "createEntry" });
456
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "streamClosed", {
457
+ id: streamId
458
+ });
459
+ }
348
460
  const context = {
349
461
  now: new Date(Date.now()).toISOString(),
350
462
  contextIds,
351
463
  indexCounter: streamEntity.numberOfItems,
352
- immutableInterval: streamEntity.immutableInterval
464
+ immutableInterval: streamEntity.immutableInterval,
465
+ organizationIdentity: streamEntity.organizationIdentity ?? contextIds?.[ContextIdKeys.Organization]
353
466
  };
354
467
  const createdId = await this.setEntry(context, streamEntity.id, {
355
468
  entryObject
@@ -357,6 +470,10 @@ export class AuditableItemStreamService {
357
470
  streamEntity.dateModified = context.now;
358
471
  streamEntity.numberOfItems = context.indexCounter;
359
472
  await this._streamStorage.set(streamEntity);
473
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.EntriesCreated, {
474
+ hasProof: context.immutableInterval > 0 &&
475
+ (context.indexCounter - 1) % context.immutableInterval === 0
476
+ });
360
477
  const fullId = new Urn(AuditableItemStreamService._NAMESPACE, [
361
478
  streamEntity.id,
362
479
  createdId
@@ -367,6 +484,9 @@ export class AuditableItemStreamService {
367
484
  catch (error) {
368
485
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "creatingEntryFailed", undefined, error);
369
486
  }
487
+ finally {
488
+ Mutex.unlock(streamIdParts);
489
+ }
370
490
  }
371
491
  /**
372
492
  * Get the entry from the stream.
@@ -408,7 +528,7 @@ export class AuditableItemStreamService {
408
528
  }
409
529
  const entry = this.streamEntryEntityToJsonLd(result.entity);
410
530
  if (verifyEntry) {
411
- entry["@context"].push(ImmutableProofContexts.Context);
531
+ entry["@context"] = JsonLdProcessor.combineContexts(entry["@context"], ImmutableProofContexts.Context);
412
532
  entry.verification = result.verification;
413
533
  }
414
534
  const result2 = await JsonLdProcessor.compact(entry, entry["@context"]);
@@ -464,12 +584,11 @@ export class AuditableItemStreamService {
464
584
  * @param streamId The id of the stream to update.
465
585
  * @param entryId The id of the entry to update.
466
586
  * @param entryObject The object for the entry as JSON-LD.
467
- * @returns Nothing.
587
+ * @returns A promise that resolves when the entry has been updated.
468
588
  */
469
589
  async updateEntry(streamId, entryId, entryObject) {
470
590
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
471
591
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "entryId", entryId);
472
- const contextIds = await ContextIdStore.getContextIds();
473
592
  const urnParsed = Urn.fromValidString(streamId);
474
593
  if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
475
594
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
@@ -484,8 +603,9 @@ export class AuditableItemStreamService {
484
603
  id: entryId
485
604
  });
486
605
  }
606
+ const streamNamespaceId = urnParsed.namespaceSpecific(0);
607
+ await Mutex.lock(streamNamespaceId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
487
608
  try {
488
- const streamNamespaceId = urnParsed.namespaceMethod();
489
609
  const streamEntryNamespaceId = urnParsedEntry.namespaceMethod();
490
610
  if (streamNamespaceId !== streamEntryNamespaceId) {
491
611
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
@@ -497,16 +617,31 @@ export class AuditableItemStreamService {
497
617
  if (Is.empty(streamEntity)) {
498
618
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
499
619
  }
620
+ if (streamEntity.closed) {
621
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ClosedStreamRejections, { operation: "updateEntry" });
622
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "streamClosed", {
623
+ id: streamId
624
+ });
625
+ }
626
+ if (streamEntity.mode === AuditableItemStreamModes.AppendOnly) {
627
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.AppendOnlyRejections, { operation: "updateEntry" });
628
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "appendOnlyNoEntryUpdates", {
629
+ id: streamId
630
+ });
631
+ }
500
632
  const entryNamespaceId = urnParsedEntry.namespaceSpecific(1);
501
633
  const existing = await this.findEntry(streamEntity.id, entryNamespaceId);
502
634
  if (Is.empty(existing)) {
503
635
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamEntryNotFound", entryId);
504
636
  }
637
+ const contextIds = await ContextIdStore.getContextIds();
638
+ const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
505
639
  const context = {
506
640
  now: new Date(Date.now()).toISOString(),
507
641
  contextIds,
508
642
  indexCounter: streamEntity.numberOfItems,
509
- immutableInterval: streamEntity.immutableInterval
643
+ immutableInterval: streamEntity.immutableInterval,
644
+ organizationIdentity: streamEntity.organizationIdentity ?? ownerOrganizationId
510
645
  };
511
646
  await this.setEntry(context, streamEntity.id, {
512
647
  ...existing.entity,
@@ -515,22 +650,25 @@ export class AuditableItemStreamService {
515
650
  streamEntity.dateModified = context.now;
516
651
  streamEntity.numberOfItems = context.indexCounter;
517
652
  await this._streamStorage.set(streamEntity);
653
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.EntriesUpdated);
518
654
  await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamEntryUpdated, { id: streamId, entryId });
519
655
  }
520
656
  catch (error) {
521
657
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "updatingEntryFailed", undefined, error);
522
658
  }
659
+ finally {
660
+ Mutex.unlock(streamNamespaceId);
661
+ }
523
662
  }
524
663
  /**
525
664
  * Delete from the stream.
526
665
  * @param streamId The id of the stream to remove from.
527
666
  * @param entryId The id of the entry to remove.
528
- * @returns Nothing.
667
+ * @returns A promise that resolves when the entry has been removed.
529
668
  */
530
669
  async removeEntry(streamId, entryId) {
531
670
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
532
671
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "entryId", entryId);
533
- const contextIds = await ContextIdStore.getContextIds();
534
672
  const urnParsed = Urn.fromValidString(streamId);
535
673
  if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
536
674
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
@@ -545,8 +683,9 @@ export class AuditableItemStreamService {
545
683
  id: entryId
546
684
  });
547
685
  }
686
+ const streamNamespaceId = urnParsed.namespaceSpecific(0);
687
+ await Mutex.lock(streamNamespaceId, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
548
688
  try {
549
- const streamNamespaceId = urnParsed.namespaceMethod();
550
689
  const streamEntryNamespaceId = urnParsedEntry.namespaceMethod();
551
690
  if (streamNamespaceId !== streamEntryNamespaceId) {
552
691
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
@@ -558,17 +697,26 @@ export class AuditableItemStreamService {
558
697
  if (Is.empty(streamEntity)) {
559
698
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
560
699
  }
700
+ if (streamEntity.mode === AuditableItemStreamModes.AppendOnly) {
701
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.AppendOnlyRejections, { operation: "removeEntry" });
702
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "appendOnlyNoEntryRemovals", {
703
+ id: streamId
704
+ });
705
+ }
561
706
  const entryNamespaceId = urnParsedEntry.namespaceSpecific(1);
562
707
  const result = await this.findEntry(streamNamespaceId, entryNamespaceId);
563
708
  if (Is.empty(result)) {
564
709
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamEntryNotFound", entryId);
565
710
  }
566
711
  if (Is.empty(result.entity.dateDeleted)) {
712
+ const contextIds = await ContextIdStore.getContextIds();
713
+ const ownerOrganizationId = contextIds?.[ContextIdKeys.UserOrganization] ?? contextIds?.[ContextIdKeys.Organization];
567
714
  const context = {
568
715
  now: new Date(Date.now()).toISOString(),
569
716
  contextIds,
570
717
  indexCounter: streamEntity.numberOfItems,
571
- immutableInterval: streamEntity.immutableInterval
718
+ immutableInterval: streamEntity.immutableInterval,
719
+ organizationIdentity: streamEntity.organizationIdentity ?? ownerOrganizationId
572
720
  };
573
721
  await this.setEntry(context, streamEntity.id, {
574
722
  ...result.entity,
@@ -577,16 +725,20 @@ export class AuditableItemStreamService {
577
725
  streamEntity.dateModified = context.now;
578
726
  streamEntity.numberOfItems = context.indexCounter;
579
727
  await this._streamStorage.set(streamEntity);
728
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.EntriesDeleted);
580
729
  await this._eventBusComponent?.publish(AuditableItemStreamTopics.StreamEntryDeleted, { id: streamId, entryId });
581
730
  }
582
731
  }
583
732
  catch (error) {
584
733
  throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removingEntryFailed", undefined, error);
585
734
  }
735
+ finally {
736
+ Mutex.unlock(streamNamespaceId);
737
+ }
586
738
  }
587
739
  /**
588
740
  * Get the entries for the stream.
589
- * @param streamId The id of the stream to get.
741
+ * @param streamId The id of the stream to get, if undefined returns all matching entries.
590
742
  * @param options Additional options for the get operation.
591
743
  * @param options.conditions The conditions to filter the stream.
592
744
  * @param options.includeDeleted Whether to include deleted entries, defaults to false.
@@ -598,19 +750,24 @@ export class AuditableItemStreamService {
598
750
  * @throws NotFoundError if the stream is not found.
599
751
  */
600
752
  async getEntries(streamId, options) {
601
- Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
602
- const urnParsed = Urn.fromValidString(streamId);
603
- if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
604
- throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
605
- namespace: AuditableItemStreamService._NAMESPACE,
606
- id: streamId
607
- });
753
+ let streamNamespaceId;
754
+ if (!Is.empty(streamId)) {
755
+ Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
756
+ const urnParsed = Urn.fromValidString(streamId);
757
+ if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
758
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
759
+ namespace: AuditableItemStreamService._NAMESPACE,
760
+ id: streamId
761
+ });
762
+ }
763
+ streamNamespaceId = urnParsed.namespaceSpecific(0);
608
764
  }
609
765
  try {
610
- const streamNamespaceId = urnParsed.namespaceSpecific(0);
611
- const streamEntity = await this._streamStorage.get(streamNamespaceId);
612
- if (Is.empty(streamEntity)) {
613
- throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
766
+ if (Is.stringValue(streamNamespaceId)) {
767
+ const streamEntity = await this._streamStorage.get(streamNamespaceId);
768
+ if (Is.empty(streamEntity)) {
769
+ throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
770
+ }
614
771
  }
615
772
  const verifyEntries = options?.verifyEntries ?? false;
616
773
  const result = await this.findEntries(streamNamespaceId, options?.includeDeleted, verifyEntries, options?.conditions, options?.order, undefined, options?.limit, options?.cursor);
@@ -638,7 +795,7 @@ export class AuditableItemStreamService {
638
795
  }
639
796
  /**
640
797
  * Get the entry objects for the stream.
641
- * @param streamId The id of the stream to get.
798
+ * @param streamId The id of the stream to get, if undefined returns all matching entries.
642
799
  * @param options Additional options for the get operation.
643
800
  * @param options.conditions The conditions to filter the stream.
644
801
  * @param options.includeDeleted Whether to include deleted entries, defaults to false.
@@ -649,19 +806,24 @@ export class AuditableItemStreamService {
649
806
  * @throws NotFoundError if the stream is not found.
650
807
  */
651
808
  async getEntryObjects(streamId, options) {
652
- Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
653
- const urnParsed = Urn.fromValidString(streamId);
654
- if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
655
- throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
656
- namespace: AuditableItemStreamService._NAMESPACE,
657
- id: streamId
658
- });
809
+ let streamNamespaceId;
810
+ if (!Is.empty(streamId)) {
811
+ Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
812
+ const urnParsed = Urn.fromValidString(streamId);
813
+ if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
814
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "namespaceMismatch", {
815
+ namespace: AuditableItemStreamService._NAMESPACE,
816
+ id: streamId
817
+ });
818
+ }
819
+ streamNamespaceId = urnParsed.namespaceSpecific(0);
659
820
  }
660
821
  try {
661
- const streamNamespaceId = urnParsed.namespaceSpecific(0);
662
- const streamEntity = await this._streamStorage.get(streamNamespaceId);
663
- if (Is.empty(streamEntity)) {
664
- throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
822
+ if (Is.stringValue(streamNamespaceId)) {
823
+ const streamEntity = await this._streamStorage.get(streamNamespaceId);
824
+ if (Is.empty(streamEntity)) {
825
+ throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamId);
826
+ }
665
827
  }
666
828
  const result = await this.findEntries(streamNamespaceId, options?.includeDeleted, false, options?.conditions, options?.order, undefined, options?.limit, options?.cursor);
667
829
  const list = {
@@ -684,12 +846,12 @@ export class AuditableItemStreamService {
684
846
  }
685
847
  }
686
848
  /**
687
- * Remove the verifiable storage for the stream and entries.
688
- * @param streamId The id of the stream to remove the storage from.
689
- * @returns Nothing.
849
+ * Remove the proof for the stream and entries.
850
+ * @param streamId The id of the stream to remove the proof from.
851
+ * @returns A promise that resolves when the proof has been removed.
690
852
  * @throws NotFoundError if the vertex is not found.
691
853
  */
692
- async removeVerifiable(streamId) {
854
+ async removeProof(streamId) {
693
855
  Guards.stringValue(AuditableItemStreamService.CLASS_NAME, "streamId", streamId);
694
856
  const urnParsed = Urn.fromValidString(streamId);
695
857
  if (urnParsed.namespaceIdentifier() !== AuditableItemStreamService._NAMESPACE) {
@@ -698,8 +860,9 @@ export class AuditableItemStreamService {
698
860
  id: streamId
699
861
  });
700
862
  }
863
+ const streamIdParts = urnParsed.namespaceSpecific(0);
864
+ await Mutex.lock(streamIdParts, { throwOnTimeout: true, timeoutMs: this._mutexTimeoutMs });
701
865
  try {
702
- const streamIdParts = urnParsed.namespaceSpecific(0);
703
866
  const streamEntity = await this._streamStorage.get(streamIdParts);
704
867
  if (Is.empty(streamEntity)) {
705
868
  throw new NotFoundError(AuditableItemStreamService.CLASS_NAME, "streamNotFound", streamIdParts);
@@ -707,9 +870,27 @@ export class AuditableItemStreamService {
707
870
  await this.internalRemoveEntries(streamEntity, true);
708
871
  }
709
872
  catch (error) {
710
- throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removeVerifiableFailed", undefined, error);
873
+ throw new GeneralError(AuditableItemStreamService.CLASS_NAME, "removeProofFailed", undefined, error);
874
+ }
875
+ finally {
876
+ Mutex.unlock(streamIdParts);
711
877
  }
712
878
  }
879
+ /**
880
+ * Create an immutable proof for the stream entity if the conditions are met.
881
+ * @param streamEntity The stream entity to create the proof for.
882
+ * @param immutableInterval The immutable interval for the stream.
883
+ * @returns The proof id.
884
+ * @internal
885
+ */
886
+ async createStreamProof(streamEntity, immutableInterval) {
887
+ const streamModel = this.streamEntityToJsonLd(ObjectHelper.pick(streamEntity, AuditableItemStreamService._PROOF_KEYS_STREAM));
888
+ if (immutableInterval > 0 && Is.stringValue(streamModel.organizationIdentity)) {
889
+ streamEntity.proofId = await this._immutableProofComponent.create(streamModel);
890
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsCreatedStream);
891
+ }
892
+ return streamModel.id;
893
+ }
713
894
  /**
714
895
  * Map the stream entity to a JSON-LD model.
715
896
  * @param streamEntity The stream entity.
@@ -719,9 +900,9 @@ export class AuditableItemStreamService {
719
900
  streamEntityToJsonLd(streamEntity) {
720
901
  const model = {
721
902
  "@context": [
903
+ SchemaOrgContexts.Context,
722
904
  AuditableItemStreamContexts.Context,
723
- AuditableItemStreamContexts.ContextCommon,
724
- SchemaOrgContexts.Context
905
+ AuditableItemStreamContexts.ContextCommon
725
906
  ],
726
907
  type: AuditableItemStreamTypes.Stream,
727
908
  id: `${AuditableItemStreamService._NAMESPACE}:${streamEntity.id}`,
@@ -732,7 +913,9 @@ export class AuditableItemStreamService {
732
913
  annotationObject: streamEntity.annotationObject,
733
914
  immutableInterval: streamEntity.immutableInterval,
734
915
  proofId: streamEntity.proofId,
735
- numberOfItems: streamEntity.numberOfItems
916
+ numberOfItems: streamEntity.numberOfItems,
917
+ closed: streamEntity.closed,
918
+ mode: streamEntity.mode
736
919
  };
737
920
  return model;
738
921
  }
@@ -794,8 +977,12 @@ export class AuditableItemStreamService {
794
977
  // Create the JSON-LD object we want to use for the proof
795
978
  // this is a subset of fixed properties from the stream entry object.
796
979
  const streamEntryModel = this.streamEntryEntityToJsonLd(ObjectHelper.pick(entity, AuditableItemStreamService._PROOF_KEYS_STREAM_ENTRY));
797
- // Create the proof for the stream object
798
- entity.proofId = await this._immutableProofComponent.create(streamEntryModel);
980
+ // Create the proof for the stream object but only if we have an organization identity,
981
+ // either from the stream or the context, as this is needed for the proof creation and immutability.
982
+ if (Is.stringValue(context.organizationIdentity)) {
983
+ entity.proofId = await this._immutableProofComponent.create(JsonLdHelper.toNodeObject(streamEntryModel));
984
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsCreatedEntry, { index: entity.index });
985
+ }
799
986
  }
800
987
  await this._streamEntryStorage.set(entity);
801
988
  return entity.id;
@@ -805,6 +992,7 @@ export class AuditableItemStreamService {
805
992
  * @param streamId The stream id.
806
993
  * @param entryId The entry id.
807
994
  * @param verifyEntry Should the entry be verified.
995
+ * @returns The entry entity and optional verification result, or undefined if not found.
808
996
  * @internal
809
997
  */
810
998
  async findEntry(streamId, entryId, verifyEntry) {
@@ -851,18 +1039,19 @@ export class AuditableItemStreamService {
851
1039
  * @param propertiesToReturn The properties to return.
852
1040
  * @param limit Limit the number of entities when finding.
853
1041
  * @param cursor The cursor.
854
- * @param contextIds The context ids to perform the operation with.
1042
+ * @returns The stream entries and optional next cursor.
855
1043
  * @internal
856
1044
  */
857
1045
  async findEntries(streamId, includeDeleted, verifyEntries, conditions, sortDirection, propertiesToReturn, limit, cursor) {
858
1046
  const needToVerify = verifyEntries ?? false;
859
- const combinedConditions = [
860
- {
1047
+ const combinedConditions = [];
1048
+ if (Is.stringValue(streamId)) {
1049
+ combinedConditions.push({
861
1050
  property: "streamId",
862
1051
  comparison: ComparisonOperator.Equals,
863
1052
  value: streamId
864
- }
865
- ];
1053
+ });
1054
+ }
866
1055
  if (Is.stringValue(cursor)) {
867
1056
  const parts = cursor.split("|");
868
1057
  if (parts.length > 1) {
@@ -923,14 +1112,15 @@ export class AuditableItemStreamService {
923
1112
  * Remove the verifiable storage for the stream and entries.
924
1113
  * @param streamEntity The stream entity.
925
1114
  * @param removeOnlyProof Should only the proof be removed.
926
- * @returns Nothing.
1115
+ * @returns A promise that resolves when the entries have been processed.
927
1116
  * @internal
928
1117
  */
929
1118
  async internalRemoveEntries(streamEntity, removeOnlyProof) {
930
1119
  if (Is.stringValue(streamEntity.proofId)) {
931
- await this._immutableProofComponent.removeVerifiable(streamEntity.proofId);
1120
+ await this._immutableProofComponent.removeNotarization(streamEntity.proofId);
932
1121
  delete streamEntity.proofId;
933
1122
  await this._streamStorage.set(streamEntity);
1123
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsRemovedStream);
934
1124
  }
935
1125
  const entryIds = [];
936
1126
  let entriesResult;
@@ -948,8 +1138,9 @@ export class AuditableItemStreamService {
948
1138
  for (const streamEntry of entriesResult.entities) {
949
1139
  entryIds.push(streamEntry.id);
950
1140
  if (Is.stringValue(streamEntry.proofId)) {
951
- await this._immutableProofComponent.removeVerifiable(streamEntry.proofId);
1141
+ await this._immutableProofComponent.removeNotarization(streamEntry.proofId);
952
1142
  delete streamEntry.proofId;
1143
+ await MetricHelper.metricIncrement(this._telemetryComponent, AuditableItemStreamMetricIds.ProofsRemovedEntry);
953
1144
  // If we are only removing the proof, we need to set the entry
954
1145
  // otherwise the entry is going to be removed later anyway.
955
1146
  if (removeOnlyProof) {