@openclaw/msteams 2026.8.1-beta.2 → 2026.8.1

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.
@@ -1,9 +1,13 @@
1
- const require_resolve_allowlist = require("./resolve-allowlist-C6YcNC9a.cjs");
1
+ const require_resolve_allowlist = require("./resolve-allowlist-CWbvAW4v.cjs");
2
+ require("./secret-input-C65t6kjM.cjs");
3
+ const require_config_schema = require("./config-schema-MyN2lamA.cjs");
2
4
  let openclaw_plugin_sdk_string_coerce_runtime = require("openclaw/plugin-sdk/string-coerce-runtime");
3
- let openclaw_plugin_sdk_account_id = require("openclaw/plugin-sdk/account-id");
4
5
  let openclaw_plugin_sdk_allow_from = require("openclaw/plugin-sdk/allow-from");
6
+ let openclaw_plugin_sdk_account_id = require("openclaw/plugin-sdk/account-id");
7
+ let openclaw_plugin_sdk_account_helpers = require("openclaw/plugin-sdk/account-helpers");
5
8
  let openclaw_plugin_sdk_number_runtime = require("openclaw/plugin-sdk/number-runtime");
6
9
  let openclaw_plugin_sdk_channel_config_helpers = require("openclaw/plugin-sdk/channel-config-helpers");
10
+ let openclaw_plugin_sdk_secret_file_runtime = require("openclaw/plugin-sdk/secret-file-runtime");
7
11
  let openclaw_plugin_sdk_channel_setup = require("openclaw/plugin-sdk/channel-setup");
8
12
  let openclaw_plugin_sdk_setup = require("openclaw/plugin-sdk/setup");
9
13
  let openclaw_plugin_sdk_setup_tools = require("openclaw/plugin-sdk/setup-tools");
@@ -20,13 +24,23 @@ const msteamsMeta = {
20
24
  aliases: ["teams"],
21
25
  order: 60
22
26
  };
27
+ function resolveMSTeamsAccount(cfg) {
28
+ const config = cfg.channels?.msteams;
29
+ const credentials = require_resolve_allowlist.resolveMSTeamsCredentials(config);
30
+ const certificatePath = credentials?.type === "federated" && !credentials.useManagedIdentity ? credentials.certificatePath : void 0;
31
+ const certificate = certificatePath ? (0, openclaw_plugin_sdk_secret_file_runtime.tryReadSecretFileSync)(certificatePath, "Microsoft Teams certificate", void 0, { configPath: config?.certificatePath?.trim() ? "channels.msteams.certificatePath" : "env.MSTEAMS_CERTIFICATE_PATH" }) : void 0;
32
+ const unavailable = certificate?.status === "configured_unavailable";
33
+ return {
34
+ accountId: openclaw_plugin_sdk_account_id.DEFAULT_ACCOUNT_ID,
35
+ enabled: config?.enabled !== false,
36
+ configured: Boolean(credentials),
37
+ tokenStatus: !credentials ? "missing" : unavailable ? "configured_unavailable" : "available",
38
+ ...unavailable ? { credentialDiagnostics: [certificate.diagnostic] } : {}
39
+ };
40
+ }
23
41
  const msteamsConfigAdapter = (0, openclaw_plugin_sdk_channel_config_helpers.createTopLevelChannelConfigAdapter)({
24
42
  sectionKey: "msteams",
25
- resolveAccount: (cfg) => ({
26
- accountId: openclaw_plugin_sdk_account_id.DEFAULT_ACCOUNT_ID,
27
- enabled: cfg.channels?.msteams?.enabled !== false,
28
- configured: Boolean(require_resolve_allowlist.resolveMSTeamsCredentials(cfg.channels?.msteams))
29
- }),
43
+ resolveAccount: resolveMSTeamsAccount,
30
44
  resolveAccessorAccount: ({ cfg }) => ({
31
45
  allowFrom: cfg.channels?.msteams?.allowFrom,
32
46
  defaultTo: cfg.channels?.msteams?.defaultTo
@@ -220,12 +234,8 @@ function parseNonNegativeRetryAfterSeconds(raw) {
220
234
  });
221
235
  }
222
236
  /**
223
- * Classify outbound send errors for safe retries and actionable logs.
224
- *
225
- * Important: We only mark errors as retryable when we have an explicit HTTP
226
- * status code that indicates the message was not accepted (e.g. 429, 5xx).
227
- * For transport-level errors where delivery is ambiguous, we prefer to avoid
228
- * retries to reduce the chance of duplicate posts.
237
+ * Owns retry safety and delivery ambiguity for Teams send failures.
238
+ * Only Teams rate-limit rejection proves this unkeyed POST safe to replay.
229
239
  */
230
240
  function classifyMSTeamsSendError(err) {
231
241
  const statusCode = extractStatusCode(err);
@@ -249,15 +259,15 @@ function classifyMSTeamsSendError(err) {
249
259
  };
250
260
  }
251
261
  if (statusCode === 429) return {
252
- kind: "throttled",
262
+ kind: "replay-safe",
253
263
  statusCode,
254
264
  retryAfterMs: retryAfterMs ?? void 0,
255
265
  errorCode
256
266
  };
257
267
  if (statusCode === 408 || statusCode != null && statusCode >= 500) return {
258
- kind: "transient",
268
+ kind: "ambiguous",
269
+ source: "http",
259
270
  statusCode,
260
- retryAfterMs: retryAfterMs ?? void 0,
261
271
  errorCode
262
272
  };
263
273
  if (statusCode != null && statusCode >= 400) return {
@@ -265,17 +275,14 @@ function classifyMSTeamsSendError(err) {
265
275
  statusCode,
266
276
  errorCode
267
277
  };
268
- if (statusCode == null) {
269
- const networkCode = (0, openclaw_plugin_sdk_string_coerce_runtime.isRecord)(err) && typeof err.code === "string" ? err.code : null;
270
- if (networkCode === "ECONNREFUSED" || networkCode === "ENOTFOUND" || networkCode === "EHOSTUNREACH" || networkCode === "ETIMEDOUT" || networkCode === "ECONNRESET") return {
271
- kind: "network",
272
- errorCode: networkCode
273
- };
274
- }
278
+ if (statusCode == null && (0, openclaw_plugin_sdk_retry_runtime.isTransientNetworkError)(err)) return {
279
+ kind: "ambiguous",
280
+ source: "transport",
281
+ errorCode
282
+ };
275
283
  return {
276
284
  kind: "unknown",
277
285
  statusCode: statusCode ?? void 0,
278
- retryAfterMs: retryAfterMs ?? void 0,
279
286
  errorCode
280
287
  };
281
288
  }
@@ -294,9 +301,15 @@ function isRevokedProxyError(err) {
294
301
  function formatMSTeamsSendErrorHint(classification) {
295
302
  if (classification.kind === "auth") return "check msteams appId/appPassword/tenantId (or env vars MSTEAMS_APP_ID/MSTEAMS_APP_PASSWORD/MSTEAMS_TENANT_ID)";
296
303
  if (classification.errorCode === "ContentStreamNotAllowed") return "Teams expired the content stream; stop streaming earlier and fall back to normal message delivery";
297
- if (classification.kind === "throttled") return "Teams throttled the bot; backing off may help";
298
- if (classification.kind === "transient") return "transient Teams/Bot Framework error; retry may succeed";
299
- if (classification.kind === "network") return "transport-level failure sending reply to Teams Bot Connector (smba.trafficmanager.net) — check egress firewall rules allow outbound HTTPS to smba.trafficmanager.net";
304
+ if (classification.kind === "replay-safe") return "Teams throttled the bot; backing off may help";
305
+ if (classification.kind === "ambiguous") {
306
+ if (classification.source === "transport") return "transport-level failure sending to Teams Bot Connector (smba.trafficmanager.net); the outcome is unknown, so check egress and conversation history before taking further action";
307
+ return "Teams/Bot Framework delivery outcome is unknown; inspect the conversation history before taking further action";
308
+ }
309
+ }
310
+ function formatMSTeamsDeliveryFailureGuidance(classification) {
311
+ if (classification.kind === "replay-safe") return "The request was rate-limited before delivery; retrying later may succeed.";
312
+ if (classification.kind === "ambiguous") return "Delivery may already have succeeded; do not send the same content again without checking the conversation.";
300
313
  }
301
314
  //#endregion
302
315
  //#region extensions/msteams/src/setup-surface.ts
@@ -466,7 +479,7 @@ const msteamsSetupWizard = {
466
479
  };
467
480
  let oauthModule;
468
481
  try {
469
- oauthModule = await Promise.resolve().then(() => require("./oauth-DktsbN22.cjs"));
482
+ oauthModule = await Promise.resolve().then(() => require("./oauth-BV7HWGe0.cjs"));
470
483
  } catch (err) {
471
484
  await noteDelegatedAuthFailure(err);
472
485
  return {
@@ -520,6 +533,40 @@ const msteamsSetupWizard = {
520
533
  disable: (cfg) => (0, openclaw_plugin_sdk_setup.setSetupChannelEnabled)(cfg, channel, false)
521
534
  };
522
535
  //#endregion
536
+ //#region extensions/msteams/src/channel.setup.ts
537
+ const msteamsSetupPlugin = {
538
+ id: "msteams",
539
+ meta: {
540
+ ...msteamsMeta,
541
+ aliases: [...msteamsMeta.aliases]
542
+ },
543
+ capabilities: {
544
+ chatTypes: [
545
+ "direct",
546
+ "channel",
547
+ "group",
548
+ "thread"
549
+ ],
550
+ polls: true,
551
+ threads: true,
552
+ media: true,
553
+ reactions: true
554
+ },
555
+ reload: { configPrefixes: ["channels.msteams"] },
556
+ configSchema: require_config_schema.MSTeamsChannelConfigSchema,
557
+ config: {
558
+ ...msteamsConfigAdapter,
559
+ isConfigured: (account) => account.configured,
560
+ describeAccount: (account) => (0, openclaw_plugin_sdk_account_helpers.describeAccountSnapshot)({
561
+ account,
562
+ configured: account.configured,
563
+ extra: { tokenStatus: account.tokenStatus }
564
+ })
565
+ },
566
+ setupWizard: msteamsSetupWizard,
567
+ setupContract: msteamsSetupContract
568
+ };
569
+ //#endregion
523
570
  Object.defineProperty(exports, "classifyMSTeamsSendError", {
524
571
  enumerable: true,
525
572
  get: function() {
@@ -532,6 +579,12 @@ Object.defineProperty(exports, "createMSTeamsSetupWizardBase", {
532
579
  return createMSTeamsSetupWizardBase;
533
580
  }
534
581
  });
582
+ Object.defineProperty(exports, "formatMSTeamsDeliveryFailureGuidance", {
583
+ enumerable: true,
584
+ get: function() {
585
+ return formatMSTeamsDeliveryFailureGuidance;
586
+ }
587
+ });
535
588
  Object.defineProperty(exports, "formatMSTeamsSendErrorHint", {
536
589
  enumerable: true,
537
590
  get: function() {
@@ -556,22 +609,16 @@ Object.defineProperty(exports, "msteamsConfigAdapter", {
556
609
  return msteamsConfigAdapter;
557
610
  }
558
611
  });
559
- Object.defineProperty(exports, "msteamsMeta", {
560
- enumerable: true,
561
- get: function() {
562
- return msteamsMeta;
563
- }
564
- });
565
612
  Object.defineProperty(exports, "msteamsSetupAdapter", {
566
613
  enumerable: true,
567
614
  get: function() {
568
615
  return msteamsSetupAdapter;
569
616
  }
570
617
  });
571
- Object.defineProperty(exports, "msteamsSetupContract", {
618
+ Object.defineProperty(exports, "msteamsSetupPlugin", {
572
619
  enumerable: true,
573
620
  get: function() {
574
- return msteamsSetupContract;
621
+ return msteamsSetupPlugin;
575
622
  }
576
623
  });
577
624
  Object.defineProperty(exports, "msteamsSetupWizard", {
@@ -586,3 +633,9 @@ Object.defineProperty(exports, "openDelegatedOAuthUrl", {
586
633
  return openDelegatedOAuthUrl;
587
634
  }
588
635
  });
636
+ Object.defineProperty(exports, "resolveMSTeamsAccount", {
637
+ enumerable: true,
638
+ get: function() {
639
+ return resolveMSTeamsAccount;
640
+ }
641
+ });
@@ -80,18 +80,19 @@ function isAzureChinaBotFrameworkServiceUrl(value) {
80
80
  return false;
81
81
  }
82
82
  }
83
+ const { accountShape, rootPolicyShape } = (0, openclaw_plugin_sdk_channel_config_schema.buildChannelAccountSchemaParts)({
84
+ omit: [
85
+ "name",
86
+ "mentionPatterns",
87
+ "replyToMode"
88
+ ],
89
+ allowFrom: zod.z.array(zod.z.string()).optional(),
90
+ groupAllowFrom: zod.z.array(zod.z.string()).optional(),
91
+ streaming: openclaw_plugin_sdk_channel_config_schema.ChannelPreviewStreamingConfigSchema.optional()
92
+ });
83
93
  const MSTeamsConfigSchema = zod.z.object({
84
- ...(0, openclaw_plugin_sdk_channel_config_schema.buildCommonChannelAccountShape)({
85
- useDefaults: true,
86
- omit: [
87
- "name",
88
- "mentionPatterns",
89
- "replyToMode"
90
- ],
91
- allowFrom: zod.z.array(zod.z.string()).optional(),
92
- groupAllowFrom: zod.z.array(zod.z.string()).optional(),
93
- streaming: openclaw_plugin_sdk_channel_config_schema.ChannelPreviewStreamingConfigSchema.optional()
94
- }),
94
+ ...accountShape,
95
+ ...rootPolicyShape,
95
96
  dangerouslyAllowNameMatching: openclaw_plugin_sdk_channel_config_schema.ChannelDangerouslyAllowNameMatchingSchema,
96
97
  appId: zod.z.string().optional(),
97
98
  appPassword: (0, openclaw_plugin_sdk_secret_input.registerSensitiveConfigSchema)(SecretInputSchema.optional()),
@@ -0,0 +1,20 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("./secret-input-C65t6kjM.cjs");
3
+ let openclaw_plugin_sdk_secret_input = require("openclaw/plugin-sdk/secret-input");
4
+ //#region extensions/msteams/configured-state.ts
5
+ /** Mirror Teams auth-mode requirements without loading the Azure SDK or full channel. */
6
+ function hasConfiguredMSTeamsChannelState(params) {
7
+ const config = params.cfg.channels?.msteams;
8
+ if (config?.enabled === false) return false;
9
+ const env = params.env ?? process.env;
10
+ const appId = (0, openclaw_plugin_sdk_secret_input.normalizeSecretInputString)(config && Object.hasOwn(config, "appId") ? config.appId : env.MSTEAMS_APP_ID);
11
+ const tenantId = (0, openclaw_plugin_sdk_secret_input.normalizeSecretInputString)(config && Object.hasOwn(config, "tenantId") ? config.tenantId : env.MSTEAMS_TENANT_ID);
12
+ if (!appId || !tenantId) return false;
13
+ if ((config?.authType ?? env.MSTEAMS_AUTH_TYPE ?? "secret") === "federated") {
14
+ const certificatePath = (0, openclaw_plugin_sdk_secret_input.normalizeSecretInputString)(config && Object.hasOwn(config, "certificatePath") ? config.certificatePath : env.MSTEAMS_CERTIFICATE_PATH);
15
+ return Boolean(certificatePath || (config?.useManagedIdentity ?? env.MSTEAMS_USE_MANAGED_IDENTITY === "true"));
16
+ }
17
+ return config && Object.hasOwn(config, "appPassword") ? (0, openclaw_plugin_sdk_secret_input.hasConfiguredSecretInput)(config.appPassword) : Boolean((0, openclaw_plugin_sdk_secret_input.normalizeSecretInputString)(env.MSTEAMS_APP_PASSWORD));
18
+ }
19
+ //#endregion
20
+ exports.hasConfiguredMSTeamsChannelState = hasConfiguredMSTeamsChannelState;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_resolve_allowlist = require("./resolve-allowlist-C6YcNC9a.cjs");
2
+ const require_resolve_allowlist = require("./resolve-allowlist-CWbvAW4v.cjs");
3
3
  let openclaw_plugin_sdk_directory_runtime = require("openclaw/plugin-sdk/directory-runtime");
4
4
  const msteamsDirectoryContractPlugin = {
5
5
  id: "msteams",
@@ -11,7 +11,7 @@ node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises, 1);
11
11
  let node_path = require("node:path");
12
12
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
13
13
  let openclaw_plugin_sdk_runtime_doctor_migrations = require("openclaw/plugin-sdk/runtime-doctor-migrations");
14
- //#region extensions/msteams/doctor-contract-api.ts
14
+ //#region extensions/msteams/config-doctor-api.ts
15
15
  const streamingAliasMigration = (0, openclaw_plugin_sdk_runtime_doctor_migrations.defineChannelAliasMigration)({
16
16
  channelId: "msteams",
17
17
  streaming: { defaultMode: "partial" }
@@ -20,6 +20,8 @@ const legacyConfigRules = streamingAliasMigration.legacyConfigRules;
20
20
  function normalizeCompatibilityConfig({ cfg }) {
21
21
  return streamingAliasMigration.normalizeChannelConfig({ cfg });
22
22
  }
23
+ //#endregion
24
+ //#region extensions/msteams/doctor-contract-api.ts
23
25
  const LEARNINGS_NAMESPACE = "feedback-learnings";
24
26
  const MAX_LEARNING_ENTRIES = 1e4;
25
27
  const MSTEAMS_PLUGIN_ID = "Microsoft Teams";
@@ -165,6 +167,33 @@ function mergeLearnings(legacy, existing) {
165
167
  }
166
168
  return merged.slice(-10);
167
169
  }
170
+ async function completeLegacyKeyedImport(params) {
171
+ const { filePath, label, imported, warnings } = params;
172
+ const changes = [];
173
+ let missing = 0;
174
+ for (const { store, requiredKeys } of params.stores) {
175
+ const retainedKeys = new Set((await store.entries()).map((entry) => entry.key));
176
+ missing += [...requiredKeys].filter((key) => !retainedKeys.has(key)).length;
177
+ }
178
+ if (missing > 0) {
179
+ warnings.push(`Incomplete ${label} migration: plugin state failed to retain every required entry (${missing} missing); left legacy source in place`);
180
+ return {
181
+ changes,
182
+ warnings
183
+ };
184
+ }
185
+ changes.push(`Migrated ${imported} ${label} ${imported === 1 ? "entry" : "entries"} -> plugin state`);
186
+ await (0, openclaw_plugin_sdk_runtime_doctor_migrations.archiveLegacyStateSource)({
187
+ filePath,
188
+ label: params.archiveLabel ?? label,
189
+ changes,
190
+ warnings
191
+ });
192
+ return {
193
+ changes,
194
+ warnings
195
+ };
196
+ }
168
197
  const stateMigrations = [
169
198
  {
170
199
  id: "msteams-conversations-json-to-plugin-state",
@@ -175,35 +204,36 @@ const stateMigrations = [
175
204
  return { preview: [`- ${MSTEAMS_PLUGIN_ID} conversations: ${Object.keys(state.conversations).length} entries -> plugin state (${require_polls.MSTEAMS_CONVERSATIONS_NAMESPACE})`] };
176
205
  },
177
206
  async migrateLegacyState(params) {
178
- const changes = [];
179
- const warnings = [];
180
207
  const filePath = resolveStateFilePath(params.stateDir, require_polls.MSTEAMS_CONVERSATIONS_LEGACY_FILENAME);
181
208
  const state = await readLegacyJsonFile(filePath, parseLegacyConversationStore);
182
209
  if (!state) return {
183
- changes,
184
- warnings
210
+ changes: [],
211
+ warnings: []
185
212
  };
186
213
  const store = params.context.openPluginStateKeyedStore({
187
214
  namespace: require_polls.MSTEAMS_CONVERSATIONS_NAMESPACE,
188
215
  maxEntries: require_polls.MSTEAMS_SQLITE_MAX_CONVERSATION_ROWS
189
216
  });
217
+ const requiredKeys = new Set((await store.entries()).map((entry) => entry.key));
190
218
  let imported = 0;
191
219
  for (const [rawConversationId, reference] of require_polls.selectRetainedMSTeamsConversations(state.conversations)) {
192
220
  const conversationId = require_polls.normalizeStoredConversationId(rawConversationId);
193
221
  if (!conversationId) continue;
194
- if (await store.registerIfAbsent(require_polls.buildMSTeamsConversationStateKey(conversationId), require_polls.prepareMSTeamsConversationReferenceForStorage(conversationId, reference))) imported++;
222
+ const key = require_polls.buildMSTeamsConversationStateKey(conversationId);
223
+ requiredKeys.add(key);
224
+ const storedReference = require_polls.prepareMSTeamsConversationReferenceForStorage(conversationId, reference);
225
+ if (await store.registerIfAbsent(key, storedReference)) imported++;
195
226
  }
196
- changes.push(`Migrated ${imported} ${MSTEAMS_PLUGIN_ID} conversation ${imported === 1 ? "entry" : "entries"} -> plugin state`);
197
- await (0, openclaw_plugin_sdk_runtime_doctor_migrations.archiveLegacyStateSource)({
227
+ return completeLegacyKeyedImport({
198
228
  filePath,
199
229
  label: `${MSTEAMS_PLUGIN_ID} conversation`,
200
- changes,
201
- warnings
230
+ imported,
231
+ warnings: [],
232
+ stores: [{
233
+ store,
234
+ requiredKeys
235
+ }]
202
236
  });
203
- return {
204
- changes,
205
- warnings
206
- };
207
237
  }
208
238
  },
209
239
  {
@@ -215,13 +245,11 @@ const stateMigrations = [
215
245
  return { preview: [`- ${MSTEAMS_PLUGIN_ID} polls: ${Object.keys(state.polls).length} entries -> plugin state (${require_polls.MSTEAMS_POLLS_NAMESPACE})`] };
216
246
  },
217
247
  async migrateLegacyState(params) {
218
- const changes = [];
219
- const warnings = [];
220
248
  const filePath = resolveStateFilePath(params.stateDir, require_polls.MSTEAMS_POLLS_LEGACY_FILENAME);
221
249
  const state = await readLegacyJsonFile(filePath, parseLegacyPollStore);
222
250
  if (!state) return {
223
- changes,
224
- warnings
251
+ changes: [],
252
+ warnings: []
225
253
  };
226
254
  const pollStore = params.context.openPluginStateKeyedStore({
227
255
  namespace: require_polls.MSTEAMS_POLLS_NAMESPACE,
@@ -231,10 +259,14 @@ const stateMigrations = [
231
259
  namespace: require_polls.MSTEAMS_POLL_VOTE_BUCKETS_NAMESPACE,
232
260
  maxEntries: require_polls.MSTEAMS_MAX_POLL_VOTE_BUCKET_ROWS
233
261
  });
262
+ const requiredPollKeys = new Set((await pollStore.entries()).map((entry) => entry.key));
263
+ const requiredVoteKeys = new Set((await voteBucketStore.entries()).map((entry) => entry.key));
234
264
  let imported = 0;
235
265
  for (const [pollId, poll] of require_polls.selectRetainedMSTeamsPolls(state.polls)) {
236
266
  const { metadata, votes } = require_polls.splitMSTeamsPoll(poll);
237
- const didImportPoll = await pollStore.registerIfAbsent(require_polls.buildMSTeamsPollStateKey(pollId), metadata);
267
+ const pollKey = require_polls.buildMSTeamsPollStateKey(pollId);
268
+ requiredPollKeys.add(pollKey);
269
+ const didImportPoll = await pollStore.registerIfAbsent(pollKey, metadata);
238
270
  const buckets = /* @__PURE__ */ new Map();
239
271
  for (const [voterId, selections] of Object.entries(votes)) {
240
272
  const bucket = require_polls.selectMSTeamsPollVoteBucket(pollId, voterId);
@@ -245,6 +277,7 @@ const stateMigrations = [
245
277
  let importedVoteBucket = false;
246
278
  for (const [bucket, bucketVotes] of buckets) {
247
279
  const key = require_polls.buildMSTeamsPollVoteBucketKey(pollId, bucket);
280
+ requiredVoteKeys.add(key);
248
281
  const existing = await voteBucketStore.lookup(key);
249
282
  await voteBucketStore.register(key, {
250
283
  pollId,
@@ -259,17 +292,19 @@ const stateMigrations = [
259
292
  }
260
293
  if (didImportPoll || importedVoteBucket) imported++;
261
294
  }
262
- changes.push(`Migrated ${imported} ${MSTEAMS_PLUGIN_ID} poll ${imported === 1 ? "entry" : "entries"} -> plugin state`);
263
- await (0, openclaw_plugin_sdk_runtime_doctor_migrations.archiveLegacyStateSource)({
295
+ return completeLegacyKeyedImport({
264
296
  filePath,
265
297
  label: `${MSTEAMS_PLUGIN_ID} poll`,
266
- changes,
267
- warnings
298
+ imported,
299
+ warnings: [],
300
+ stores: [{
301
+ store: pollStore,
302
+ requiredKeys: requiredPollKeys
303
+ }, {
304
+ store: voteBucketStore,
305
+ requiredKeys: requiredVoteKeys
306
+ }]
268
307
  });
269
- return {
270
- changes,
271
- warnings
272
- };
273
308
  }
274
309
  },
275
310
  {
@@ -281,18 +316,18 @@ const stateMigrations = [
281
316
  return { preview: [`- ${MSTEAMS_PLUGIN_ID} SSO tokens: ${Object.keys(state.tokens).length} entries -> plugin state (${require_sso_token_store.MSTEAMS_SSO_TOKENS_NAMESPACE})`] };
282
317
  },
283
318
  async migrateLegacyState(params) {
284
- const changes = [];
285
319
  const warnings = [];
286
320
  const filePath = resolveStateFilePath(params.stateDir, require_sso_token_store.MSTEAMS_SSO_TOKENS_LEGACY_FILENAME);
287
321
  const state = await readLegacyJsonFile(filePath, (value) => require_sso_token_store.isMSTeamsSsoStoreData(value) ? value : null);
288
322
  if (!state) return {
289
- changes,
323
+ changes: [],
290
324
  warnings
291
325
  };
292
326
  const store = params.context.openPluginStateKeyedStore({
293
327
  namespace: require_sso_token_store.MSTEAMS_SSO_TOKENS_NAMESPACE,
294
328
  maxEntries: require_sso_token_store.MSTEAMS_MAX_SSO_TOKENS
295
329
  });
330
+ const requiredKeys = new Set((await store.entries()).map((entry) => entry.key));
296
331
  let imported = 0;
297
332
  let skipped = 0;
298
333
  for (const token of Object.values(state.tokens)) {
@@ -301,20 +336,22 @@ const stateMigrations = [
301
336
  skipped++;
302
337
  continue;
303
338
  }
304
- if (await store.registerIfAbsent(require_sso_token_store.makeMSTeamsSsoTokenStoreKey(normalized.connectionName, normalized.userId), normalized)) imported++;
339
+ const key = require_sso_token_store.makeMSTeamsSsoTokenStoreKey(normalized.connectionName, normalized.userId);
340
+ requiredKeys.add(key);
341
+ if (await store.registerIfAbsent(key, normalized)) imported++;
305
342
  }
306
- changes.push(`Migrated ${imported} ${MSTEAMS_PLUGIN_ID} SSO token ${imported === 1 ? "entry" : "entries"} -> plugin state`);
307
343
  if (skipped > 0) warnings.push(`Skipped ${skipped} malformed ${MSTEAMS_PLUGIN_ID} SSO token ${skipped === 1 ? "entry" : "entries"} during migration`);
308
- await (0, openclaw_plugin_sdk_runtime_doctor_migrations.archiveLegacyStateSource)({
344
+ return completeLegacyKeyedImport({
309
345
  filePath,
310
- label: `${MSTEAMS_PLUGIN_ID} SSO-token`,
311
- changes,
312
- warnings
346
+ label: `${MSTEAMS_PLUGIN_ID} SSO token`,
347
+ archiveLabel: `${MSTEAMS_PLUGIN_ID} SSO-token`,
348
+ imported,
349
+ warnings,
350
+ stores: [{
351
+ store,
352
+ requiredKeys
353
+ }]
313
354
  });
314
- return {
315
- changes,
316
- warnings
317
- };
318
355
  }
319
356
  },
320
357
  {