@frockbot/plugin-settings 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/user.ts CHANGED
@@ -2,18 +2,19 @@ import {
2
2
  configurationCommandFingerprintV1,
3
3
  ConfigurationConflictError,
4
4
  ConfigurationDecodeError,
5
+ decodePackageSettingIdsV1,
5
6
  decodePackageSettingsPatchV1,
6
7
  MAX_PACKAGE_SETTINGS_V1,
7
- decodeConnectionDependencyRequirementV1,
8
8
  decodeOperationReceiptV1,
9
9
  decodeUserConfigurationExecuteRpcV1,
10
10
  decodeUserConfigurationReadRpcV1,
11
11
  decodeUserSettingsViewV1,
12
+ migrateStoredUserSettingsV1,
12
13
  MAX_USER_CONNECTIONS_V1,
13
14
  USER_PROFILE_PLACEHOLDER_NAME_V1,
14
- type ConnectionDependencyRequirementV1,
15
15
  type ConnectionView,
16
16
  type JsonValue,
17
+ type PackageSettingValueV1,
17
18
  type OperationReceiptV1,
18
19
  type PackageInstallationView,
19
20
  type UserConfigurationCommandV1,
@@ -41,16 +42,6 @@ const DEFAULT_PACKAGES_BOOTSTRAP_KEY = "user-default-packages-bootstrap:v1";
41
42
  const CATALOG_PIN_KEY = "user-catalog-pin";
42
43
  const IDENTITY_KEY = "user-id";
43
44
  const RECEIPT_PREFIX = "configuration-receipt:";
44
- const MAX_CONNECTION_DEPENDENCIES = 256;
45
-
46
- type ConnectionDependency = {
47
- botId: string;
48
- generation: string;
49
- packageId: string;
50
- capabilityId: string;
51
- claimOrder: number;
52
- status: "pending" | "acknowledged";
53
- };
54
45
 
55
46
  interface StoredConfigurationReceipt {
56
47
  commandFingerprint: string;
@@ -137,6 +128,10 @@ export interface AvailableUserPackage {
137
128
  * from their first configuration read.
138
129
  */
139
130
  installByDefault?: boolean;
131
+ /** The seeded installation state. Omission preserves the enabled default. */
132
+ defaultEnablement?: "enabled" | "disabled";
133
+ /** The Package manifest's Package-id-to-version-range dependency record. */
134
+ dependencies?: Readonly<Record<string, string>>;
140
135
  /**
141
136
  * `configuration.settings` from this version's manifest. Absent is the same
142
137
  * as empty and means the Package offers no User-level setting, so every
@@ -195,61 +190,6 @@ function requireMatchingConfigurationReceipt(
195
190
  return stored.receipt;
196
191
  }
197
192
 
198
- function connectionDependencies(
199
- connection: ConnectionView,
200
- ): ConnectionDependency[] {
201
- const value = connection.safeMetadata.dependentAssignments;
202
- if (!Array.isArray(value)) return [];
203
- return value.flatMap((candidate) => {
204
- if (
205
- !candidate ||
206
- typeof candidate !== "object" ||
207
- Array.isArray(candidate)
208
- ) {
209
- return [];
210
- }
211
- const dependency = candidate as Record<string, unknown>;
212
- if (
213
- typeof dependency.botId !== "string" ||
214
- typeof dependency.generation !== "string" ||
215
- typeof dependency.packageId !== "string" ||
216
- typeof dependency.capabilityId !== "string" ||
217
- (dependency.claimOrder !== undefined &&
218
- (!Number.isSafeInteger(dependency.claimOrder) ||
219
- (dependency.claimOrder as number) < 0)) ||
220
- (dependency.status !== "pending" && dependency.status !== "acknowledged")
221
- ) {
222
- return [];
223
- }
224
- return [
225
- {
226
- botId: dependency.botId,
227
- generation: dependency.generation,
228
- packageId: dependency.packageId,
229
- capabilityId: dependency.capabilityId,
230
- claimOrder:
231
- dependency.claimOrder === undefined
232
- ? 0
233
- : (dependency.claimOrder as number),
234
- status: dependency.status,
235
- } satisfies ConnectionDependency,
236
- ];
237
- });
238
- }
239
-
240
- function withConnectionDependencies(
241
- connection: ConnectionView,
242
- dependencies: ConnectionDependency[],
243
- ): ConnectionView {
244
- return {
245
- ...connection,
246
- safeMetadata: {
247
- ...connection.safeMetadata,
248
- dependentAssignments: dependencies,
249
- },
250
- };
251
- }
252
-
253
193
  /**
254
194
  * An install may only name the generation this User is pinned to. Refusing
255
195
  * anything else is what makes "Composition consumes immutable,
@@ -292,13 +232,17 @@ function withCatalogPin(
292
232
  * client already reads needs no second source.
293
233
  */
294
234
  function mergePackageSettingValues(
295
- current: Record<string, JsonValue> | undefined,
296
- patch: Record<string, string | number | boolean>,
297
- ): Record<string, JsonValue> {
298
- const merged: Record<string, JsonValue> = { ...(current ?? {}) };
235
+ current: Record<string, JsonValue | PackageSettingValueV1> | undefined,
236
+ patch: Record<string, PackageSettingValueV1>,
237
+ unset: readonly string[],
238
+ ): Record<string, JsonValue | PackageSettingValueV1> {
239
+ const merged: Record<string, JsonValue | PackageSettingValueV1> = {
240
+ ...(current ?? {}),
241
+ };
299
242
  for (const [settingId, value] of Object.entries(patch)) {
300
243
  merged[settingId] = value;
301
244
  }
245
+ for (const settingId of unset) delete merged[settingId];
302
246
  if (Object.keys(merged).length > MAX_PACKAGE_SETTINGS_V1) {
303
247
  throw new ConfigurationDecodeError("Package settings are too many");
304
248
  }
@@ -317,12 +261,11 @@ function applyUserCommand(
317
261
  switch (command.type) {
318
262
  case "user/update-profile":
319
263
  return { ...current, revision, profile: command.profile };
320
- case "user/set-new-bot-model":
264
+ case "user/set-platform-model":
321
265
  return {
322
266
  ...current,
323
267
  revision,
324
- newBotModelTemplate: command.model,
325
- newBotModelTemplateSource: command.source,
268
+ platformModel: command.model,
326
269
  };
327
270
  case "user/install-package": {
328
271
  const existing = current.packages.find(
@@ -346,7 +289,12 @@ function applyUserCommand(
346
289
  {
347
290
  packageId: command.packageId,
348
291
  version: command.version,
349
- state: existing?.state === "failed" ? "failed" : "installed",
292
+ state:
293
+ existing?.state === "failed"
294
+ ? "failed"
295
+ : command.enabled === false
296
+ ? "disabled"
297
+ : "installed",
350
298
  failure: existing?.failure,
351
299
  // A Catalog install records where it came from; the compiled-in
352
300
  // path records nothing new, so an old row keeps its exact shape.
@@ -355,6 +303,9 @@ function applyUserCommand(
355
303
  : {
356
304
  catalogId: command.catalogId,
357
305
  catalogGeneration: command.catalogGeneration,
306
+ ...(command.contentHash === undefined
307
+ ? {}
308
+ : { contentHash: command.contentHash }),
358
309
  provenance: "catalog" as const,
359
310
  }),
360
311
  ...(Object.keys(values).length === 0 ? {} : { values }),
@@ -363,10 +314,8 @@ function applyUserCommand(
363
314
  };
364
315
  }
365
316
  case "user/uninstall-package": {
366
- // Removing the row is the whole effect. Assignments that depend on it
367
- // are not touched: `capabilityAssignmentFailureV1` resolves them as
368
- // unavailable tombstones the User can repair (ADR 0003), and
369
- // Connections are the User's own and outlive any Package.
317
+ // Removing the row is the whole effect. Connections are the User's own
318
+ // and outlive any Package (ADR 0019).
370
319
  if (
371
320
  !current.packages.some((pkg) => pkg.packageId === command.packageId)
372
321
  ) {
@@ -391,20 +340,33 @@ function applyUserCommand(
391
340
  }
392
341
  // Validated against the manifest of the version this User has, not the
393
342
  // one the client happened to be looking at.
394
- const patch = decodePackageSettingsPatchV1(
395
- settingDefinitions(installed.packageId, installed.version),
396
- command.values,
343
+ const definitions = settingDefinitions(
344
+ installed.packageId,
345
+ installed.version,
397
346
  );
347
+ const patch = command.values
348
+ ? decodePackageSettingsPatchV1(definitions, command.values)
349
+ : {};
350
+ const unset = command.unset
351
+ ? decodePackageSettingIdsV1(definitions, command.unset)
352
+ : [];
398
353
  return {
399
354
  ...current,
400
355
  revision,
401
356
  packages: current.packages.map((pkg) =>
402
- pkg.packageId === command.packageId
403
- ? {
404
- ...pkg,
405
- values: mergePackageSettingValues(pkg.values, patch),
406
- }
407
- : pkg,
357
+ pkg.packageId !== command.packageId
358
+ ? pkg
359
+ : (() => {
360
+ const values = mergePackageSettingValues(
361
+ pkg.values,
362
+ patch,
363
+ unset,
364
+ );
365
+ const { values: _storedValues, ...withoutValues } = pkg;
366
+ return Object.keys(values).length > 0
367
+ ? { ...withoutValues, values }
368
+ : withoutValues;
369
+ })(),
408
370
  ),
409
371
  };
410
372
  }
@@ -435,6 +397,12 @@ function applyUserCommand(
435
397
  export class UserSettingsBackendContribution {
436
398
  private readonly availablePackages: ReadonlySet<string>;
437
399
 
400
+ /** Declared Package dependencies, by Package id and version. */
401
+ private readonly packageDependencies: ReadonlyMap<
402
+ string,
403
+ Readonly<Record<string, string>>
404
+ >;
405
+
438
406
  /** The immutable first-party installation rows written on first read. */
439
407
  private readonly defaultPackages: readonly PackageInstallationView[];
440
408
 
@@ -457,6 +425,12 @@ export class UserSettingsBackendContribution {
457
425
  ({ packageId, version }) => `${packageId}\u0000${version}`,
458
426
  ),
459
427
  );
428
+ this.packageDependencies = new Map(
429
+ host.availablePackages.map((pkg) => [
430
+ `${pkg.packageId}\u0000${pkg.version}`,
431
+ pkg.dependencies ?? {},
432
+ ]),
433
+ );
460
434
  this.packageSettingDefinitions = new Map(
461
435
  host.availablePackages.map((pkg) => [
462
436
  `${pkg.packageId}\u0000${pkg.version}`,
@@ -469,7 +443,10 @@ export class UserSettingsBackendContribution {
469
443
  {
470
444
  packageId: pkg.packageId,
471
445
  version: pkg.version,
472
- state: "installed" as const,
446
+ state:
447
+ pkg.defaultEnablement === "disabled"
448
+ ? ("disabled" as const)
449
+ : ("installed" as const),
473
450
  provenance: "first-party" as const,
474
451
  },
475
452
  ]
@@ -549,6 +526,26 @@ export class UserSettingsBackendContribution {
549
526
  );
550
527
  }
551
528
 
529
+ private packageDependencyFailure(
530
+ packageId: string,
531
+ version: string,
532
+ settings: UserSettingsViewV1,
533
+ ): string | undefined {
534
+ const dependencies = this.packageDependencies.get(
535
+ `${packageId}\u0000${version}`,
536
+ );
537
+ if (!dependencies) return undefined;
538
+ for (const dependencyId of Object.keys(dependencies).sort()) {
539
+ const available = settings.packages.some(
540
+ (pkg) => pkg.packageId === dependencyId && pkg.state === "installed",
541
+ );
542
+ if (!available) {
543
+ return `Package "${packageId}" requires Package "${dependencyId}" to be installed and enabled; enable "${dependencyId}" first`;
544
+ }
545
+ }
546
+ return undefined;
547
+ }
548
+
552
549
  async readConfiguration(input: unknown): Promise<UserSettingsViewV1> {
553
550
  const request = decodeUserConfigurationReadRpcV1(input);
554
551
  for (const bootstrap of this.readBootstraps.values()) {
@@ -622,6 +619,7 @@ export class UserSettingsBackendContribution {
622
619
  version: string;
623
620
  catalogId: string;
624
621
  catalogGeneration: string;
622
+ contentHash?: string;
625
623
  }): Promise<CatalogEntryV1> {
626
624
  const catalog = this.host.catalog;
627
625
  if (!catalog) {
@@ -646,6 +644,17 @@ export class UserSettingsBackendContribution {
646
644
  `Catalog entry "${command.catalogId}" does not offer Package "${command.packageId}" at version "${command.version}"`,
647
645
  );
648
646
  }
647
+ if (entry.bundle) {
648
+ if (command.contentHash !== entry.bundle.contentHash) {
649
+ throw new Error(
650
+ `Catalog entry "${command.catalogId}" requires bundle hash "${entry.bundle.contentHash}"`,
651
+ );
652
+ }
653
+ } else if (command.contentHash !== undefined) {
654
+ throw new Error(
655
+ `Catalog entry "${command.catalogId}" does not carry a Package bundle`,
656
+ );
657
+ }
649
658
  return entry;
650
659
  }
651
660
 
@@ -663,6 +672,9 @@ export class UserSettingsBackendContribution {
663
672
  version: command.version,
664
673
  catalogId: command.catalogId,
665
674
  catalogGeneration: command.catalogGeneration,
675
+ ...(command.contentHash === undefined
676
+ ? {}
677
+ : { contentHash: command.contentHash }),
666
678
  })
667
679
  : undefined;
668
680
  return this.host.storage.transaction((storage) =>
@@ -738,7 +750,7 @@ export class UserSettingsBackendContribution {
738
750
  const current =
739
751
  storedSettings === undefined
740
752
  ? initialState()
741
- : decodeUserSettingsViewV1(storedSettings);
753
+ : decodeUserSettingsViewV1(migrateStoredUserSettingsV1(storedSettings));
742
754
  if (command.type === "user/set-package-enabled" && command.enabled) {
743
755
  const installed = current.packages.find(
744
756
  (pkg) => pkg.packageId === command.packageId,
@@ -755,6 +767,36 @@ export class UserSettingsBackendContribution {
755
767
  if (command.expectedRevision !== current.revision) {
756
768
  throw new ConfigurationConflictError(current.revision);
757
769
  }
770
+ let dependencyFailure: string | undefined;
771
+ if (command.type === "user/install-package" && command.enabled !== false) {
772
+ dependencyFailure = this.packageDependencyFailure(
773
+ command.packageId,
774
+ command.version,
775
+ current,
776
+ );
777
+ } else if (command.type === "user/set-package-enabled" && command.enabled) {
778
+ const installed = current.packages.find(
779
+ (pkg) => pkg.packageId === command.packageId,
780
+ );
781
+ if (installed) {
782
+ dependencyFailure = this.packageDependencyFailure(
783
+ installed.packageId,
784
+ installed.version,
785
+ current,
786
+ );
787
+ }
788
+ }
789
+ if (dependencyFailure) {
790
+ const receipt: OperationReceiptV1 = {
791
+ schemaVersion: 1,
792
+ commandId: command.commandId,
793
+ revision: current.revision,
794
+ status: "rejected",
795
+ failure: dependencyFailure,
796
+ };
797
+ await storage.put(receiptKey, { commandFingerprint, receipt });
798
+ return receipt;
799
+ }
758
800
  const next = applyUserCommand(current, command, (packageId, version) =>
759
801
  this.settingDefinitions(packageId, version),
760
802
  );
@@ -777,7 +819,7 @@ export class UserSettingsBackendContribution {
777
819
  const stored = await storage.get<unknown>(STATE_KEY);
778
820
  return stored === undefined
779
821
  ? initialState()
780
- : decodeUserSettingsViewV1(stored);
822
+ : decodeUserSettingsViewV1(migrateStoredUserSettingsV1(stored));
781
823
  }
782
824
 
783
825
  async read(
@@ -937,261 +979,6 @@ export class UserSettingsBackendContribution {
937
979
  );
938
980
  }
939
981
 
940
- /**
941
- * The durable state of one Bot's dependency on one Connection. `absent` is
942
- * the answer for a Connection this object does not hold, so a reconciling
943
- * saga can distinguish "never claimed" from "claimed and pending".
944
- */
945
- async readConnectionDependency(
946
- userId: string,
947
- connectionId: string,
948
- botId: string,
949
- generation: string,
950
- ): Promise<"absent" | "pending" | "acknowledged"> {
951
- const connection = await this.getConnection(userId, connectionId);
952
- if (!connection) return "absent";
953
- const dependency = connectionDependencies(connection).find(
954
- (candidate) =>
955
- candidate.botId === botId && candidate.generation === generation,
956
- );
957
- return dependency?.status ?? "absent";
958
- }
959
-
960
- async claimConnectionDependency(
961
- userId: string,
962
- connectionId: string,
963
- botId: string,
964
- generation: string,
965
- requirement: ConnectionDependencyRequirementV1,
966
- storage?: UserSettingsTransaction,
967
- ): Promise<boolean> {
968
- const decoded = decodeConnectionDependencyRequirementV1(requirement);
969
- return this.transitionConnectionDependency(
970
- userId,
971
- connectionId,
972
- (current, settings) => {
973
- const installation = settings.packages.find(
974
- (pkg) =>
975
- pkg.packageId === decoded.packageId &&
976
- pkg.version === decoded.packageVersion &&
977
- pkg.state === "installed",
978
- );
979
- if (
980
- !installation ||
981
- current.state !== "ready" ||
982
- current.packageId !== decoded.packageId ||
983
- !decoded.connectionTypeIds.includes(current.connectionTypeId)
984
- ) {
985
- return undefined;
986
- }
987
- const existing = connectionDependencies(current);
988
- const replay = existing.find(
989
- (dependency) =>
990
- dependency.botId === botId && dependency.generation === generation,
991
- );
992
- if (replay) {
993
- return replay.packageId === decoded.packageId &&
994
- replay.capabilityId === decoded.capabilityId
995
- ? current
996
- : undefined;
997
- }
998
- if (existing.length >= MAX_CONNECTION_DEPENDENCIES) return undefined;
999
- return withConnectionDependencies(current, [
1000
- ...existing,
1001
- {
1002
- botId,
1003
- generation,
1004
- packageId: decoded.packageId,
1005
- capabilityId: decoded.capabilityId,
1006
- claimOrder: settings.revision + 1,
1007
- status: "pending",
1008
- },
1009
- ]);
1010
- },
1011
- storage,
1012
- );
1013
- }
1014
-
1015
- async acknowledgeConnectionDependency(
1016
- userId: string,
1017
- connectionId: string,
1018
- botId: string,
1019
- generation: string,
1020
- ): Promise<boolean> {
1021
- return this.host.storage.transaction(async (storage) => {
1022
- await this.assertIdentity(userId, storage);
1023
- const current = await this.readSnapshot(storage);
1024
- const target = current.connections.find(
1025
- (connection) => connection.connectionId === connectionId,
1026
- );
1027
- if (
1028
- !target ||
1029
- target.state === "revoking" ||
1030
- target.state === "revoked"
1031
- ) {
1032
- return false;
1033
- }
1034
- const matched = connectionDependencies(target).find(
1035
- (dependency) =>
1036
- dependency.botId === botId && dependency.generation === generation,
1037
- );
1038
- if (!matched) return false;
1039
- const latestClaimOrder = Math.max(
1040
- ...current.connections.flatMap((connection) =>
1041
- connectionDependencies(connection).flatMap((dependency) =>
1042
- dependency.botId === botId &&
1043
- dependency.packageId === matched.packageId &&
1044
- dependency.capabilityId === matched.capabilityId
1045
- ? [dependency.claimOrder]
1046
- : [],
1047
- ),
1048
- ),
1049
- );
1050
- if (matched.claimOrder < latestClaimOrder) return false;
1051
- if (
1052
- matched.status === "acknowledged" &&
1053
- !current.connections.some((connection) =>
1054
- connectionDependencies(connection).some(
1055
- (dependency) =>
1056
- dependency.botId === botId &&
1057
- dependency.packageId === matched.packageId &&
1058
- dependency.capabilityId === matched.capabilityId &&
1059
- (connection.connectionId !== connectionId ||
1060
- dependency.generation !== generation),
1061
- ),
1062
- )
1063
- ) {
1064
- return true;
1065
- }
1066
- const connections = current.connections.map((connection) => {
1067
- const dependencies = connectionDependencies(connection);
1068
- const nextDependencies = dependencies.flatMap((dependency) => {
1069
- const sameAuthority =
1070
- dependency.botId === botId &&
1071
- dependency.packageId === matched.packageId &&
1072
- dependency.capabilityId === matched.capabilityId;
1073
- if (!sameAuthority) return [dependency];
1074
- if (
1075
- connection.connectionId === connectionId &&
1076
- dependency.generation === generation
1077
- ) {
1078
- return [{ ...dependency, status: "acknowledged" as const }];
1079
- }
1080
- return [];
1081
- });
1082
- return nextDependencies.length === dependencies.length &&
1083
- nextDependencies.every(
1084
- (dependency, index) => dependency === dependencies[index],
1085
- )
1086
- ? connection
1087
- : withConnectionDependencies(connection, nextDependencies);
1088
- });
1089
- await storage.put(STATE_KEY, {
1090
- ...current,
1091
- revision: current.revision + 1,
1092
- connections,
1093
- } satisfies UserSettingsViewV1);
1094
- return true;
1095
- });
1096
- }
1097
-
1098
- async releaseConnectionDependency(
1099
- userId: string,
1100
- connectionId: string,
1101
- botId: string,
1102
- generation: string,
1103
- ): Promise<boolean> {
1104
- return this.host.storage.transaction(async (storage) => {
1105
- await this.assertIdentity(userId, storage);
1106
- const current = await this.readSnapshot(storage);
1107
- const target = current.connections.find(
1108
- (connection) => connection.connectionId === connectionId,
1109
- );
1110
- if (!target) return true;
1111
- const existing = connectionDependencies(target);
1112
- const matching = existing.filter(
1113
- (dependency) =>
1114
- dependency.botId === botId && dependency.generation === generation,
1115
- );
1116
- if (matching.length === 0) return true;
1117
- if (matching.some((dependency) => dependency.status !== "acknowledged")) {
1118
- return false;
1119
- }
1120
- const remaining = existing.filter(
1121
- (dependency) =>
1122
- dependency.botId !== botId || dependency.generation !== generation,
1123
- );
1124
- const connections = current.connections.map((connection) =>
1125
- connection.connectionId === connectionId
1126
- ? withConnectionDependencies(connection, remaining)
1127
- : connection,
1128
- );
1129
- await storage.put(STATE_KEY, {
1130
- ...current,
1131
- revision: current.revision + 1,
1132
- connections,
1133
- } satisfies UserSettingsViewV1);
1134
- return true;
1135
- });
1136
- }
1137
-
1138
- async compensateConnectionDependency(
1139
- userId: string,
1140
- connectionId: string,
1141
- botId: string,
1142
- generation: string,
1143
- ): Promise<boolean> {
1144
- return this.transitionConnectionDependency(
1145
- userId,
1146
- connectionId,
1147
- (current) => {
1148
- const existing = connectionDependencies(current);
1149
- const remaining = existing.filter(
1150
- (dependency) =>
1151
- dependency.botId !== botId ||
1152
- dependency.generation !== generation ||
1153
- dependency.status !== "pending",
1154
- );
1155
- return remaining.length === existing.length
1156
- ? undefined
1157
- : withConnectionDependencies(current, remaining);
1158
- },
1159
- );
1160
- }
1161
-
1162
- private async transitionConnectionDependency(
1163
- userId: string,
1164
- connectionId: string,
1165
- transition: (
1166
- connection: ConnectionView,
1167
- settings: UserSettingsViewV1,
1168
- ) => ConnectionView | undefined,
1169
- transaction?: UserSettingsTransaction,
1170
- ): Promise<boolean> {
1171
- const apply = async (storage: UserSettingsTransaction) => {
1172
- await this.assertIdentity(userId, storage);
1173
- const current = await this.readSnapshot(storage);
1174
- const connection = current.connections.find(
1175
- (candidate) => candidate.connectionId === connectionId,
1176
- );
1177
- if (!connection) return false;
1178
- const nextConnection = transition(connection, current);
1179
- if (!nextConnection) return false;
1180
- if (nextConnection === connection) return true;
1181
- await storage.put(STATE_KEY, {
1182
- ...current,
1183
- revision: current.revision + 1,
1184
- connections: current.connections.map((candidate) =>
1185
- candidate.connectionId === connectionId ? nextConnection : candidate,
1186
- ),
1187
- } satisfies UserSettingsViewV1);
1188
- return true;
1189
- };
1190
- return transaction
1191
- ? apply(transaction)
1192
- : this.host.storage.transaction(apply);
1193
- }
1194
-
1195
982
  private async assertIdentity(
1196
983
  userId: string,
1197
984
  storage: UserSettingsTransaction = this.host.storage,
@@ -1,22 +0,0 @@
1
- import type {
2
- BotSettingsViewV1,
3
- CapabilityAssignmentOperationViewV1,
4
- } from "@frockbot/configuration-core";
5
-
6
- /** Projects every durable Assignment operation without requiring catalog state. */
7
- export function projectAssignmentOperations(
8
- settings: Pick<BotSettingsViewV1, "assignmentOperations"> | undefined,
9
- ): CapabilityAssignmentOperationViewV1[] {
10
- return (settings?.assignmentOperations ?? []).map((operation) =>
11
- structuredClone(operation),
12
- );
13
- }
14
-
15
- export function assignmentHasPendingOperation(
16
- operations: readonly CapabilityAssignmentOperationViewV1[],
17
- assignmentId: string,
18
- ): boolean {
19
- return operations.some(
20
- (operation) => operation.assignmentId === assignmentId,
21
- );
22
- }