@openclaw/sms 2026.7.1 → 2026.7.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-
|
|
1
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-Cp0f-YPc.js";
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
3
|
import { createHybridChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
4
4
|
import { buildChannelOutboundSessionRoute, createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
@@ -6,19 +6,23 @@ import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, w
|
|
|
6
6
|
import { createConditionalWarningCollector } from "openclaw/plugin-sdk/channel-policy";
|
|
7
7
|
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
8
8
|
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
|
-
import { chunkTextForOutbound, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
|
|
9
|
+
import { chunkTextForOutbound, renderMarkdownIRChunksWithinLimit, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
|
|
10
10
|
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, listCombinedAccountIds, resolveAccountEntry, resolveListedDefaultAccountId, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
|
|
11
11
|
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
12
12
|
import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
13
|
-
import { AllowFromListSchema, DmPolicySchema, buildChannelConfigSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-
|
|
13
|
+
import { AllowFromListSchema, DmPolicySchema, buildChannelConfigSchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
|
|
14
14
|
import { requireChannelOpenAllowFrom } from "openclaw/plugin-sdk/extension-shared";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
-
import { createFixedWindowRateLimiter, readRequestBodyWithLimit, registerPluginHttpRoute } from "openclaw/plugin-sdk/webhook-ingress";
|
|
16
|
+
import { createFixedWindowRateLimiter, readRequestBodyWithLimit, registerPluginHttpRoute, resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
|
|
17
|
+
import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
|
|
17
18
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
18
19
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
19
|
-
import { createHmac
|
|
20
|
+
import { createHmac } from "node:crypto";
|
|
20
21
|
import * as querystring from "node:querystring";
|
|
22
|
+
import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
23
|
+
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
21
24
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
25
|
+
import { performance } from "node:perf_hooks";
|
|
22
26
|
//#region extensions/sms/src/phone.ts
|
|
23
27
|
function normalizeSmsPhoneNumber(raw) {
|
|
24
28
|
const trimmed = raw.trim().replace(/^(?:sms|twilio-sms):/i, "");
|
|
@@ -129,7 +133,7 @@ function isSmsAccountConfigured(account) {
|
|
|
129
133
|
//#endregion
|
|
130
134
|
//#region extensions/sms/src/config-schema.ts
|
|
131
135
|
const SecretInputSchema = buildSecretInputSchema();
|
|
132
|
-
const
|
|
136
|
+
const SmsChannelConfigSchema = buildChannelConfigSchema(buildMultiAccountChannelSchema(z.object({
|
|
133
137
|
name: z.string().optional(),
|
|
134
138
|
enabled: z.boolean().optional(),
|
|
135
139
|
accountSid: z.string().optional(),
|
|
@@ -143,18 +147,17 @@ const SmsAccountConfigSchema = z.object({
|
|
|
143
147
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
144
148
|
allowFrom: AllowFromListSchema,
|
|
145
149
|
textChunkLimit: z.number().int().positive().optional()
|
|
146
|
-
}).strict()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
defaultAccount: z.string().optional()
|
|
150
|
+
}).strict(), {
|
|
151
|
+
optionalAccount: true,
|
|
152
|
+
refine: (value, ctx) => {
|
|
153
|
+
requireChannelOpenAllowFrom({
|
|
154
|
+
channel: "sms",
|
|
155
|
+
policy: value.dmPolicy,
|
|
156
|
+
allowFrom: value.allowFrom,
|
|
157
|
+
ctx,
|
|
158
|
+
requireOpenAllowFrom
|
|
159
|
+
});
|
|
160
|
+
}
|
|
158
161
|
}), { uiHints: {
|
|
159
162
|
"": {
|
|
160
163
|
label: "SMS",
|
|
@@ -213,6 +216,7 @@ const TWILIO_API_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
|
|
213
216
|
const TRUNCATED_RESPONSE_SUFFIX = "... [truncated]";
|
|
214
217
|
const WEBHOOK_BODY_LIMIT_BYTES = 32 * 1024;
|
|
215
218
|
const WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
|
219
|
+
const TWILIO_CHANNEL_ADDRESS_RE = /^([a-z][a-z0-9-]*):(.*)$/i;
|
|
216
220
|
function firstString(value) {
|
|
217
221
|
if (Array.isArray(value)) return firstString(value[0]);
|
|
218
222
|
return typeof value === "string" ? value : "";
|
|
@@ -262,17 +266,16 @@ function requestSearch(req) {
|
|
|
262
266
|
return "";
|
|
263
267
|
}
|
|
264
268
|
}
|
|
265
|
-
function
|
|
269
|
+
function stripUrlFragment(url) {
|
|
266
270
|
const hashIndex = url.indexOf("#");
|
|
267
|
-
return
|
|
271
|
+
return hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
268
272
|
}
|
|
269
273
|
function resolveTwilioWebhookSignatureUrl(params) {
|
|
270
|
-
|
|
274
|
+
const signatureBaseUrl = stripUrlFragment(params.publicWebhookUrl);
|
|
275
|
+
if (signatureBaseUrl.includes("?")) return signatureBaseUrl;
|
|
271
276
|
const search = requestSearch(params.req);
|
|
272
|
-
if (!search) return
|
|
273
|
-
|
|
274
|
-
if (hashIndex === -1) return `${params.publicWebhookUrl}${search}`;
|
|
275
|
-
return `${params.publicWebhookUrl.slice(0, hashIndex)}${search}${params.publicWebhookUrl.slice(hashIndex)}`;
|
|
277
|
+
if (!search) return signatureBaseUrl;
|
|
278
|
+
return `${signatureBaseUrl}${search}`;
|
|
276
279
|
}
|
|
277
280
|
var TwilioSmsApiError = class extends Error {
|
|
278
281
|
constructor(httpStatus, responseText, operation = "send") {
|
|
@@ -295,21 +298,26 @@ function computeTwilioSignature(params) {
|
|
|
295
298
|
const data = params.url + Object.keys(params.form).toSorted().map((key) => `${key}${params.form[key] ?? ""}`).join("");
|
|
296
299
|
return createHmac("sha1", params.authToken).update(data).digest("base64");
|
|
297
300
|
}
|
|
298
|
-
function safeEqual(a, b) {
|
|
299
|
-
const left = Buffer.from(a);
|
|
300
|
-
const right = Buffer.from(b);
|
|
301
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
302
|
-
}
|
|
303
301
|
function verifyTwilioSignature(params) {
|
|
304
302
|
if (!params.signature || !params.url || !params.authToken) return false;
|
|
305
|
-
return
|
|
303
|
+
return safeEqualSecret(params.signature, computeTwilioSignature({
|
|
306
304
|
url: params.url,
|
|
307
305
|
authToken: params.authToken,
|
|
308
306
|
form: params.form
|
|
309
307
|
}));
|
|
310
308
|
}
|
|
309
|
+
function parseTwilioInboundFrom(raw) {
|
|
310
|
+
const trimmed = raw.trim();
|
|
311
|
+
if (!trimmed) return null;
|
|
312
|
+
const channelAddress = trimmed.match(TWILIO_CHANNEL_ADDRESS_RE);
|
|
313
|
+
const kind = channelAddress?.[1]?.toLowerCase();
|
|
314
|
+
if (kind && kind !== "rcs") return null;
|
|
315
|
+
const phoneNumber = normalizeSmsPhoneNumber(channelAddress?.[2] ?? trimmed);
|
|
316
|
+
if (!looksLikeSmsPhoneNumber(phoneNumber)) return null;
|
|
317
|
+
return phoneNumber;
|
|
318
|
+
}
|
|
311
319
|
function buildTwilioInboundMessage(form) {
|
|
312
|
-
const from = firstTrimmedString(form.From);
|
|
320
|
+
const from = parseTwilioInboundFrom(firstTrimmedString(form.From));
|
|
313
321
|
const to = firstTrimmedString(form.To);
|
|
314
322
|
const body = firstString(form.Body);
|
|
315
323
|
const accountSid = firstTrimmedString(form.AccountSid);
|
|
@@ -353,37 +361,13 @@ function appendTruncatedResponseSuffix(text) {
|
|
|
353
361
|
return `${text.trimEnd()}${TRUNCATED_RESPONSE_SUFFIX}`;
|
|
354
362
|
}
|
|
355
363
|
async function readTwilioApiResponseText(response) {
|
|
356
|
-
if (!response.body) return "";
|
|
357
364
|
const maxBytes = response.ok ? TWILIO_API_SUCCESS_BODY_LIMIT_BYTES : TWILIO_API_ERROR_BODY_LIMIT_BYTES;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
let totalBytes = 0;
|
|
362
|
-
let text = "";
|
|
363
|
-
try {
|
|
364
|
-
while (true) {
|
|
365
|
-
const { done, value } = await reader.read();
|
|
366
|
-
if (done) return text + decoder.decode();
|
|
367
|
-
if (!value?.byteLength) continue;
|
|
368
|
-
const remainingBytes = maxBytes - totalBytes;
|
|
369
|
-
if (value.byteLength > remainingBytes) {
|
|
370
|
-
const clipped = remainingBytes > 0 ? value.slice(0, remainingBytes) : void 0;
|
|
371
|
-
if (truncateOnLimit) {
|
|
372
|
-
if (clipped) text += decoder.decode(clipped, { stream: true });
|
|
373
|
-
await reader.cancel().catch(() => void 0);
|
|
374
|
-
return appendTruncatedResponseSuffix(text + decoder.decode());
|
|
375
|
-
}
|
|
376
|
-
await reader.cancel().catch(() => void 0);
|
|
377
|
-
throw new Error(`Twilio SMS API response body too large: ${totalBytes + value.byteLength} bytes (limit: ${maxBytes} bytes)`);
|
|
378
|
-
}
|
|
379
|
-
text += decoder.decode(value, { stream: true });
|
|
380
|
-
totalBytes += value.byteLength;
|
|
381
|
-
}
|
|
382
|
-
} finally {
|
|
383
|
-
try {
|
|
384
|
-
reader.releaseLock();
|
|
385
|
-
} catch {}
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
const prefix = await readResponseTextPrefix(response, maxBytes);
|
|
367
|
+
return prefix.truncated ? appendTruncatedResponseSuffix(prefix.text) : prefix.text;
|
|
386
368
|
}
|
|
369
|
+
const body = await readResponseWithLimit(response, maxBytes, { onOverflow: ({ size, maxBytes: limit }) => /* @__PURE__ */ new Error(`Twilio SMS API response body too large: ${size} bytes (limit: ${limit} bytes)`) });
|
|
370
|
+
return new TextDecoder().decode(body);
|
|
387
371
|
}
|
|
388
372
|
function normalizeRequestHeaders(headers) {
|
|
389
373
|
if (!headers) return {};
|
|
@@ -547,17 +531,31 @@ async function sendSmsViaTwilio(params) {
|
|
|
547
531
|
}
|
|
548
532
|
//#endregion
|
|
549
533
|
//#region extensions/sms/src/send.ts
|
|
534
|
+
const SMS_ASSISTANT_TRANSCRIPT_ROLE_PREFIX = "[assistant-authored transcript] ";
|
|
550
535
|
function toSmsPlainText(text) {
|
|
551
|
-
return stripMarkdown(sanitizeAssistantVisibleText(text)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
})
|
|
536
|
+
return stripMarkdown(sanitizeAssistantVisibleText(text), {
|
|
537
|
+
assistantTranscriptRoleHeaders: true,
|
|
538
|
+
assistantTranscriptRolePrefix: SMS_ASSISTANT_TRANSCRIPT_ROLE_PREFIX,
|
|
539
|
+
linkStyle: "label-and-url"
|
|
540
|
+
}).replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
541
|
+
}
|
|
542
|
+
function chunkSmsPlainText(text, limit) {
|
|
543
|
+
return renderMarkdownIRChunksWithinLimit({
|
|
544
|
+
ir: {
|
|
545
|
+
text,
|
|
546
|
+
styles: [],
|
|
547
|
+
links: []
|
|
548
|
+
},
|
|
549
|
+
limit,
|
|
550
|
+
assistantTranscriptRoleMessageBoundaries: true,
|
|
551
|
+
renderChunk: (chunk) => chunk.annotations?.some((annotation) => annotation.type === "assistant_transcript_role") ? `${SMS_ASSISTANT_TRANSCRIPT_ROLE_PREFIX}${chunk.text}` : chunk.text,
|
|
552
|
+
measureRendered: (rendered) => rendered.length
|
|
553
|
+
}).map(({ rendered }) => rendered).filter(Boolean);
|
|
556
554
|
}
|
|
557
555
|
async function sendSmsTextChunks(params) {
|
|
558
556
|
const text = toSmsPlainText(params.text);
|
|
559
557
|
if (!text) throw new Error("SMS send requires non-empty text.");
|
|
560
|
-
const chunks =
|
|
558
|
+
const chunks = chunkSmsPlainText(text, params.account.textChunkLimit);
|
|
561
559
|
const sendChunks = chunks.length ? chunks : [text];
|
|
562
560
|
const results = [];
|
|
563
561
|
for (const textLocal of sendChunks) results.push(await sendSmsViaTwilio({
|
|
@@ -729,49 +727,98 @@ async function dispatchSmsInboundEvent(params) {
|
|
|
729
727
|
});
|
|
730
728
|
}
|
|
731
729
|
//#endregion
|
|
730
|
+
//#region extensions/sms/src/webhook-replay-guard.ts
|
|
731
|
+
const REPLAY_CACHE_TTL_MS = 10 * 6e4;
|
|
732
|
+
const REPLAY_CACHE_MAX_KEYS = 1e4;
|
|
733
|
+
function createSmsWebhookReplayGuard(options = {}) {
|
|
734
|
+
const ttlMs = options.ttlMs ?? REPLAY_CACHE_TTL_MS;
|
|
735
|
+
const maxKeys = options.maxKeys ?? REPLAY_CACHE_MAX_KEYS;
|
|
736
|
+
const now = options.now ?? (() => performance.now());
|
|
737
|
+
const entries = /* @__PURE__ */ new Map();
|
|
738
|
+
const pruneExpired = (nowMs) => {
|
|
739
|
+
for (const [key, expiresAt] of entries) {
|
|
740
|
+
if (expiresAt > nowMs) break;
|
|
741
|
+
entries.delete(key);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
return { remember: (messageSid) => {
|
|
745
|
+
const nowMs = now();
|
|
746
|
+
pruneExpired(nowMs);
|
|
747
|
+
if (entries.has(messageSid)) return { kind: "replayed" };
|
|
748
|
+
if (entries.size >= maxKeys) {
|
|
749
|
+
const oldestExpiresAt = entries.values().next().value ?? nowMs;
|
|
750
|
+
return {
|
|
751
|
+
kind: "saturated",
|
|
752
|
+
retryAfterMs: Math.max(0, oldestExpiresAt - nowMs)
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
entries.set(messageSid, nowMs + ttlMs);
|
|
756
|
+
return { kind: "accepted" };
|
|
757
|
+
} };
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
732
760
|
//#region extensions/sms/src/webhook.ts
|
|
733
|
-
const
|
|
734
|
-
|
|
761
|
+
const INVALID_REQUEST_MAX_REQUESTS = 300;
|
|
762
|
+
const CALLBACK_DISPATCH_MAX_REQUESTS = 30;
|
|
763
|
+
const invalidRequestRateLimiter = createFixedWindowRateLimiter({
|
|
764
|
+
maxRequests: INVALID_REQUEST_MAX_REQUESTS,
|
|
735
765
|
windowMs: 6e4,
|
|
736
766
|
maxTrackedKeys: 5e3
|
|
737
767
|
});
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
768
|
+
const callbackDispatchRateLimiter = createFixedWindowRateLimiter({
|
|
769
|
+
maxRequests: CALLBACK_DISPATCH_MAX_REQUESTS,
|
|
770
|
+
windowMs: 6e4,
|
|
771
|
+
maxTrackedKeys: 5e3
|
|
772
|
+
});
|
|
773
|
+
const replayGuardsByAccount = /* @__PURE__ */ new Map();
|
|
774
|
+
function resolveSmsWebhookReplayGuard(account) {
|
|
775
|
+
const key = `${account.accountId}\0${account.accountSid}`;
|
|
776
|
+
const existing = replayGuardsByAccount.get(key);
|
|
777
|
+
if (existing) return existing;
|
|
778
|
+
const created = createSmsWebhookReplayGuard();
|
|
779
|
+
replayGuardsByAccount.set(key, created);
|
|
780
|
+
return created;
|
|
781
|
+
}
|
|
741
782
|
function headerValue(value) {
|
|
742
783
|
if (Array.isArray(value)) return value[0];
|
|
743
784
|
return value;
|
|
744
785
|
}
|
|
745
|
-
function
|
|
746
|
-
return req.socket?.remoteAddress ?? "unknown";
|
|
786
|
+
function resolvedClientAddress(params) {
|
|
787
|
+
return resolveRequestClientIp(params.req, params.cfg.gateway?.trustedProxies, params.cfg.gateway?.allowRealIpFallback === true) ?? params.req.socket?.remoteAddress ?? "unknown";
|
|
747
788
|
}
|
|
748
|
-
function
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const key = `${params.accountId}:${params.messageSid}`;
|
|
755
|
-
if ((replayCache.get(key) ?? 0) > now) return false;
|
|
756
|
-
replayCache.set(key, now + REPLAY_CACHE_TTL_MS);
|
|
789
|
+
function rateLimitKey(params) {
|
|
790
|
+
return `${params.account.accountId}:${params.account.webhookPath}:${params.clientAddress}`;
|
|
791
|
+
}
|
|
792
|
+
function rejectInvalidRequestRateLimit(params) {
|
|
793
|
+
params.log?.warn?.(`SMS webhook invalid-request rate limit exceeded for ${params.key}`);
|
|
794
|
+
respondTwiml(params.res, 429, "Rate limit exceeded");
|
|
757
795
|
return true;
|
|
758
796
|
}
|
|
759
797
|
function createSmsWebhookHandler(params) {
|
|
798
|
+
const webhookReplayGuard = resolveSmsWebhookReplayGuard(params.account);
|
|
760
799
|
return async (req, res) => {
|
|
761
800
|
if (req.method !== "POST") {
|
|
762
801
|
respondTwiml(res, 405, "Method not allowed");
|
|
763
802
|
return true;
|
|
764
803
|
}
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
804
|
+
const clientAddress = resolvedClientAddress({
|
|
805
|
+
cfg: params.cfg,
|
|
806
|
+
req
|
|
807
|
+
});
|
|
808
|
+
const key = rateLimitKey({
|
|
809
|
+
account: params.account,
|
|
810
|
+
clientAddress
|
|
811
|
+
});
|
|
812
|
+
const invalidRequestRateLimited = invalidRequestRateLimiter.isRateLimited(key);
|
|
771
813
|
let form;
|
|
772
814
|
try {
|
|
773
815
|
form = await readTwilioWebhookForm(req);
|
|
774
816
|
} catch {
|
|
817
|
+
if (invalidRequestRateLimited) return rejectInvalidRequestRateLimit({
|
|
818
|
+
key,
|
|
819
|
+
log: params.log,
|
|
820
|
+
res
|
|
821
|
+
});
|
|
775
822
|
respondTwiml(res, 400, "Invalid request body");
|
|
776
823
|
return true;
|
|
777
824
|
}
|
|
@@ -785,6 +832,11 @@ function createSmsWebhookHandler(params) {
|
|
|
785
832
|
authToken: params.account.authToken,
|
|
786
833
|
form
|
|
787
834
|
})) {
|
|
835
|
+
if (invalidRequestRateLimited) return rejectInvalidRequestRateLimit({
|
|
836
|
+
key,
|
|
837
|
+
log: params.log,
|
|
838
|
+
res
|
|
839
|
+
});
|
|
788
840
|
params.log?.warn?.("SMS webhook rejected invalid Twilio signature");
|
|
789
841
|
respondTwiml(res, 403, "Invalid signature");
|
|
790
842
|
return true;
|
|
@@ -792,29 +844,54 @@ function createSmsWebhookHandler(params) {
|
|
|
792
844
|
}
|
|
793
845
|
const msg = buildTwilioInboundMessage(form);
|
|
794
846
|
if (!msg) {
|
|
847
|
+
if (invalidRequestRateLimited) return rejectInvalidRequestRateLimit({
|
|
848
|
+
key,
|
|
849
|
+
log: params.log,
|
|
850
|
+
res
|
|
851
|
+
});
|
|
795
852
|
respondTwiml(res, 400, "Missing SMS payload");
|
|
796
853
|
return true;
|
|
797
854
|
}
|
|
798
855
|
if (msg.accountSid && msg.accountSid !== params.account.accountSid) {
|
|
856
|
+
if (invalidRequestRateLimited) return rejectInvalidRequestRateLimit({
|
|
857
|
+
key,
|
|
858
|
+
log: params.log,
|
|
859
|
+
res
|
|
860
|
+
});
|
|
799
861
|
params.log?.warn?.("SMS webhook rejected mismatched Twilio AccountSid");
|
|
800
862
|
respondTwiml(res, 403, "Invalid account");
|
|
801
863
|
return true;
|
|
802
864
|
}
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
865
|
+
if (invalidRequestRateLimited && params.account.dangerouslyDisableSignatureValidation) return rejectInvalidRequestRateLimit({
|
|
866
|
+
key,
|
|
867
|
+
log: params.log,
|
|
868
|
+
res
|
|
869
|
+
});
|
|
870
|
+
if (callbackDispatchRateLimiter.isRateLimited(key)) {
|
|
871
|
+
params.log?.warn?.(`SMS webhook rate limit exceeded for ${key}`);
|
|
872
|
+
respondTwiml(res, 429, "Rate limit exceeded");
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
const replayDecision = webhookReplayGuard.remember(msg.messageSid);
|
|
876
|
+
if (replayDecision.kind === "replayed") {
|
|
807
877
|
params.log?.warn?.(`SMS webhook ignored replayed message ${msg.messageSid}`);
|
|
808
878
|
respondTwiml(res, 200);
|
|
809
879
|
return true;
|
|
810
880
|
}
|
|
811
|
-
|
|
881
|
+
if (replayDecision.kind === "saturated") {
|
|
882
|
+
const retryAfterSeconds = Math.max(1, Math.ceil(replayDecision.retryAfterMs / 1e3));
|
|
883
|
+
params.log?.warn?.("SMS webhook replay cache is full of unexpired message SIDs");
|
|
884
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
885
|
+
respondTwiml(res, 429, "Replay cache saturated");
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
runDetachedWebhookWork(() => dispatchSmsInboundEvent({
|
|
812
889
|
cfg: params.cfg,
|
|
813
890
|
account: params.account,
|
|
814
891
|
msg,
|
|
815
892
|
channelRuntime: params.channelRuntime,
|
|
816
893
|
log: params.log
|
|
817
|
-
}).catch((err) => {
|
|
894
|
+
})).catch((err) => {
|
|
818
895
|
params.log?.error?.(`SMS webhook dispatch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
819
896
|
});
|
|
820
897
|
respondTwiml(res, 200);
|
|
@@ -1,27 +1,11 @@
|
|
|
1
|
-
import { collectConditionalChannelFieldAssignments, getChannelSurface, hasOwnProperty } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
1
|
+
import { collectConditionalChannelFieldAssignments, createChannelSecretTargetRegistryEntries, getChannelSurface, hasOwnProperty } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
2
2
|
//#region extensions/sms/src/secret-contract.ts
|
|
3
3
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
4
|
-
const secretTargetRegistryEntries =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
secretShape: "secret_input",
|
|
10
|
-
expectedResolvedValue: "string",
|
|
11
|
-
includeInPlan: true,
|
|
12
|
-
includeInConfigure: true,
|
|
13
|
-
includeInAudit: true
|
|
14
|
-
}, {
|
|
15
|
-
id: "channels.sms.authToken",
|
|
16
|
-
targetType: "channels.sms.authToken",
|
|
17
|
-
configFile: "openclaw.json",
|
|
18
|
-
pathPattern: "channels.sms.authToken",
|
|
19
|
-
secretShape: "secret_input",
|
|
20
|
-
expectedResolvedValue: "string",
|
|
21
|
-
includeInPlan: true,
|
|
22
|
-
includeInConfigure: true,
|
|
23
|
-
includeInAudit: true
|
|
24
|
-
}];
|
|
4
|
+
const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
|
|
5
|
+
channelKey: "sms",
|
|
6
|
+
account: ["authToken"],
|
|
7
|
+
channel: ["authToken"]
|
|
8
|
+
});
|
|
25
9
|
function hasTopLevelSmsAccount(channel) {
|
|
26
10
|
for (const field of [
|
|
27
11
|
"accountSid",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-
|
|
1
|
+
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-Cp0f-YPc.js";
|
|
2
2
|
export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/sms",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.2-beta.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/sms",
|
|
9
|
-
"version": "2026.7.
|
|
9
|
+
"version": "2026.7.2-beta.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"zod": "4.4.3"
|
|
12
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/sms",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.2-beta.2",
|
|
4
4
|
"description": "OpenClaw SMS channel plugin for Twilio text messages.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"quickstartAllowFrom": true
|
|
27
27
|
},
|
|
28
28
|
"compat": {
|
|
29
|
-
"pluginApi": ">=2026.7.
|
|
29
|
+
"pluginApi": ">=2026.7.2-beta.2"
|
|
30
30
|
},
|
|
31
31
|
"build": {
|
|
32
|
-
"openclawVersion": "2026.7.
|
|
32
|
+
"openclawVersion": "2026.7.2-beta.2",
|
|
33
33
|
"bundledDist": false
|
|
34
34
|
},
|
|
35
35
|
"install": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"README.md"
|
|
55
55
|
],
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"openclaw": ">=2026.7.
|
|
57
|
+
"openclaw": ">=2026.7.2-beta.2"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
60
60
|
"openclaw": {
|