aamp-openclaw-plugin 0.1.37 → 0.1.39

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/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- // ../sdk/src/jmap-push.js
1
+ // ../sdk/dist/jmap-push.js
2
2
  import WebSocket from "ws";
3
3
 
4
- // ../sdk/src/types.js
4
+ // ../sdk/dist/types.js
5
5
  var AAMP_PROTOCOL_VERSION = "1.1";
6
6
  var AAMP_HEADER = {
7
7
  VERSION: "X-AAMP-Version",
8
8
  INTENT: "X-AAMP-Intent",
9
9
  TASK_ID: "X-AAMP-TaskId",
10
- CONTEXT_LINKS: "X-AAMP-ContextLinks",
10
+ SESSION_KEY: "X-AAMP-Session-Key",
11
11
  DISPATCH_CONTEXT: "X-AAMP-Dispatch-Context",
12
12
  PRIORITY: "X-AAMP-Priority",
13
13
  EXPIRES_AT: "X-AAMP-Expires-At",
@@ -19,11 +19,13 @@ var AAMP_HEADER = {
19
19
  BLOCKED_REASON: "X-AAMP-BlockedReason",
20
20
  SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
21
21
  STREAM_ID: "X-AAMP-Stream-Id",
22
+ PAIR_CODE: "X-AAMP-Pair-Code",
23
+ DISPATCH_CONTEXT_RULES: "X-AAMP-Dispatch-Context-Rules",
22
24
  PARENT_TASK_ID: "X-AAMP-ParentTaskId",
23
25
  CARD_SUMMARY: "X-AAMP-Card-Summary"
24
26
  };
25
27
 
26
- // ../sdk/src/parser.js
28
+ // ../sdk/dist/parser.js
27
29
  function normalizeBodyText(value) {
28
30
  return value?.replace(/\r\n/g, "\n").trim() ?? "";
29
31
  }
@@ -158,6 +160,20 @@ function encodeStructuredResult(value) {
158
160
  const json = JSON.stringify(value);
159
161
  return Buffer.from(json, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
160
162
  }
163
+ function decodeBase64UrlJson(value) {
164
+ if (!value)
165
+ return void 0;
166
+ try {
167
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
168
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
169
+ return JSON.parse(Buffer.from(normalized + padding, "base64").toString("utf-8"));
170
+ } catch {
171
+ return void 0;
172
+ }
173
+ }
174
+ function encodeBase64UrlJson(value) {
175
+ return Buffer.from(JSON.stringify(value), "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
176
+ }
161
177
  function parseAampHeaders(meta) {
162
178
  const headers = normalizeHeaders(meta.headers);
163
179
  const intent = getAampHeader(headers, AAMP_HEADER.INTENT);
@@ -169,8 +185,8 @@ function parseAampHeaders(meta) {
169
185
  const to = meta.to.replace(/^<|>$/g, "");
170
186
  const decodedSubject = decodeMimeEncodedWords(meta.subject);
171
187
  if (intent === "task.dispatch") {
172
- const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
173
188
  const dispatchContext = parseDispatchContextHeader(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT));
189
+ const sessionKey = getAampHeader(headers, AAMP_HEADER.SESSION_KEY);
174
190
  const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
175
191
  const priority = getAampHeader(headers, AAMP_HEADER.PRIORITY) ?? "normal";
176
192
  const expiresAt = getAampHeader(headers, AAMP_HEADER.EXPIRES_AT);
@@ -178,10 +194,10 @@ function parseAampHeaders(meta) {
178
194
  protocolVersion,
179
195
  intent: "task.dispatch",
180
196
  taskId,
197
+ ...sessionKey ? { sessionKey } : {},
181
198
  title: decodedSubject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
182
199
  priority: priority === "urgent" || priority === "high" ? priority : "normal",
183
200
  ...expiresAt ? { expiresAt } : {},
184
- contextLinks: contextLinksStr ? contextLinksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
185
201
  ...dispatchContext ? { dispatchContext } : {},
186
202
  ...parentTaskId ? { parentTaskId } : {},
187
203
  from,
@@ -270,6 +286,43 @@ function parseAampHeaders(meta) {
270
286
  };
271
287
  return streamOpened;
272
288
  }
289
+ if (intent === "pair.request") {
290
+ const pairCode = getAampHeader(headers, AAMP_HEADER.PAIR_CODE) ?? "";
291
+ if (!pairCode)
292
+ return null;
293
+ const pairRequest = {
294
+ protocolVersion,
295
+ intent: "pair.request",
296
+ taskId,
297
+ pairCode,
298
+ dispatchContextRules: decodeBase64UrlJson(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT_RULES)) ?? {},
299
+ from,
300
+ to,
301
+ messageId: meta.messageId,
302
+ subject: meta.subject,
303
+ bodyText: ""
304
+ };
305
+ return pairRequest;
306
+ }
307
+ if (intent === "pair.respond") {
308
+ const rawStatus = getAampHeader(headers, AAMP_HEADER.STATUS);
309
+ const status = rawStatus === "completed" ? "completed" : "rejected";
310
+ const reason = getAampHeader(headers, AAMP_HEADER.ERROR_MSG) || void 0;
311
+ const pairRespond = {
312
+ protocolVersion,
313
+ intent: "pair.respond",
314
+ taskId,
315
+ status,
316
+ success: status === "completed",
317
+ reason,
318
+ from,
319
+ to,
320
+ messageId: meta.messageId,
321
+ subject: meta.subject,
322
+ bodyText: normalizeBodyText(meta.bodyText)
323
+ };
324
+ return pairRespond;
325
+ }
273
326
  if (intent === "card.query") {
274
327
  const cardQuery = {
275
328
  protocolVersion,
@@ -310,8 +363,8 @@ function buildDispatchHeaders(params) {
310
363
  if (params.expiresAt) {
311
364
  headers[AAMP_HEADER.EXPIRES_AT] = params.expiresAt;
312
365
  }
313
- if (params.contextLinks.length > 0) {
314
- headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
366
+ if (params.sessionKey?.trim()) {
367
+ headers[AAMP_HEADER.SESSION_KEY] = params.sessionKey.trim();
315
368
  }
316
369
  const dispatchContext = serializeDispatchContextHeader(params.dispatchContext);
317
370
  if (dispatchContext) {
@@ -344,6 +397,27 @@ function buildStreamOpenedHeaders(opts) {
344
397
  [AAMP_HEADER.STREAM_ID]: opts.streamId
345
398
  };
346
399
  }
400
+ function buildPairRequestHeaders(opts) {
401
+ return {
402
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
403
+ [AAMP_HEADER.INTENT]: "pair.request",
404
+ [AAMP_HEADER.TASK_ID]: opts.taskId,
405
+ [AAMP_HEADER.PAIR_CODE]: opts.pairCode,
406
+ [AAMP_HEADER.DISPATCH_CONTEXT_RULES]: encodeBase64UrlJson(opts.dispatchContextRules ?? {})
407
+ };
408
+ }
409
+ function buildPairRespondHeaders(opts) {
410
+ const headers = {
411
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
412
+ [AAMP_HEADER.INTENT]: "pair.respond",
413
+ [AAMP_HEADER.TASK_ID]: opts.taskId,
414
+ [AAMP_HEADER.STATUS]: opts.success ? "completed" : "rejected"
415
+ };
416
+ if (!opts.success && opts.reason?.trim()) {
417
+ headers[AAMP_HEADER.ERROR_MSG] = opts.reason.replace(/[\r\n]/g, " ").trim();
418
+ }
419
+ return headers;
420
+ }
347
421
  function buildResultHeaders(params) {
348
422
  const headers = {
349
423
  [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
@@ -381,7 +455,7 @@ function buildCardResponseHeaders(params) {
381
455
  };
382
456
  }
383
457
 
384
- // ../sdk/src/tiny-emitter.js
458
+ // ../sdk/dist/tiny-emitter.js
385
459
  var TinyEmitter = class {
386
460
  listeners = /* @__PURE__ */ new Map();
387
461
  onceWrappers = /* @__PURE__ */ new WeakMap();
@@ -434,7 +508,7 @@ var TinyEmitter = class {
434
508
  }
435
509
  };
436
510
 
437
- // ../sdk/src/jmap-push.js
511
+ // ../sdk/dist/jmap-push.js
438
512
  function describeError(err) {
439
513
  if (!(err instanceof Error))
440
514
  return String(err);
@@ -729,6 +803,12 @@ var JmapPushClient = class extends TinyEmitter {
729
803
  case "task.stream.opened":
730
804
  this.emit("task.stream.opened", aampMsg);
731
805
  break;
806
+ case "pair.request":
807
+ this.emit("pair.request", aampMsg);
808
+ break;
809
+ case "pair.respond":
810
+ this.emit("pair.respond", aampMsg);
811
+ break;
732
812
  case "card.query":
733
813
  this.emit("card.query", aampMsg);
734
814
  break;
@@ -1114,10 +1194,177 @@ var JmapPushClient = class extends TinyEmitter {
1114
1194
  }
1115
1195
  };
1116
1196
 
1117
- // ../sdk/src/smtp-sender.js
1197
+ // ../sdk/dist/pairing.js
1198
+ import { randomBytes } from "node:crypto";
1199
+ function normalizeMailbox(value) {
1200
+ const mailbox = value.trim().toLowerCase();
1201
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mailbox)) {
1202
+ throw new Error(`Invalid AAMP mailbox in pairing URL: ${value}`);
1203
+ }
1204
+ return mailbox;
1205
+ }
1206
+ function normalizeDispatchContextRules(rules) {
1207
+ if (!rules)
1208
+ return void 0;
1209
+ const normalized = Object.fromEntries(Object.entries(rules).map(([key, values]) => [
1210
+ key.trim().toLowerCase(),
1211
+ (Array.isArray(values) ? values : []).map((value) => value.trim()).filter(Boolean)
1212
+ ]).filter(([key, values]) => Boolean(key) && values.length > 0));
1213
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
1214
+ }
1215
+ function encodeBase64UrlJson2(value) {
1216
+ return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
1217
+ }
1218
+ function decodeDispatchContextRules(value) {
1219
+ const candidates = [
1220
+ () => Buffer.from(value, "base64url").toString("utf8"),
1221
+ () => value
1222
+ ];
1223
+ for (const read of candidates) {
1224
+ try {
1225
+ const parsed = JSON.parse(read());
1226
+ return normalizeDispatchContextRules(parsed);
1227
+ } catch {
1228
+ }
1229
+ }
1230
+ throw new Error("Invalid dispatch_context_rules in pairing URL");
1231
+ }
1232
+ function buildPairingUrl(payload) {
1233
+ const mailbox = normalizeMailbox(payload.mailbox);
1234
+ const pairCode = payload.pairCode.trim();
1235
+ if (!pairCode)
1236
+ throw new Error("pairCode cannot be empty");
1237
+ const url = new URL("aamp://connect");
1238
+ url.searchParams.set("mailbox", mailbox);
1239
+ url.searchParams.set("pair_code", pairCode);
1240
+ const rules = normalizeDispatchContextRules(payload.dispatchContextRules);
1241
+ if (rules) {
1242
+ url.searchParams.set("dispatch_context_rules", encodeBase64UrlJson2(rules));
1243
+ }
1244
+ return url.toString();
1245
+ }
1246
+ function createPairingCode(options) {
1247
+ const pairCode = options.pairCode?.trim() || randomBytes(6).toString("base64url");
1248
+ const dispatchContextRules = normalizeDispatchContextRules(options.dispatchContextRules);
1249
+ return {
1250
+ mailbox: normalizeMailbox(options.mailbox),
1251
+ pairCode,
1252
+ expiresAt: new Date(Date.now() + (options.ttlSeconds ?? 300) * 1e3).toISOString(),
1253
+ connectUrl: buildPairingUrl({
1254
+ mailbox: options.mailbox,
1255
+ pairCode,
1256
+ ...dispatchContextRules ? { dispatchContextRules } : {}
1257
+ }),
1258
+ ...dispatchContextRules ? { dispatchContextRules } : {}
1259
+ };
1260
+ }
1261
+ function parsePairingUrl(input) {
1262
+ let url;
1263
+ try {
1264
+ url = new URL(input.trim());
1265
+ } catch {
1266
+ throw new Error("Invalid pairing URL");
1267
+ }
1268
+ if (url.protocol !== "aamp:" || url.hostname !== "connect") {
1269
+ throw new Error("Pairing URL must start with aamp://connect");
1270
+ }
1271
+ const mailbox = url.searchParams.get("mailbox") ?? "";
1272
+ const pairCode = url.searchParams.get("pair_code") ?? "";
1273
+ if (!pairCode.trim())
1274
+ throw new Error("Pairing URL is missing pair_code");
1275
+ const rawRules = url.searchParams.get("dispatch_context_rules") ?? url.searchParams.get("dispatchContextRules");
1276
+ const dispatchContextRules = rawRules ? decodeDispatchContextRules(rawRules) : void 0;
1277
+ return {
1278
+ mailbox: normalizeMailbox(mailbox),
1279
+ pairCode: pairCode.trim(),
1280
+ ...dispatchContextRules ? { dispatchContextRules } : {}
1281
+ };
1282
+ }
1283
+ function isPairingUrl(input) {
1284
+ try {
1285
+ parsePairingUrl(input);
1286
+ return true;
1287
+ } catch {
1288
+ return false;
1289
+ }
1290
+ }
1291
+ function consumePairingCode(state, options) {
1292
+ if (state.consumedAt)
1293
+ return null;
1294
+ if (normalizeMailbox(state.mailbox) !== normalizeMailbox(options.mailbox))
1295
+ return null;
1296
+ if (state.pairCode !== options.pairCode.trim())
1297
+ return null;
1298
+ if (new Date(state.expiresAt).getTime() <= (options.now ?? /* @__PURE__ */ new Date()).getTime())
1299
+ return null;
1300
+ return {
1301
+ ...state,
1302
+ pairCode: "",
1303
+ connectUrl: "",
1304
+ consumedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString()
1305
+ };
1306
+ }
1307
+ function createPairedSenderPolicy(request, pairedAt = /* @__PURE__ */ new Date()) {
1308
+ return {
1309
+ sender: normalizeMailbox(request.from),
1310
+ dispatchContextRules: normalizeDispatchContextRules(request.dispatchContextRules) ?? {},
1311
+ pairedAt: pairedAt.toISOString()
1312
+ };
1313
+ }
1314
+ function upsertPairedSenderPolicy(policies, policy) {
1315
+ const sender = normalizeMailbox(policy.sender);
1316
+ return [
1317
+ ...policies.filter((item) => normalizeMailbox(item.sender) !== sender),
1318
+ {
1319
+ ...policy,
1320
+ sender,
1321
+ dispatchContextRules: normalizeDispatchContextRules(policy.dispatchContextRules) ?? {}
1322
+ }
1323
+ ];
1324
+ }
1325
+ function matchPairedSenderPolicy(policies, sender, dispatchContext) {
1326
+ if (policies.length === 0) {
1327
+ return { allowed: false, reason: "no paired sender policies configured" };
1328
+ }
1329
+ const normalizedSender = normalizeMailbox(sender);
1330
+ const policy = policies.find((item) => normalizeMailbox(item.sender) === normalizedSender);
1331
+ if (!policy) {
1332
+ return { allowed: false, reason: `sender ${sender} is not paired` };
1333
+ }
1334
+ for (const [key, allowedValues] of Object.entries(policy.dispatchContextRules ?? {})) {
1335
+ if (!Array.isArray(allowedValues) || allowedValues.length === 0)
1336
+ continue;
1337
+ const observed = dispatchContext?.[key];
1338
+ if (!observed || !allowedValues.includes(observed)) {
1339
+ return { allowed: false, reason: `dispatchContext does not match paired sender policy for ${sender}` };
1340
+ }
1341
+ }
1342
+ return { allowed: true };
1343
+ }
1344
+
1345
+ // ../sdk/dist/smtp-sender.js
1118
1346
  import { createTransport } from "nodemailer";
1119
1347
  import { randomUUID } from "crypto";
1120
1348
  var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
1349
+ var HTTP_SEND_MAX_ATTEMPTS = 4;
1350
+ var HTTP_SEND_RETRY_BASE_DELAY_MS = 500;
1351
+ function sleep2(ms) {
1352
+ return new Promise((resolve) => setTimeout(resolve, ms));
1353
+ }
1354
+ function isRetryableHttpStatus(status) {
1355
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
1356
+ }
1357
+ function describeError2(err) {
1358
+ if (!(err instanceof Error))
1359
+ return String(err);
1360
+ const details = err;
1361
+ const parts = [err.message];
1362
+ if (details.code)
1363
+ parts.push(`code=${details.code}`);
1364
+ if (details.cause instanceof Error)
1365
+ parts.push(`cause=${describeError2(details.cause)}`);
1366
+ return parts.join(" | ");
1367
+ }
1121
1368
  function deriveMailboxServiceDefaults(email, baseUrl2) {
1122
1369
  const domain = email.split("@")[1]?.trim();
1123
1370
  const resolvedBaseUrl = baseUrl2?.trim() || (domain ? `https://${domain}` : void 0);
@@ -1130,6 +1377,7 @@ function deriveMailboxServiceDefaults(email, baseUrl2) {
1130
1377
  var SmtpSender = class _SmtpSender {
1131
1378
  config;
1132
1379
  transport;
1380
+ fetch;
1133
1381
  discoveredApiUrlPromise = null;
1134
1382
  jmapSessionPromise = null;
1135
1383
  sentMailboxIdPromise = null;
@@ -1142,12 +1390,16 @@ var SmtpSender = class _SmtpSender {
1142
1390
  password: config.password,
1143
1391
  httpBaseUrl: derived.httpBaseUrl,
1144
1392
  authToken: Buffer.from(`${config.email}:${config.password}`).toString("base64"),
1393
+ fetch: config.fetch,
1394
+ forceHttpSend: config.forceHttpSend,
1395
+ persistSentCopy: config.persistSentCopy,
1145
1396
  secure: config.secure,
1146
1397
  rejectUnauthorized: config.rejectUnauthorized
1147
1398
  });
1148
1399
  }
1149
1400
  constructor(config) {
1150
1401
  this.config = config;
1402
+ this.fetch = config.fetch ?? fetch;
1151
1403
  this.transport = createTransport({
1152
1404
  host: config.host,
1153
1405
  port: config.port,
@@ -1168,6 +1420,9 @@ var SmtpSender = class _SmtpSender {
1168
1420
  return email.split("@")[1]?.toLowerCase() ?? "";
1169
1421
  }
1170
1422
  shouldUseHttpFallback(to) {
1423
+ if (this.config.forceHttpSend) {
1424
+ return Boolean(this.config.httpBaseUrl && this.config.authToken);
1425
+ }
1171
1426
  return Boolean(this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to));
1172
1427
  }
1173
1428
  async resolveAampApiUrl() {
@@ -1177,7 +1432,7 @@ var SmtpSender = class _SmtpSender {
1177
1432
  }
1178
1433
  if (!this.discoveredApiUrlPromise) {
1179
1434
  this.discoveredApiUrlPromise = (async () => {
1180
- const discoveryRes = await fetch(`${base}/.well-known/aamp`);
1435
+ const discoveryRes = await this.fetch(`${base}/.well-known/aamp`);
1181
1436
  if (!discoveryRes.ok) {
1182
1437
  throw new Error(`AAMP discovery failed: ${discoveryRes.status}`);
1183
1438
  }
@@ -1199,33 +1454,49 @@ var SmtpSender = class _SmtpSender {
1199
1454
  if (!this.config.authToken) {
1200
1455
  throw new Error("HTTP send fallback is not configured");
1201
1456
  }
1202
- const apiUrl = new URL(await this.resolveAampApiUrl());
1203
- apiUrl.searchParams.set("action", "aamp.mailbox.send");
1204
- const res = await fetch(apiUrl, {
1205
- method: "POST",
1206
- headers: {
1207
- Authorization: `Basic ${this.config.authToken}`,
1208
- "Content-Type": "application/json"
1209
- },
1210
- body: JSON.stringify({
1211
- to: opts.to,
1212
- subject: opts.subject,
1213
- text: opts.text,
1214
- aampHeaders: opts.aampHeaders,
1215
- attachments: opts.attachments?.map((a) => ({
1216
- filename: a.filename,
1217
- contentType: a.contentType,
1218
- content: typeof a.content === "string" ? a.content : a.content.toString("base64")
1219
- }))
1220
- })
1221
- });
1222
- const data = await res.json().catch(() => ({}));
1223
- if (!res.ok) {
1224
- throw new Error(data.details || `HTTP send failed: ${res.status}`);
1457
+ let lastError = null;
1458
+ for (let attempt = 1; attempt <= HTTP_SEND_MAX_ATTEMPTS; attempt += 1) {
1459
+ const apiUrl = new URL(await this.resolveAampApiUrl());
1460
+ apiUrl.searchParams.set("action", "aamp.mailbox.send");
1461
+ try {
1462
+ const res = await this.fetch(apiUrl, {
1463
+ method: "POST",
1464
+ headers: {
1465
+ Authorization: `Basic ${this.config.authToken}`,
1466
+ "Content-Type": "application/json"
1467
+ },
1468
+ body: JSON.stringify({
1469
+ to: opts.to,
1470
+ subject: opts.subject,
1471
+ text: opts.text,
1472
+ aampHeaders: opts.aampHeaders,
1473
+ attachments: opts.attachments?.map((a) => ({
1474
+ filename: a.filename,
1475
+ contentType: a.contentType,
1476
+ content: typeof a.content === "string" ? a.content : a.content.toString("base64")
1477
+ }))
1478
+ })
1479
+ });
1480
+ const data = await res.json().catch(() => ({}));
1481
+ if (res.ok)
1482
+ return { messageId: data.messageId };
1483
+ lastError = new Error(data.details || `HTTP send failed: ${res.status}`);
1484
+ if (!isRetryableHttpStatus(res.status) || attempt === HTTP_SEND_MAX_ATTEMPTS)
1485
+ break;
1486
+ } catch (err) {
1487
+ lastError = err instanceof Error ? err : new Error(String(err));
1488
+ if (attempt === HTTP_SEND_MAX_ATTEMPTS) {
1489
+ lastError = new Error(`HTTP send failed after ${attempt} attempts: ${describeError2(lastError)}`);
1490
+ break;
1491
+ }
1492
+ }
1493
+ await sleep2(HTTP_SEND_RETRY_BASE_DELAY_MS * attempt);
1225
1494
  }
1226
- return { messageId: data.messageId };
1495
+ throw lastError ?? new Error("HTTP send failed");
1227
1496
  }
1228
1497
  canPersistSentCopy() {
1498
+ if (this.config.persistSentCopy === false)
1499
+ return false;
1229
1500
  return Boolean(this.config.httpBaseUrl && this.config.authToken);
1230
1501
  }
1231
1502
  getJmapAuthHeader() {
@@ -1241,7 +1512,7 @@ var SmtpSender = class _SmtpSender {
1241
1512
  }
1242
1513
  if (!this.jmapSessionPromise) {
1243
1514
  this.jmapSessionPromise = (async () => {
1244
- const res = await fetch(`${base}/.well-known/jmap`, {
1515
+ const res = await this.fetch(`${base}/.well-known/jmap`, {
1245
1516
  headers: { Authorization: this.getJmapAuthHeader() }
1246
1517
  });
1247
1518
  if (!res.ok) {
@@ -1267,7 +1538,7 @@ var SmtpSender = class _SmtpSender {
1267
1538
  }
1268
1539
  async jmapCall(methodCalls) {
1269
1540
  const session = await this.resolveJmapSession();
1270
- const res = await fetch(session.apiUrl, {
1541
+ const res = await this.fetch(session.apiUrl, {
1271
1542
  method: "POST",
1272
1543
  headers: {
1273
1544
  Authorization: this.getJmapAuthHeader(),
@@ -1364,7 +1635,6 @@ var SmtpSender = class _SmtpSender {
1364
1635
  taskId,
1365
1636
  priority: opts.priority,
1366
1637
  expiresAt: opts.expiresAt,
1367
- contextLinks: opts.contextLinks ?? [],
1368
1638
  dispatchContext: opts.dispatchContext,
1369
1639
  parentTaskId: opts.parentTaskId
1370
1640
  });
@@ -1372,13 +1642,11 @@ var SmtpSender = class _SmtpSender {
1372
1642
  from: this.config.user,
1373
1643
  to: opts.to,
1374
1644
  subject: `[AAMP Task] ${sanitize(opts.title)}`,
1375
- text: [
1645
+ text: opts.rawBodyText ?? [
1376
1646
  `Task: ${opts.title}`,
1377
1647
  `Task ID: ${taskId}`,
1378
1648
  `Priority: ${opts.priority ?? "normal"}`,
1379
1649
  opts.expiresAt ? `Expires At: ${opts.expiresAt}` : `Expires At: none`,
1380
- opts.contextLinks?.length ? `Context:
1381
- ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
1382
1650
  opts.bodyText ?? "",
1383
1651
  ``,
1384
1652
  `--- This email was sent by AAMP. Reply directly to submit your result. ---`
@@ -1440,7 +1708,7 @@ ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
1440
1708
  from: this.config.user,
1441
1709
  to: opts.to,
1442
1710
  subject: `[AAMP Result] Task ${opts.taskId} \u2014 ${opts.status}`,
1443
- text: [
1711
+ text: opts.rawBodyText ?? [
1444
1712
  `AAMP Task Result`,
1445
1713
  ``,
1446
1714
  `Task ID: ${opts.taskId}`,
@@ -1514,7 +1782,7 @@ Error: ${opts.errorMsg}` : ""
1514
1782
  from: this.config.user,
1515
1783
  to: opts.to,
1516
1784
  subject: `[AAMP Help] Task ${opts.taskId} needs assistance`,
1517
- text: [
1785
+ text: opts.rawBodyText ?? [
1518
1786
  `AAMP Task Help Request`,
1519
1787
  ``,
1520
1788
  `Task ID: ${opts.taskId}`,
@@ -1721,6 +1989,110 @@ Stream ID: ${opts.streamId}`,
1721
1989
  references: opts.inReplyTo
1722
1990
  });
1723
1991
  }
1992
+ async sendPairRequest(opts) {
1993
+ const taskId = opts.taskId ?? randomUUID();
1994
+ const aampHeaders = buildPairRequestHeaders({
1995
+ taskId,
1996
+ pairCode: opts.pairCode,
1997
+ dispatchContextRules: opts.dispatchContextRules ?? {}
1998
+ });
1999
+ const text = [
2000
+ "AAMP Pair Request",
2001
+ "",
2002
+ `Pair code: ${opts.pairCode}`,
2003
+ `Dispatch context rules: ${JSON.stringify(opts.dispatchContextRules ?? {})}`
2004
+ ].join("\n");
2005
+ const mailOpts = {
2006
+ from: this.config.user,
2007
+ to: opts.to,
2008
+ subject: "[AAMP Pair] Connection request",
2009
+ text,
2010
+ headers: aampHeaders
2011
+ };
2012
+ if (this.shouldUseHttpFallback(opts.to)) {
2013
+ const info2 = await this.sendViaHttp({
2014
+ to: opts.to,
2015
+ subject: mailOpts.subject,
2016
+ text,
2017
+ aampHeaders
2018
+ });
2019
+ await this.saveToSentBestEffort({
2020
+ from: this.config.user,
2021
+ to: opts.to,
2022
+ subject: mailOpts.subject,
2023
+ text,
2024
+ aampHeaders,
2025
+ messageId: info2.messageId
2026
+ });
2027
+ return { taskId, messageId: info2.messageId ?? "" };
2028
+ }
2029
+ const info = await this.transport.sendMail(mailOpts);
2030
+ await this.saveToSentBestEffort({
2031
+ from: this.config.user,
2032
+ to: opts.to,
2033
+ subject: mailOpts.subject,
2034
+ text,
2035
+ aampHeaders,
2036
+ messageId: info.messageId
2037
+ });
2038
+ return { taskId, messageId: info.messageId ?? "" };
2039
+ }
2040
+ async sendPairRespond(opts) {
2041
+ const aampHeaders = buildPairRespondHeaders({
2042
+ taskId: opts.taskId,
2043
+ success: opts.success,
2044
+ reason: opts.reason
2045
+ });
2046
+ const status = opts.success ? "completed" : "rejected";
2047
+ const text = [
2048
+ "AAMP Pair Response",
2049
+ "",
2050
+ `Task ID: ${opts.taskId}`,
2051
+ `Status: ${status}`,
2052
+ ...opts.reason?.trim() ? ["", `Reason: ${opts.reason.trim()}`] : []
2053
+ ].join("\n");
2054
+ const mailOpts = {
2055
+ from: this.config.user,
2056
+ to: opts.to,
2057
+ subject: `[AAMP Pair] ${status}`,
2058
+ text,
2059
+ headers: aampHeaders
2060
+ };
2061
+ if (opts.inReplyTo) {
2062
+ mailOpts.inReplyTo = opts.inReplyTo;
2063
+ mailOpts.references = opts.inReplyTo;
2064
+ }
2065
+ if (this.shouldUseHttpFallback(opts.to)) {
2066
+ const info2 = await this.sendViaHttp({
2067
+ to: opts.to,
2068
+ subject: mailOpts.subject,
2069
+ text,
2070
+ aampHeaders
2071
+ });
2072
+ await this.saveToSentBestEffort({
2073
+ from: this.config.user,
2074
+ to: opts.to,
2075
+ subject: mailOpts.subject,
2076
+ text,
2077
+ aampHeaders,
2078
+ messageId: info2.messageId,
2079
+ inReplyTo: opts.inReplyTo,
2080
+ references: opts.inReplyTo
2081
+ });
2082
+ return;
2083
+ }
2084
+ const info = await this.transport.sendMail(mailOpts);
2085
+ await this.saveToSentBestEffort({
2086
+ from: this.config.user,
2087
+ to: opts.to,
2088
+ subject: mailOpts.subject,
2089
+ text,
2090
+ aampHeaders,
2091
+ messageId: info.messageId,
2092
+ inReplyTo: opts.inReplyTo,
2093
+ references: opts.inReplyTo
2094
+ });
2095
+ }
1724
2096
  async sendCardQuery(opts) {
1725
2097
  const taskId = opts.taskId ?? randomUUID();
1726
2098
  const aampHeaders = buildCardQueryHeaders({ taskId });
@@ -1830,7 +2202,7 @@ Stream ID: ${opts.streamId}`,
1830
2202
  }
1831
2203
  };
1832
2204
 
1833
- // ../sdk/src/thread.js
2205
+ // ../sdk/dist/thread.js
1834
2206
  function singleLine(value, maxLength = 220) {
1835
2207
  const normalized = (value ?? "").replace(/\s+/g, " ").trim();
1836
2208
  if (!normalized)
@@ -1884,7 +2256,7 @@ function renderThreadHistoryForAgent(events, options = {}) {
1884
2256
  ].join("\n");
1885
2257
  }
1886
2258
 
1887
- // ../sdk/src/client.js
2259
+ // ../sdk/dist/client.js
1888
2260
  function buildRegisteredCommandDispatchPayload(opts) {
1889
2261
  const command = opts.command.trim();
1890
2262
  if (!command) {
@@ -1961,6 +2333,9 @@ var AampClient = class _AampClient extends TinyEmitter {
1961
2333
  password: config.smtpPassword,
1962
2334
  httpBaseUrl: config.httpSendBaseUrl ?? resolvedBaseUrl,
1963
2335
  authToken: mailboxToken,
2336
+ fetch: config.fetch,
2337
+ forceHttpSend: config.forceHttpSend,
2338
+ persistSentCopy: config.persistSentCopy,
1964
2339
  rejectUnauthorized: config.rejectUnauthorized
1965
2340
  });
1966
2341
  this.jmapClient.on("task.dispatch", (task) => {
@@ -1981,6 +2356,12 @@ var AampClient = class _AampClient extends TinyEmitter {
1981
2356
  this.jmapClient.on("task.stream.opened", (stream) => {
1982
2357
  this.emit("task.stream.opened", stream);
1983
2358
  });
2359
+ this.jmapClient.on("pair.request", (request) => {
2360
+ this.emit("pair.request", request);
2361
+ });
2362
+ this.jmapClient.on("pair.respond", (response) => {
2363
+ this.emit("pair.respond", response);
2364
+ });
1984
2365
  this.jmapClient.on("card.query", (query) => {
1985
2366
  this.emit("card.query", query);
1986
2367
  });
@@ -2018,12 +2399,23 @@ var AampClient = class _AampClient extends TinyEmitter {
2018
2399
  smtpPassword: config.smtpPassword,
2019
2400
  reconnectInterval: config.reconnectInterval,
2020
2401
  taskDispatchConcurrency: config.taskDispatchConcurrency,
2402
+ fetch: config.fetch,
2403
+ forceHttpSend: config.forceHttpSend,
2404
+ persistSentCopy: config.persistSentCopy,
2021
2405
  rejectUnauthorized: config.rejectUnauthorized
2022
2406
  });
2023
2407
  }
2024
- static async discoverAampService(aampHost) {
2408
+ static createPairingCode = createPairingCode;
2409
+ static buildPairingUrl = buildPairingUrl;
2410
+ static parsePairingUrl = parsePairingUrl;
2411
+ static isPairingUrl = isPairingUrl;
2412
+ static consumePairingCode = consumePairingCode;
2413
+ static createPairedSenderPolicy = createPairedSenderPolicy;
2414
+ static upsertPairedSenderPolicy = upsertPairedSenderPolicy;
2415
+ static matchPairedSenderPolicy = matchPairedSenderPolicy;
2416
+ static async discoverAampService(aampHost, fetchImpl = fetch) {
2025
2417
  const base = aampHost.replace(/\/$/, "");
2026
- const res = await fetch(`${base}/.well-known/aamp`);
2418
+ const res = await fetchImpl(`${base}/.well-known/aamp`);
2027
2419
  if (!res.ok) {
2028
2420
  throw new Error(`AAMP discovery failed: ${res.status} ${res.statusText}`);
2029
2421
  }
@@ -2034,7 +2426,8 @@ var AampClient = class _AampClient extends TinyEmitter {
2034
2426
  return discovery;
2035
2427
  }
2036
2428
  static async callDiscoveredApi(base, opts) {
2037
- const discovery = await _AampClient.discoverAampService(base);
2429
+ const fetchImpl = opts.fetch ?? fetch;
2430
+ const discovery = await _AampClient.discoverAampService(base, fetchImpl);
2038
2431
  const apiUrl = new URL(discovery.api.url, `${base}/`);
2039
2432
  apiUrl.searchParams.set("action", opts.action);
2040
2433
  for (const [key, value] of Object.entries(opts.query ?? {})) {
@@ -2042,7 +2435,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2042
2435
  continue;
2043
2436
  apiUrl.searchParams.set(key, String(value));
2044
2437
  }
2045
- return fetch(apiUrl, {
2438
+ return fetchImpl(apiUrl, {
2046
2439
  method: opts.method ?? "GET",
2047
2440
  headers: {
2048
2441
  ...opts.authToken ? { Authorization: `Basic ${opts.authToken}` } : {},
@@ -2088,6 +2481,22 @@ var AampClient = class _AampClient extends TinyEmitter {
2088
2481
  baseUrl: base
2089
2482
  };
2090
2483
  }
2484
+ static async checkMailbox(opts) {
2485
+ const base = opts.aampHost.replace(/\/$/, "");
2486
+ const res = await _AampClient.callDiscoveredApi(base, {
2487
+ action: "aamp.mailbox.check",
2488
+ query: { email: opts.email }
2489
+ });
2490
+ if (!res.ok) {
2491
+ const body = await res.text().catch(() => "");
2492
+ throw new Error(`Mailbox check failed: ${res.status} ${body || res.statusText}`);
2493
+ }
2494
+ const payload = await res.json();
2495
+ return {
2496
+ aamp: Boolean(payload.aamp),
2497
+ ...payload.domain ? { domain: payload.domain } : {}
2498
+ };
2499
+ }
2091
2500
  // =====================================================
2092
2501
  // Lifecycle
2093
2502
  // =====================================================
@@ -2134,7 +2543,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2134
2543
  rawBodyText: JSON.stringify(payload, null, 2),
2135
2544
  priority: opts.priority,
2136
2545
  expiresAt: opts.expiresAt,
2137
- contextLinks: opts.contextLinks,
2546
+ sessionKey: opts.sessionKey,
2138
2547
  dispatchContext: opts.dispatchContext,
2139
2548
  parentTaskId: opts.parentTaskId,
2140
2549
  attachments: opts.attachments
@@ -2158,6 +2567,12 @@ var AampClient = class _AampClient extends TinyEmitter {
2158
2567
  async sendStreamOpened(opts) {
2159
2568
  return this.smtpSender.sendStreamOpened(opts);
2160
2569
  }
2570
+ async sendPairRequest(opts) {
2571
+ return this.smtpSender.sendPairRequest(opts);
2572
+ }
2573
+ async sendPairRespond(opts) {
2574
+ return this.smtpSender.sendPairRespond(opts);
2575
+ }
2161
2576
  async sendCardQuery(opts) {
2162
2577
  return this.smtpSender.sendCardQuery(opts);
2163
2578
  }
@@ -2171,6 +2586,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2171
2586
  action: "aamp.directory.upsert",
2172
2587
  method: "POST",
2173
2588
  authToken: mailboxToken,
2589
+ fetch: this.config.fetch,
2174
2590
  body: opts
2175
2591
  });
2176
2592
  if (!res.ok) {
@@ -2186,6 +2602,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2186
2602
  const res = await _AampClient.callDiscoveredApi(base, {
2187
2603
  action: "aamp.directory.list",
2188
2604
  authToken: mailboxToken,
2605
+ fetch: this.config.fetch,
2189
2606
  query: {
2190
2607
  scope: opts.scope,
2191
2608
  includeSelf: opts.includeSelf,
@@ -2205,6 +2622,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2205
2622
  const res = await _AampClient.callDiscoveredApi(base, {
2206
2623
  action: "aamp.directory.search",
2207
2624
  authToken: mailboxToken,
2625
+ fetch: this.config.fetch,
2208
2626
  query: {
2209
2627
  q: opts.query,
2210
2628
  scope: opts.scope,
@@ -2225,6 +2643,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2225
2643
  const res = await _AampClient.callDiscoveredApi(base, {
2226
2644
  action: "aamp.mailbox.thread",
2227
2645
  authToken: mailboxToken,
2646
+ fetch: this.config.fetch,
2228
2647
  query: {
2229
2648
  taskId,
2230
2649
  includeStreamOpened: opts.includeStreamOpened
@@ -2250,7 +2669,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2250
2669
  };
2251
2670
  }
2252
2671
  async resolveStreamCapability() {
2253
- const discovery = await _AampClient.discoverAampService(this.config.baseUrl);
2672
+ const discovery = await _AampClient.discoverAampService(this.config.baseUrl, this.config.fetch);
2254
2673
  const stream = discovery.capabilities?.stream;
2255
2674
  if (!stream?.transport) {
2256
2675
  throw new Error("AAMP stream capability is not available on this service");
@@ -2263,6 +2682,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2263
2682
  action: stream.createAction ?? "aamp.stream.create",
2264
2683
  method: "POST",
2265
2684
  authToken: this.config.mailboxToken,
2685
+ fetch: this.config.fetch,
2266
2686
  body: opts
2267
2687
  });
2268
2688
  if (!res.ok) {
@@ -2309,6 +2729,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2309
2729
  action: stream.appendAction ?? "aamp.stream.append",
2310
2730
  method: "POST",
2311
2731
  authToken: this.config.mailboxToken,
2732
+ fetch: this.config.fetch,
2312
2733
  body: opts
2313
2734
  });
2314
2735
  if (!res.ok) {
@@ -2414,6 +2835,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2414
2835
  action: stream.closeAction ?? "aamp.stream.close",
2415
2836
  method: "POST",
2416
2837
  authToken: this.config.mailboxToken,
2838
+ fetch: this.config.fetch,
2417
2839
  body: opts
2418
2840
  });
2419
2841
  if (!res.ok) {
@@ -2427,6 +2849,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2427
2849
  const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2428
2850
  action: stream.getAction ?? "aamp.stream.get",
2429
2851
  authToken: this.config.mailboxToken,
2852
+ fetch: this.config.fetch,
2430
2853
  query: {
2431
2854
  ...opts.taskId ? { taskId: opts.taskId } : {},
2432
2855
  ...opts.streamId ? { streamId: opts.streamId } : {}
@@ -2453,7 +2876,8 @@ var AampClient = class _AampClient extends TinyEmitter {
2453
2876
  if (opts.signal) {
2454
2877
  opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
2455
2878
  }
2456
- const res = await fetch(url, {
2879
+ const fetchImpl = this.config.fetch ?? fetch;
2880
+ const res = await fetchImpl(url, {
2457
2881
  headers: {
2458
2882
  Authorization: `Basic ${this.config.mailboxToken}`,
2459
2883
  Accept: "text/event-stream"
@@ -2564,12 +2988,19 @@ import { readFileSync as readFileSync2 } from "node:fs";
2564
2988
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2565
2989
  import { dirname, join } from "node:path";
2566
2990
  import { homedir } from "node:os";
2991
+ import { randomBytes as randomBytes2 } from "node:crypto";
2567
2992
  function defaultCredentialsPath() {
2568
2993
  return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".credentials.json");
2569
2994
  }
2570
2995
  function defaultTaskStatePath() {
2571
2996
  return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".task-state.json");
2572
2997
  }
2998
+ function defaultPairingPath() {
2999
+ return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".pairing.json");
3000
+ }
3001
+ function defaultSenderPoliciesPath() {
3002
+ return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".sender-policies.json");
3003
+ }
2573
3004
  function loadCachedIdentity(file) {
2574
3005
  const resolved = file ?? defaultCredentialsPath();
2575
3006
  if (!existsSync(resolved))
@@ -2616,6 +3047,66 @@ function saveTaskState(state, file) {
2616
3047
  terminalTaskIds: state.terminalTaskIds ?? []
2617
3048
  }, null, 2), "utf-8");
2618
3049
  }
3050
+ function createPairingCode2(params) {
3051
+ const pairCode = randomBytes2(6).toString("base64url");
3052
+ const expiresAt = new Date(Date.now() + (params.ttlSeconds ?? 300) * 1e3).toISOString();
3053
+ const connectUrl = `aamp://connect?mailbox=${encodeURIComponent(params.mailbox.toLowerCase())}&pair_code=${encodeURIComponent(pairCode)}`;
3054
+ const state = {
3055
+ mailbox: params.mailbox.toLowerCase(),
3056
+ pairCode,
3057
+ expiresAt,
3058
+ connectUrl
3059
+ };
3060
+ const resolved = params.file ?? defaultPairingPath();
3061
+ mkdirSync(dirname(resolved), { recursive: true });
3062
+ writeFileSync(resolved, JSON.stringify(state, null, 2), "utf-8");
3063
+ return state;
3064
+ }
3065
+ function consumePairingCode2(params) {
3066
+ const resolved = params.file ?? defaultPairingPath();
3067
+ if (!existsSync(resolved))
3068
+ return null;
3069
+ const state = JSON.parse(readFileSync(resolved, "utf-8"));
3070
+ if (state.mailbox.toLowerCase() !== params.mailbox.toLowerCase())
3071
+ return null;
3072
+ if (state.pairCode !== params.pairCode)
3073
+ return null;
3074
+ if (state.consumedAt)
3075
+ return null;
3076
+ if (new Date(state.expiresAt).getTime() <= Date.now())
3077
+ return null;
3078
+ writeFileSync(resolved, JSON.stringify({ ...state, pairCode: "", consumedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf-8");
3079
+ return state;
3080
+ }
3081
+ function isPairedSenderPolicy(value) {
3082
+ if (!value || typeof value !== "object")
3083
+ return false;
3084
+ const policy = value;
3085
+ return typeof policy.sender === "string" && typeof policy.dispatchContextRules === "object" && typeof policy.pairedAt === "string";
3086
+ }
3087
+ function loadPairedSenderPolicies(file) {
3088
+ const resolved = file ?? defaultSenderPoliciesPath();
3089
+ if (!existsSync(resolved))
3090
+ return [];
3091
+ try {
3092
+ const data = JSON.parse(readFileSync(resolved, "utf-8"));
3093
+ return Array.isArray(data) ? data.filter(isPairedSenderPolicy) : [];
3094
+ } catch {
3095
+ return [];
3096
+ }
3097
+ }
3098
+ function addPairedSenderPolicy(file, policy) {
3099
+ const resolved = file ?? defaultSenderPoliciesPath();
3100
+ const policies = loadPairedSenderPolicies(resolved);
3101
+ const normalizedSender = policy.sender.toLowerCase();
3102
+ const next = [
3103
+ ...policies.filter((item) => item.sender.toLowerCase() !== normalizedSender),
3104
+ { ...policy, sender: normalizedSender }
3105
+ ];
3106
+ mkdirSync(dirname(resolved), { recursive: true });
3107
+ writeFileSync(resolved, JSON.stringify(next, null, 2), "utf-8");
3108
+ return next;
3109
+ }
2619
3110
  function ensureDir(dir) {
2620
3111
  mkdirSync(dir, { recursive: true });
2621
3112
  }
@@ -2630,7 +3121,7 @@ function writeBinaryFile(path, content) {
2630
3121
  // src/index.ts
2631
3122
  function matchSenderPolicy(task, senderPolicies) {
2632
3123
  if (!senderPolicies?.length)
2633
- return { allowed: true };
3124
+ return { allowed: false, reason: "no configured senderPolicies" };
2634
3125
  const sender = task.from.toLowerCase();
2635
3126
  const policy = senderPolicies.find((item) => item.sender.trim().toLowerCase() === sender);
2636
3127
  if (!policy) {
@@ -2659,12 +3150,62 @@ function matchSenderPolicy(task, senderPolicies) {
2659
3150
  }
2660
3151
  return { allowed: true };
2661
3152
  }
3153
+ function rulesMatch(rules, dispatchContext) {
3154
+ for (const [key, allowedValues] of Object.entries(rules ?? {})) {
3155
+ if (!Array.isArray(allowedValues) || allowedValues.length === 0)
3156
+ continue;
3157
+ const observed = dispatchContext?.[key];
3158
+ if (!observed || !allowedValues.includes(observed))
3159
+ return false;
3160
+ }
3161
+ return true;
3162
+ }
3163
+ function matchPairedSenderPolicy2(task, senderPolicies) {
3164
+ if (senderPolicies.length === 0)
3165
+ return { allowed: false, reason: "no paired sender policies configured" };
3166
+ const sender = task.from.toLowerCase();
3167
+ const policy = senderPolicies.find((item) => item.sender.trim().toLowerCase() === sender);
3168
+ if (!policy) {
3169
+ return { allowed: false, reason: `sender ${task.from} is not paired` };
3170
+ }
3171
+ if (!rulesMatch(policy.dispatchContextRules, task.dispatchContext)) {
3172
+ return { allowed: false, reason: `dispatchContext does not match paired sender policy for ${task.from}` };
3173
+ }
3174
+ return { allowed: true };
3175
+ }
3176
+ function matchCombinedSenderPolicy(task, configuredPolicies, pairedPolicies) {
3177
+ const hasConfiguredPolicies = (configuredPolicies?.length ?? 0) > 0;
3178
+ const hasPairedPolicies = pairedPolicies.length > 0;
3179
+ if (!hasConfiguredPolicies && !hasPairedPolicies) {
3180
+ return { allowed: false, reason: "no sender policy configured" };
3181
+ }
3182
+ const configuredDecision = hasConfiguredPolicies ? matchSenderPolicy(task, configuredPolicies) : { allowed: false, reason: "no configured senderPolicies" };
3183
+ const pairedDecision = hasPairedPolicies ? matchPairedSenderPolicy2(task, pairedPolicies) : { allowed: false, reason: "no paired sender policies configured" };
3184
+ if (pairedDecision.allowed)
3185
+ return pairedDecision;
3186
+ if (configuredDecision.allowed)
3187
+ return configuredDecision;
3188
+ return configuredDecision.reason ? configuredDecision : pairedDecision;
3189
+ }
2662
3190
  function baseUrl(aampHost) {
2663
3191
  if (aampHost.startsWith("http://") || aampHost.startsWith("https://")) {
2664
3192
  return aampHost.replace(/\/$/, "");
2665
3193
  }
2666
3194
  return `https://${aampHost}`;
2667
3195
  }
3196
+ async function renderTerminalQr(value) {
3197
+ try {
3198
+ const qrcode = await import("qrcode-terminal");
3199
+ const generator = qrcode.default?.generate ?? qrcode.generate;
3200
+ if (!generator)
3201
+ return "";
3202
+ return await new Promise((resolve) => {
3203
+ generator(value, { small: true }, (qr) => resolve(qr));
3204
+ });
3205
+ } catch {
3206
+ return "";
3207
+ }
3208
+ }
2668
3209
  var pendingTasks = /* @__PURE__ */ new Map();
2669
3210
  var activeTaskStreams = /* @__PURE__ */ new Map();
2670
3211
  var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
@@ -2688,6 +3229,7 @@ var transportMonitorTimer = null;
2688
3229
  var historicalReconcileCompleted = false;
2689
3230
  var channelRuntime = null;
2690
3231
  var channelCfg = null;
3232
+ var pairedSenderPolicies = [];
2691
3233
  async function ensureTaskStream(task) {
2692
3234
  if (!aampClient?.isConnected())
2693
3235
  return null;
@@ -2756,17 +3298,8 @@ function isTaskAwaitingHelpReply(task) {
2756
3298
  return task.awaitingHelpReply === true;
2757
3299
  }
2758
3300
  function isConversationalTask(task) {
2759
- return task.dispatchContext?.source === "feishu";
2760
- }
2761
- function firstDispatchContextValue(context, keys) {
2762
- if (!context)
2763
- return void 0;
2764
- for (const key of keys) {
2765
- const value = context[key]?.trim();
2766
- if (value)
2767
- return value;
2768
- }
2769
- return void 0;
3301
+ const source = task.dispatchContext?.source?.trim().toLowerCase();
3302
+ return source === "feishu" || source === "wechat";
2770
3303
  }
2771
3304
  function threadAlreadyTerminal(events) {
2772
3305
  return (events ?? []).some(
@@ -2814,8 +3347,8 @@ function buildOpenClawMainSessionKey(mainKey, config) {
2814
3347
  function buildAampConversationSessionKey(value, config) {
2815
3348
  return buildOpenClawMainSessionKey(`${AAMP_SESSION_PREFIX}default:${value}`, config);
2816
3349
  }
2817
- function buildAampStickySessionKey(dispatchContext, config) {
2818
- const stickyValue = firstDispatchContextValue(dispatchContext, ["session_key", "conversation_key", "thread_key"]);
3350
+ function buildAampStickySessionKey(sessionKey, config) {
3351
+ const stickyValue = sessionKey?.trim();
2819
3352
  if (!stickyValue)
2820
3353
  return void 0;
2821
3354
  return buildAampConversationSessionKey(`session:${stickyValue}`, config);
@@ -2827,10 +3360,10 @@ function buildAampWakeSessionKey(kind, id) {
2827
3360
  return `${AAMP_SESSION_PREFIX}wake:${kind}:${id}`;
2828
3361
  }
2829
3362
  function buildSessionKeyForPendingTask(task, config) {
2830
- return buildAampStickySessionKey(task.dispatchContext, config) ?? buildAampTaskSessionKey(task.taskId, config);
3363
+ return buildAampStickySessionKey(task.sessionKey, config) ?? buildAampTaskSessionKey(task.taskId, config);
2831
3364
  }
2832
3365
  function buildWakeSessionKeyForPendingTask(task, config) {
2833
- return buildAampStickySessionKey(task.dispatchContext, config) ?? buildAampWakeSessionKey("task", task.taskId);
3366
+ return buildAampStickySessionKey(task.sessionKey, config) ?? buildAampWakeSessionKey("task", task.taskId);
2834
3367
  }
2835
3368
  function findPendingEntryForSession(sessionKey, config) {
2836
3369
  if (typeof sessionKey !== "string" || !isAampSessionKey(sessionKey))
@@ -2939,7 +3472,6 @@ function queuePendingTask(task) {
2939
3472
  threadContextText: task.threadContextText ?? "",
2940
3473
  priority: task.priority ?? "normal",
2941
3474
  ...task.expiresAt ? { expiresAt: task.expiresAt } : {},
2942
- contextLinks: task.contextLinks ?? [],
2943
3475
  messageId: task.messageId ?? "",
2944
3476
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
2945
3477
  });
@@ -2952,39 +3484,15 @@ function queuePendingTask(task) {
2952
3484
  }
2953
3485
  async function registerNode(cfg) {
2954
3486
  const slug = (cfg.slug ?? "openclaw-agent").toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "");
2955
- const base = baseUrl(cfg.aampHost);
2956
- const discoveryRes = await fetch(`${base}/.well-known/aamp`);
2957
- if (!discoveryRes.ok) {
2958
- throw new Error(`AAMP discovery failed (${discoveryRes.status}): ${discoveryRes.statusText}`);
2959
- }
2960
- const discovery = await discoveryRes.json();
2961
- const apiUrl = discovery.api?.url;
2962
- if (!apiUrl) {
2963
- throw new Error("AAMP discovery did not return api.url");
2964
- }
2965
- const apiBase = new URL(apiUrl, `${base}/`).toString();
2966
- const res = await fetch(`${apiBase}?action=aamp.mailbox.register`, {
2967
- method: "POST",
2968
- headers: { "Content-Type": "application/json" },
2969
- body: JSON.stringify({ slug, description: "OpenClaw AAMP agent node" })
3487
+ const credData = await AampClient.registerMailbox({
3488
+ aampHost: cfg.aampHost,
3489
+ slug,
3490
+ description: "OpenClaw AAMP agent node"
2970
3491
  });
2971
- if (!res.ok) {
2972
- const err = await res.json().catch(() => ({}));
2973
- throw new Error(`AAMP registration failed (${res.status}): ${err.error ?? res.statusText}`);
2974
- }
2975
- const regData = await res.json();
2976
- const credRes = await fetch(
2977
- `${apiBase}?action=aamp.mailbox.credentials&code=${encodeURIComponent(regData.registrationCode)}`
2978
- );
2979
- if (!credRes.ok) {
2980
- const err = await credRes.json().catch(() => ({}));
2981
- throw new Error(`AAMP credential exchange failed (${credRes.status}): ${err.error ?? credRes.statusText}`);
2982
- }
2983
- const credData = await credRes.json();
2984
3492
  return {
2985
3493
  email: credData.email,
2986
- mailboxToken: credData.mailbox.token,
2987
- smtpPassword: credData.smtp.password
3494
+ mailboxToken: credData.mailboxToken,
3495
+ smtpPassword: credData.smtpPassword
2988
3496
  };
2989
3497
  }
2990
3498
  async function resolveIdentity(cfg) {
@@ -3026,6 +3534,14 @@ var src_default = {
3026
3534
  type: "string",
3027
3535
  description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json. Delete this file to force re-registration with a new mailbox."
3028
3536
  },
3537
+ pairingFile: {
3538
+ type: "string",
3539
+ description: "Absolute path to store the current one-time AAMP pairing code. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.pairing.json."
3540
+ },
3541
+ senderPoliciesFile: {
3542
+ type: "string",
3543
+ description: "Absolute path to persist senders approved by pair.request. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.sender-policies.json."
3544
+ },
3029
3545
  senderPolicies: {
3030
3546
  type: "array",
3031
3547
  description: "Per-sender authorization policies. Each sender can optionally require specific X-AAMP-Dispatch-Context key/value pairs before a task is accepted.",
@@ -3116,6 +3632,70 @@ var src_default = {
3116
3632
  });
3117
3633
  api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
3118
3634
  }
3635
+ async function sendPairResponse(request, success, reason) {
3636
+ if (!aampClient)
3637
+ return;
3638
+ try {
3639
+ await aampClient.sendPairRespond({
3640
+ to: request.from,
3641
+ taskId: request.taskId,
3642
+ success,
3643
+ reason,
3644
+ inReplyTo: request.messageId
3645
+ });
3646
+ } catch (err) {
3647
+ api.logger.warn(`[AAMP] Failed to send pair.respond to ${request.from}: ${err.message}`);
3648
+ }
3649
+ }
3650
+ async function handlePairRequest(request) {
3651
+ if (!agentEmail)
3652
+ return;
3653
+ if (request.to.trim().toLowerCase() !== agentEmail.trim().toLowerCase())
3654
+ return;
3655
+ const consumed = consumePairingCode2({
3656
+ file: cfg.pairingFile ?? defaultPairingPath(),
3657
+ mailbox: agentEmail,
3658
+ pairCode: request.pairCode
3659
+ });
3660
+ if (!consumed) {
3661
+ const reason = "invalid or expired pair code";
3662
+ api.logger.warn(`[AAMP] Rejected pair.request from ${request.from}: ${reason}`);
3663
+ await sendPairResponse(request, false, reason);
3664
+ return;
3665
+ }
3666
+ pairedSenderPolicies = addPairedSenderPolicy(cfg.senderPoliciesFile ?? defaultSenderPoliciesPath(), {
3667
+ sender: request.from.trim().toLowerCase(),
3668
+ dispatchContextRules: request.dispatchContextRules ?? {},
3669
+ pairedAt: (/* @__PURE__ */ new Date()).toISOString()
3670
+ });
3671
+ api.logger.info(`[AAMP] Paired sender ${request.from}; sender policy saved to ${cfg.senderPoliciesFile ?? defaultSenderPoliciesPath()}`);
3672
+ await sendPairResponse(request, true);
3673
+ }
3674
+ async function renderPairingCodeForCurrentAgent() {
3675
+ const identity = agentEmail ? { email: agentEmail } : loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
3676
+ const email = identity?.email?.trim();
3677
+ if (!email) {
3678
+ return "Error: AAMP mailbox identity is not ready. Start the AAMP plugin service first.";
3679
+ }
3680
+ const pairing = createPairingCode2({
3681
+ mailbox: email,
3682
+ file: cfg.pairingFile ?? defaultPairingPath()
3683
+ });
3684
+ const qr = await renderTerminalQr(pairing.connectUrl);
3685
+ api.logger.info(`[AAMP] Pair with AAMP App before ${pairing.expiresAt}: ${pairing.connectUrl}`);
3686
+ if (qr)
3687
+ api.logger.info(`
3688
+ ${qr}`);
3689
+ return [
3690
+ `Pair ${email} with AAMP App or another AAMP runtime.`,
3691
+ `Expires: ${pairing.expiresAt}`,
3692
+ qr ? `
3693
+ Scan this QR code:
3694
+ ${qr}` : "\nCould not render a terminal QR code.",
3695
+ `
3696
+ Pairing URL: ${pairing.connectUrl}`
3697
+ ].join("\n");
3698
+ }
3119
3699
  function wakeAgentForPendingTask(task) {
3120
3700
  const fallbackSessionKey = buildWakeSessionKeyForPendingTask(task, api.config);
3121
3701
  const openClawSessionKey = buildSessionKeyForPendingTask(task, api.config);
@@ -3199,6 +3779,16 @@ var src_default = {
3199
3779
  lastTransportMode = "disconnected";
3200
3780
  lastLoggedTransportMode = "disconnected";
3201
3781
  api.logger.info(`[AAMP] Mailbox identity ready \u2014 ${agentEmail}`);
3782
+ pairedSenderPolicies = loadPairedSenderPolicies(cfg.senderPoliciesFile ?? defaultSenderPoliciesPath());
3783
+ const pairing = createPairingCode2({
3784
+ mailbox: agentEmail,
3785
+ file: cfg.pairingFile ?? defaultPairingPath()
3786
+ });
3787
+ api.logger.info(`[AAMP] Pair with AAMP App before ${pairing.expiresAt}: ${pairing.connectUrl}`);
3788
+ const qr = await renderTerminalQr(pairing.connectUrl);
3789
+ if (qr)
3790
+ api.logger.info(`
3791
+ ${qr}`);
3202
3792
  const base = baseUrl(cfg.aampHost);
3203
3793
  aampClient = AampClient.fromMailboxIdentity({
3204
3794
  email: identity.email,
@@ -3217,7 +3807,7 @@ var src_default = {
3217
3807
  api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
3218
3808
  return;
3219
3809
  }
3220
- const decision = matchSenderPolicy(task, cfg.senderPolicies);
3810
+ const decision = matchCombinedSenderPolicy(task, cfg.senderPolicies, pairedSenderPolicies);
3221
3811
  if (!decision.allowed) {
3222
3812
  api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
3223
3813
  void aampClient.sendResult({
@@ -3274,6 +3864,11 @@ var src_default = {
3274
3864
  api.logger.info(`[AAMP] Cancelled task ${cancel.taskId} \u2014 removed from pending queue`);
3275
3865
  }
3276
3866
  });
3867
+ aampClient.on("pair.request", (request) => {
3868
+ void handlePairRequest(request).catch((err) => {
3869
+ api.logger.warn(`[AAMP] Failed to handle pair.request: ${err.message}`);
3870
+ });
3871
+ });
3277
3872
  aampClient.on("task.result", (result) => {
3278
3873
  if (result.from.toLowerCase() === agentEmail.toLowerCase())
3279
3874
  return;
@@ -3341,7 +3936,6 @@ ${truncatedOutput}${attachmentInfo}` : `Agent ${result.from} rejected the sub-ta
3341
3936
 
3342
3937
  Reason: ${result.errorMsg ?? "unknown"}`,
3343
3938
  priority: "urgent",
3344
- contextLinks: [],
3345
3939
  messageId: "",
3346
3940
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
3347
3941
  });
@@ -3424,7 +4018,6 @@ Question: ${help.question}
3424
4018
  Blocked reason: ${help.blockedReason}${help.suggestedOptions?.length ? `
3425
4019
  Suggested options: ${help.suggestedOptions.join(", ")}` : ""}`,
3426
4020
  priority: "urgent",
3427
- contextLinks: [],
3428
4021
  messageId: "",
3429
4022
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
3430
4023
  });
@@ -3726,8 +4319,6 @@ ${Object.entries(task.dispatchContext).map(([key, value]) => ` - ${key}: ${valu
3726
4319
  task.threadContextText ? `${task.threadContextText}` : "",
3727
4320
  task.bodyText ? `Latest user message:
3728
4321
  ${task.bodyText}` : "",
3729
- task.contextLinks.length ? `Context Links:
3730
- ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3731
4322
  task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
3732
4323
  `Received: ${task.receivedAt}`,
3733
4324
  otherActionableTasks.length > 0 ? `
@@ -3768,8 +4359,6 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3768
4359
  task.threadContextText ? `${task.threadContextText}` : "",
3769
4360
  task.bodyText ? `Description:
3770
4361
  ${task.bodyText}` : "",
3771
- task.contextLinks.length ? `Context Links:
3772
- ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3773
4362
  task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
3774
4363
  `Received: ${task.receivedAt}`,
3775
4364
  otherActionableTasks.length > 0 ? `
@@ -4091,6 +4680,14 @@ ${lines.join("\n")}`
4091
4680
  };
4092
4681
  }
4093
4682
  }, { name: "aamp_pending_tasks" });
4683
+ api.registerTool({
4684
+ name: "aamp_pairing_code",
4685
+ description: "Generate a fresh five-minute AAMP pairing code for this OpenClaw agent and show a QR code. Use this when the user asks to pair AAMP App or another AAMP runtime with this agent.",
4686
+ parameters: { type: "object", properties: {} },
4687
+ execute: async () => ({
4688
+ content: [{ type: "text", text: await renderPairingCodeForCurrentAgent() }]
4689
+ })
4690
+ }, { name: "aamp_pairing_code" });
4094
4691
  api.registerTool({
4095
4692
  name: "aamp_cancel_task",
4096
4693
  description: "Cancel a pending AAMP task and notify the dispatcher.",
@@ -4138,11 +4735,6 @@ ${lines.join("\n")}`
4138
4735
  parentTaskId: { type: "string", description: "If you are processing a pending AAMP task, pass its Task ID here to establish parent-child nesting. Omit for top-level tasks." },
4139
4736
  priority: { type: "string", enum: ["urgent", "high", "normal"], description: "Task priority (optional)" },
4140
4737
  expiresAt: { type: "string", description: "Absolute expiry time in ISO 8601 format (optional)" },
4141
- contextLinks: {
4142
- type: "array",
4143
- items: { type: "string" },
4144
- description: "URLs providing context (optional)"
4145
- },
4146
4738
  attachments: {
4147
4739
  type: "array",
4148
4740
  description: "File attachments. Each item: { filename, contentType, path (local file path) }",
@@ -4177,7 +4769,6 @@ ${lines.join("\n")}`
4177
4769
  parentTaskId: params.parentTaskId,
4178
4770
  priority: params.priority,
4179
4771
  expiresAt: params.expiresAt,
4180
- contextLinks: params.contextLinks,
4181
4772
  attachments
4182
4773
  });
4183
4774
  dispatchedSubtasks.set(result.taskId, {
@@ -4207,17 +4798,9 @@ ${lines.join("\n")}`
4207
4798
  const dir = "/tmp/aamp-files";
4208
4799
  ensureDir(dir);
4209
4800
  const downloaded = [];
4210
- const base = baseUrl(cfg.aampHost);
4211
- const identity = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
4212
- const authHeader = identity ? `Basic ${Buffer.from(identity.email + ":" + identity.smtpPassword).toString("base64")}` : "";
4213
4801
  for (const att of r.attachments) {
4214
4802
  try {
4215
- const dlUrl = `${base}/jmap/download/n/${encodeURIComponent(att.blobId)}/${encodeURIComponent(att.filename)}?accept=application/octet-stream`;
4216
- api.logger.info(`[AAMP] Fetching ${dlUrl}`);
4217
- const dlRes = await fetch(dlUrl, { headers: { Authorization: authHeader } });
4218
- if (!dlRes.ok)
4219
- throw new Error(`HTTP ${dlRes.status}`);
4220
- const buffer = Buffer.from(await dlRes.arrayBuffer());
4803
+ const buffer = await aampClient.downloadBlob(att.blobId, att.filename);
4221
4804
  const filepath = `${dir}/${att.filename}`;
4222
4805
  writeBinaryFile(filepath, buffer);
4223
4806
  downloaded.push(`${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
@@ -4289,18 +4872,10 @@ Question: ${h.question}`,
4289
4872
  return { content: [{ type: "text", text: "Error: email parameter is required" }] };
4290
4873
  }
4291
4874
  try {
4292
- const discoveryRes = await fetch(`${base}/.well-known/aamp`);
4293
- if (!discoveryRes.ok)
4294
- throw new Error(`HTTP ${discoveryRes.status}`);
4295
- const discovery = await discoveryRes.json();
4296
- const apiUrl = discovery.api?.url;
4297
- if (!apiUrl)
4298
- throw new Error("AAMP discovery did not return api.url");
4299
- const apiBase = new URL(apiUrl, `${base}/`).toString();
4300
- const res = await fetch(`${apiBase}?action=aamp.mailbox.check&email=${encodeURIComponent(email)}`);
4301
- if (!res.ok)
4302
- throw new Error(`HTTP ${res.status}`);
4303
- const data = await res.json();
4875
+ const data = await AampClient.checkMailbox({
4876
+ aampHost: base,
4877
+ email
4878
+ });
4304
4879
  return {
4305
4880
  content: [{
4306
4881
  type: "text",
@@ -4374,6 +4949,15 @@ Question: ${h.question}`,
4374
4949
  };
4375
4950
  }
4376
4951
  });
4952
+ api.registerCommand({
4953
+ name: "aamp-pair",
4954
+ description: "Show a fresh AAMP pairing QR code for this OpenClaw agent",
4955
+ acceptsArgs: false,
4956
+ requireAuth: false,
4957
+ handler: async () => ({
4958
+ text: await renderPairingCodeForCurrentAgent()
4959
+ })
4960
+ });
4377
4961
  }
4378
4962
  };
4379
4963
  export {