@kittymi/openclaw-generic-http 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/dist/channel/account.d.ts +7 -0
  4. package/dist/channel/account.js +18 -0
  5. package/dist/channel/capabilities.d.ts +10 -0
  6. package/dist/channel/capabilities.js +11 -0
  7. package/dist/channel/host-adapter.d.ts +28 -0
  8. package/dist/channel/host-adapter.js +36 -0
  9. package/dist/channel/lifecycle.d.ts +18 -0
  10. package/dist/channel/lifecycle.js +28 -0
  11. package/dist/channel/plugin.d.ts +46 -0
  12. package/dist/channel/plugin.js +120 -0
  13. package/dist/channel/probe.d.ts +19 -0
  14. package/dist/channel/probe.js +149 -0
  15. package/dist/channel/resolve.d.ts +30 -0
  16. package/dist/channel/resolve.js +98 -0
  17. package/dist/channel/stream.d.ts +35 -0
  18. package/dist/channel/stream.js +127 -0
  19. package/dist/config/host-config-schema.d.ts +21 -0
  20. package/dist/config/host-config-schema.js +80 -0
  21. package/dist/config/loader.d.ts +7 -0
  22. package/dist/config/loader.js +38 -0
  23. package/dist/config/schema.d.ts +48 -0
  24. package/dist/config/schema.js +1 -0
  25. package/dist/errors/codes.d.ts +11 -0
  26. package/dist/errors/codes.js +10 -0
  27. package/dist/errors/exceptions.d.ts +7 -0
  28. package/dist/errors/exceptions.js +12 -0
  29. package/dist/inbound/mapper.d.ts +21 -0
  30. package/dist/inbound/mapper.js +22 -0
  31. package/dist/inbound/validator.d.ts +4 -0
  32. package/dist/inbound/validator.js +114 -0
  33. package/dist/index.d.ts +33 -0
  34. package/dist/index.js +31 -0
  35. package/dist/mapping/conversation-mapper.d.ts +1 -0
  36. package/dist/mapping/conversation-mapper.js +3 -0
  37. package/dist/mapping/sender-mapper.d.ts +1 -0
  38. package/dist/mapping/sender-mapper.js +3 -0
  39. package/dist/mapping/thread-mapper.d.ts +1 -0
  40. package/dist/mapping/thread-mapper.js +7 -0
  41. package/dist/openclaw-entry.d.ts +276 -0
  42. package/dist/openclaw-entry.js +728 -0
  43. package/dist/outbound/client.d.ts +6 -0
  44. package/dist/outbound/client.js +1 -0
  45. package/dist/outbound/controller.d.ts +15 -0
  46. package/dist/outbound/controller.js +28 -0
  47. package/dist/outbound/http-client.d.ts +23 -0
  48. package/dist/outbound/http-client.js +150 -0
  49. package/dist/outbound/mapper.d.ts +29 -0
  50. package/dist/outbound/mapper.js +19 -0
  51. package/dist/outbound/mock-client.d.ts +18 -0
  52. package/dist/outbound/mock-client.js +26 -0
  53. package/dist/outbound/sender.d.ts +3 -0
  54. package/dist/outbound/sender.js +5 -0
  55. package/dist/protocol/attachments.d.ts +10 -0
  56. package/dist/protocol/attachments.js +56 -0
  57. package/dist/protocol/dto.d.ts +46 -0
  58. package/dist/protocol/dto.js +1 -0
  59. package/dist/protocol/serializer.d.ts +1 -0
  60. package/dist/protocol/serializer.js +3 -0
  61. package/dist/security/nonce-store.d.ts +30 -0
  62. package/dist/security/nonce-store.js +32 -0
  63. package/dist/security/signer.d.ts +10 -0
  64. package/dist/security/signer.js +20 -0
  65. package/dist/security/verifier.d.ts +2 -0
  66. package/dist/security/verifier.js +20 -0
  67. package/dist/setup-entry.d.ts +351 -0
  68. package/dist/setup-entry.js +73 -0
  69. package/dist/utils/json.d.ts +1 -0
  70. package/dist/utils/json.js +3 -0
  71. package/dist/utils/log.d.ts +1 -0
  72. package/dist/utils/log.js +3 -0
  73. package/dist/utils/time.d.ts +1 -0
  74. package/dist/utils/time.js +3 -0
  75. package/openclaw.config.schema.json +80 -0
  76. package/openclaw.plugin.json +175 -0
  77. package/package.json +72 -0
@@ -0,0 +1,6 @@
1
+ import type { OutboundMessageRequest, OutboundMessageResult } from "./mapper.js";
2
+ export interface OutboundClient {
3
+ send(request: OutboundMessageRequest): Promise<OutboundMessageResult>;
4
+ }
5
+ export { HttpOutboundClient } from "./http-client.js";
6
+ export type { HttpOutboundClientOptions } from "./http-client.js";
@@ -0,0 +1 @@
1
+ export { HttpOutboundClient } from "./http-client.js";
@@ -0,0 +1,15 @@
1
+ import type { OutboundClient } from "./client.js";
2
+ import { type InternalOutboundMessage, type OutboundMessageResult } from "./mapper.js";
3
+ export interface OutboundHandlingSuccess {
4
+ result: OutboundMessageResult;
5
+ }
6
+ export interface OutboundHandlingError {
7
+ success: false;
8
+ code: string;
9
+ message: string;
10
+ requestId: string;
11
+ details?: Record<string, unknown>;
12
+ retryable: boolean;
13
+ }
14
+ export declare function buildOutboundErrorResponse(requestId: string, error: unknown): OutboundHandlingError;
15
+ export declare function handleOutboundMessage(client: OutboundClient, message: InternalOutboundMessage): Promise<OutboundHandlingSuccess>;
@@ -0,0 +1,28 @@
1
+ import { GenericHttpPluginError } from "../errors/exceptions.js";
2
+ import { sendOutboundMessage } from "./sender.js";
3
+ export function buildOutboundErrorResponse(requestId, error) {
4
+ if (error instanceof GenericHttpPluginError) {
5
+ return {
6
+ success: false,
7
+ code: error.code,
8
+ message: error.message,
9
+ requestId,
10
+ details: error.details,
11
+ retryable: error.retryable
12
+ };
13
+ }
14
+ return {
15
+ success: false,
16
+ code: "INTERNAL_ERROR",
17
+ message: "Unexpected outbound message handling failure.",
18
+ requestId,
19
+ retryable: true
20
+ };
21
+ }
22
+ export async function handleOutboundMessage(client, message) {
23
+ // The outbound control path mirrors inbound: normalize the internal message,
24
+ // delegate delivery to the transport client, and return a protocol-shaped
25
+ // result for the caller or HTTP layer.
26
+ const result = await sendOutboundMessage(client, message);
27
+ return { result };
28
+ }
@@ -0,0 +1,23 @@
1
+ import type { GenericHttpAccountConfig } from "../config/schema.js";
2
+ import type { OutboundClient } from "./client.js";
3
+ import type { OutboundMessageRequest, OutboundMessageResult } from "./mapper.js";
4
+ export interface HttpOutboundClientOptions {
5
+ nowEpochSeconds?: () => number;
6
+ nonceFactory?: () => string;
7
+ fetchImpl?: typeof fetch;
8
+ }
9
+ /**
10
+ * Minimal HTTP transport for the generic bridge outbound path.
11
+ *
12
+ * The caller hands over a normalized outbound request, this client signs the
13
+ * serialized body with the account secret, and then posts it to the configured
14
+ * third-party bridge endpoint.
15
+ */
16
+ export declare class HttpOutboundClient implements OutboundClient {
17
+ private readonly accountConfig;
18
+ private readonly nowEpochSeconds;
19
+ private readonly nonceFactory;
20
+ private readonly fetchImpl;
21
+ constructor(accountConfig: GenericHttpAccountConfig, options?: HttpOutboundClientOptions);
22
+ send(request: OutboundMessageRequest): Promise<OutboundMessageResult>;
23
+ }
@@ -0,0 +1,150 @@
1
+ import { ERROR_CODES } from "../errors/codes.js";
2
+ import { GenericHttpPluginError } from "../errors/exceptions.js";
3
+ import { normalizeAttachment } from "../protocol/attachments.js";
4
+ import { serializeProtocolObject } from "../protocol/serializer.js";
5
+ import { signPayload } from "../security/signer.js";
6
+ function normalizeOutboundRequest(request) {
7
+ return {
8
+ ...request,
9
+ message: {
10
+ ...request.message,
11
+ attachments: (request.message.attachments ?? []).map((attachment) => normalizeAttachment(attachment))
12
+ }
13
+ };
14
+ }
15
+ function createTimeoutSignal(timeoutMillis) {
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), timeoutMillis);
18
+ controller.signal.addEventListener("abort", () => clearTimeout(timer), {
19
+ once: true
20
+ });
21
+ return controller.signal;
22
+ }
23
+ function buildOutboundEndpoint(baseUrl) {
24
+ if (baseUrl.trim() === "") {
25
+ throw new GenericHttpPluginError(ERROR_CODES.INVALID_REQUEST, "Outbound account config requires a non-empty baseUrl.", { field: "baseUrl" });
26
+ }
27
+ return new URL("/outbound/messages", baseUrl).toString();
28
+ }
29
+ function buildOutboundHeaders(accountConfig, request, signature, timestamp, nonce) {
30
+ return {
31
+ accept: "application/json",
32
+ "content-type": "application/json",
33
+ "x-api-key": accountConfig.apiKey ?? "",
34
+ "x-generic-http-version": "1",
35
+ "x-nonce": nonce,
36
+ "x-request-id": request.requestId,
37
+ "x-signature": signature,
38
+ "x-timestamp": timestamp
39
+ };
40
+ }
41
+ function parseOutboundResult(value) {
42
+ if (typeof value !== "object" || value === null) {
43
+ throw new GenericHttpPluginError(ERROR_CODES.INTERNAL_ERROR, "Outbound endpoint returned a non-object response.", { responseType: typeof value });
44
+ }
45
+ const result = value;
46
+ if (result.success !== true ||
47
+ result.code !== "DELIVERED" ||
48
+ typeof result.providerMessageId !== "string" ||
49
+ typeof result.acceptedAt !== "string") {
50
+ throw new GenericHttpPluginError(ERROR_CODES.INTERNAL_ERROR, "Outbound endpoint returned an invalid delivery result.", {
51
+ response: value
52
+ });
53
+ }
54
+ return {
55
+ success: true,
56
+ code: "DELIVERED",
57
+ providerMessageId: result.providerMessageId,
58
+ acceptedAt: result.acceptedAt,
59
+ metadata: typeof result.metadata === "object" && result.metadata !== null
60
+ ? result.metadata
61
+ : {}
62
+ };
63
+ }
64
+ function isRetryableStatus(status) {
65
+ return status === 408 || status === 429 || status >= 500;
66
+ }
67
+ function shouldRetry(error) {
68
+ if (error instanceof GenericHttpPluginError) {
69
+ return error.retryable;
70
+ }
71
+ if (error instanceof Error) {
72
+ return error.name === "AbortError" || error.name === "TypeError";
73
+ }
74
+ return false;
75
+ }
76
+ function createTransportError(code, message, details, retryable = false) {
77
+ return new GenericHttpPluginError(code, message, details, retryable);
78
+ }
79
+ /**
80
+ * Minimal HTTP transport for the generic bridge outbound path.
81
+ *
82
+ * The caller hands over a normalized outbound request, this client signs the
83
+ * serialized body with the account secret, and then posts it to the configured
84
+ * third-party bridge endpoint.
85
+ */
86
+ export class HttpOutboundClient {
87
+ accountConfig;
88
+ nowEpochSeconds;
89
+ nonceFactory;
90
+ fetchImpl;
91
+ constructor(accountConfig, options = {}) {
92
+ this.accountConfig = accountConfig;
93
+ this.nowEpochSeconds =
94
+ options.nowEpochSeconds ?? (() => Math.floor(Date.now() / 1000));
95
+ this.nonceFactory = options.nonceFactory ?? (() => crypto.randomUUID());
96
+ this.fetchImpl = options.fetchImpl ?? fetch;
97
+ }
98
+ async send(request) {
99
+ const endpoint = buildOutboundEndpoint(this.accountConfig.baseUrl);
100
+ const normalizedRequest = normalizeOutboundRequest(request);
101
+ const rawBody = serializeProtocolObject(normalizedRequest);
102
+ const timestamp = String(this.nowEpochSeconds());
103
+ const nonce = this.nonceFactory();
104
+ const signingSecret = this.accountConfig.outboundSecret ?? this.accountConfig.signingSecret;
105
+ if (signingSecret === undefined || signingSecret.trim() === "") {
106
+ throw new GenericHttpPluginError(ERROR_CODES.INVALID_REQUEST, "Outbound account config requires signingSecret or outboundSecret.", {
107
+ accountId: request.accountId
108
+ });
109
+ }
110
+ const path = new URL(endpoint).pathname;
111
+ const signature = signPayload(signingSecret, {
112
+ method: "POST",
113
+ path,
114
+ timestamp,
115
+ nonce,
116
+ rawBody
117
+ });
118
+ const maxAttempts = Math.max(1, (this.accountConfig.maxRetries ?? 0) + 1);
119
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
120
+ try {
121
+ const response = await this.fetchImpl(endpoint, {
122
+ method: "POST",
123
+ headers: buildOutboundHeaders(this.accountConfig, normalizedRequest, signature, timestamp, nonce),
124
+ body: rawBody,
125
+ signal: createTimeoutSignal(this.accountConfig.readTimeoutMillis ?? 10000)
126
+ });
127
+ if (!response.ok) {
128
+ throw createTransportError(ERROR_CODES.INTERNAL_ERROR, `Outbound HTTP request failed with status ${response.status} ${response.statusText}.`, {
129
+ status: response.status,
130
+ statusText: response.statusText
131
+ }, isRetryableStatus(response.status));
132
+ }
133
+ return parseOutboundResult(await response.json());
134
+ }
135
+ catch (error) {
136
+ if (attempt >= maxAttempts || !shouldRetry(error)) {
137
+ if (error instanceof GenericHttpPluginError) {
138
+ throw error;
139
+ }
140
+ throw createTransportError(ERROR_CODES.INTERNAL_ERROR, "Outbound HTTP transport failed before a valid delivery result was received.", {
141
+ cause: error instanceof Error
142
+ ? `${error.name}: ${error.message}`
143
+ : String(error)
144
+ }, true);
145
+ }
146
+ }
147
+ }
148
+ throw createTransportError(ERROR_CODES.INTERNAL_ERROR, "Outbound HTTP transport exhausted retries without returning a delivery result.", { accountId: request.accountId }, true);
149
+ }
150
+ }
@@ -0,0 +1,29 @@
1
+ import type { AttachmentDto, ConversationDto, MessageDto } from "../protocol/dto.js";
2
+ export interface OutboundMessageRequest {
3
+ requestId: string;
4
+ accountId: string;
5
+ conversation: ConversationDto;
6
+ threadId: string | null;
7
+ message: MessageDto;
8
+ metadata: Record<string, unknown>;
9
+ }
10
+ export interface OutboundMessageResult {
11
+ success: true;
12
+ code: "DELIVERED";
13
+ providerMessageId: string;
14
+ acceptedAt: string;
15
+ metadata: Record<string, unknown>;
16
+ }
17
+ export interface InternalOutboundMessage {
18
+ requestId: string;
19
+ accountId: string;
20
+ conversationId: string;
21
+ conversationType: ConversationDto["type"];
22
+ threadId?: string | null;
23
+ messageId: string;
24
+ text?: string | null;
25
+ attachments?: AttachmentDto[];
26
+ replyToMessageId?: string | null;
27
+ metadata?: Record<string, unknown>;
28
+ }
29
+ export declare function mapOutboundMessage(message: InternalOutboundMessage): OutboundMessageRequest;
@@ -0,0 +1,19 @@
1
+ import { normalizeAttachment } from "../protocol/attachments.js";
2
+ export function mapOutboundMessage(message) {
3
+ return {
4
+ requestId: message.requestId,
5
+ accountId: message.accountId,
6
+ conversation: {
7
+ conversationId: message.conversationId,
8
+ type: message.conversationType
9
+ },
10
+ threadId: message.threadId ?? null,
11
+ message: {
12
+ messageId: message.messageId,
13
+ text: message.text ?? null,
14
+ attachments: (message.attachments ?? []).map((attachment) => normalizeAttachment(attachment)),
15
+ replyToMessageId: message.replyToMessageId ?? null
16
+ },
17
+ metadata: message.metadata ?? {}
18
+ };
19
+ }
@@ -0,0 +1,18 @@
1
+ import type { OutboundClient } from "./client.js";
2
+ import type { OutboundMessageRequest, OutboundMessageResult } from "./mapper.js";
3
+ export interface MockOutboundClientOptions {
4
+ nowIsoString?: () => string;
5
+ providerMessageIdPrefix?: string;
6
+ }
7
+ /**
8
+ * Simple in-memory outbound client for local demos and tests. It lets the
9
+ * plugin exercise the full outbound mapping/sender/controller flow before a
10
+ * real HTTP transport is introduced.
11
+ */
12
+ export declare class MockOutboundClient implements OutboundClient {
13
+ readonly requests: OutboundMessageRequest[];
14
+ private readonly nowIsoString;
15
+ private readonly providerMessageIdPrefix;
16
+ constructor(options?: MockOutboundClientOptions);
17
+ send(request: OutboundMessageRequest): Promise<OutboundMessageResult>;
18
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Simple in-memory outbound client for local demos and tests. It lets the
3
+ * plugin exercise the full outbound mapping/sender/controller flow before a
4
+ * real HTTP transport is introduced.
5
+ */
6
+ export class MockOutboundClient {
7
+ requests = [];
8
+ nowIsoString;
9
+ providerMessageIdPrefix;
10
+ constructor(options = {}) {
11
+ this.nowIsoString = options.nowIsoString ?? (() => new Date().toISOString());
12
+ this.providerMessageIdPrefix = options.providerMessageIdPrefix ?? "mock";
13
+ }
14
+ async send(request) {
15
+ this.requests.push(request);
16
+ return {
17
+ success: true,
18
+ code: "DELIVERED",
19
+ providerMessageId: `${this.providerMessageIdPrefix}-${request.message.messageId}`,
20
+ acceptedAt: this.nowIsoString(),
21
+ metadata: {
22
+ transport: "mock"
23
+ }
24
+ };
25
+ }
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { OutboundClient } from "./client.js";
2
+ import { type InternalOutboundMessage, type OutboundMessageResult } from "./mapper.js";
3
+ export declare function sendOutboundMessage(client: OutboundClient, message: InternalOutboundMessage): Promise<OutboundMessageResult>;
@@ -0,0 +1,5 @@
1
+ import { mapOutboundMessage } from "./mapper.js";
2
+ export async function sendOutboundMessage(client, message) {
3
+ const request = mapOutboundMessage(message);
4
+ return client.send(request);
5
+ }
@@ -0,0 +1,10 @@
1
+ import type { AttachmentDto } from "./dto.js";
2
+ export declare const DEFAULT_MAX_ATTACHMENT_SIZE_BYTES: number;
3
+ export interface NormalizeAttachmentOptions {
4
+ maxAttachmentSizeBytes?: number;
5
+ }
6
+ /**
7
+ * Centralize attachment shaping rules so inbound validation and outbound
8
+ * serialization do not drift apart when file/image support evolves.
9
+ */
10
+ export declare function normalizeAttachment(attachment: AttachmentDto, options?: NormalizeAttachmentOptions): AttachmentDto;
@@ -0,0 +1,56 @@
1
+ import { ERROR_CODES } from "../errors/codes.js";
2
+ import { GenericHttpPluginError } from "../errors/exceptions.js";
3
+ export const DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
4
+ function inferAttachmentKind(attachment) {
5
+ if (attachment.kind !== undefined) {
6
+ return attachment.kind;
7
+ }
8
+ if (attachment.contentType?.startsWith("image/") === true) {
9
+ return "image";
10
+ }
11
+ return "file";
12
+ }
13
+ function normalizeAttachmentName(attachment) {
14
+ if (attachment.name === undefined) {
15
+ return undefined;
16
+ }
17
+ const normalized = attachment.name.trim();
18
+ return normalized === "" ? undefined : normalized;
19
+ }
20
+ /**
21
+ * Centralize attachment shaping rules so inbound validation and outbound
22
+ * serialization do not drift apart when file/image support evolves.
23
+ */
24
+ export function normalizeAttachment(attachment, options = {}) {
25
+ const normalized = {
26
+ kind: inferAttachmentKind(attachment),
27
+ id: attachment.id,
28
+ name: normalizeAttachmentName(attachment),
29
+ contentType: attachment.contentType,
30
+ url: attachment.url,
31
+ contentBase64: attachment.contentBase64,
32
+ sizeBytes: attachment.sizeBytes,
33
+ caption: attachment.caption ?? null,
34
+ altText: attachment.altText ?? null,
35
+ previewUrl: attachment.previewUrl,
36
+ metadata: attachment.metadata ?? {}
37
+ };
38
+ const maxAttachmentSizeBytes = options.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES;
39
+ if (normalized.sizeBytes !== undefined &&
40
+ normalized.sizeBytes > maxAttachmentSizeBytes) {
41
+ throw new GenericHttpPluginError(ERROR_CODES.INVALID_REQUEST, "Attachment size exceeds the supported maximum.", {
42
+ maxAttachmentSizeBytes,
43
+ sizeBytes: normalized.sizeBytes
44
+ });
45
+ }
46
+ if (normalized.kind === "image") {
47
+ if (normalized.contentType !== undefined &&
48
+ !normalized.contentType.startsWith("image/")) {
49
+ throw new GenericHttpPluginError(ERROR_CODES.INVALID_FIELD_FORMAT, "Image attachment contentType must start with image/.", {
50
+ field: "contentType",
51
+ contentType: normalized.contentType
52
+ });
53
+ }
54
+ }
55
+ return normalized;
56
+ }
@@ -0,0 +1,46 @@
1
+ export interface ConversationDto {
2
+ conversationId: string;
3
+ type: ConversationType;
4
+ title?: string | null;
5
+ metadata?: Record<string, unknown>;
6
+ }
7
+ export type ConversationType = "dm" | "group" | "room" | "ticket";
8
+ export type SenderType = "user" | "bot" | "system";
9
+ export type AttachmentKind = "file" | "image";
10
+ export interface SenderDto {
11
+ id: string;
12
+ name?: string | null;
13
+ type: SenderType;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+ export interface AttachmentDto {
17
+ kind?: AttachmentKind;
18
+ id?: string;
19
+ name?: string;
20
+ contentType?: string;
21
+ url?: string;
22
+ contentBase64?: string;
23
+ sizeBytes?: number;
24
+ caption?: string | null;
25
+ altText?: string | null;
26
+ previewUrl?: string;
27
+ metadata?: Record<string, unknown>;
28
+ }
29
+ export interface MessageDto {
30
+ messageId: string;
31
+ text?: string | null;
32
+ attachments?: AttachmentDto[];
33
+ replyToMessageId?: string | null;
34
+ metadata?: Record<string, unknown>;
35
+ }
36
+ export interface InboundMessageRequestDto {
37
+ eventId: string;
38
+ accountId: string;
39
+ conversation: ConversationDto;
40
+ threadId?: string | null;
41
+ sender: SenderDto;
42
+ message: MessageDto;
43
+ occurredAt?: string;
44
+ idempotencyKey?: string;
45
+ metadata?: Record<string, unknown>;
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function serializeProtocolObject(value: unknown): string;
@@ -0,0 +1,3 @@
1
+ export function serializeProtocolObject(value) {
2
+ return JSON.stringify(value);
3
+ }
@@ -0,0 +1,30 @@
1
+ export interface NonceStore {
2
+ tryUse(nonce: string, timestampEpochSeconds: number): boolean;
3
+ }
4
+ export interface InMemoryNonceStoreOptions {
5
+ /**
6
+ * Maximum allowed age window for accepted nonces, in seconds.
7
+ *
8
+ * Entries older than this window are evicted during normal insert/check
9
+ * operations so the store stays bounded for the minimum viable runtime.
10
+ */
11
+ ttlSeconds?: number;
12
+ /**
13
+ * Clock source override for tests and deterministic validation flows.
14
+ */
15
+ nowEpochSeconds?: () => number;
16
+ }
17
+ /**
18
+ * Small in-memory nonce store for local development and single-process
19
+ * deployments. This is enough for the first runtime loop and can later be
20
+ * replaced with Redis or another shared backing store without changing the
21
+ * caller contract.
22
+ */
23
+ export declare class InMemoryNonceStore implements NonceStore {
24
+ private readonly ttlSeconds;
25
+ private readonly nowEpochSeconds;
26
+ private readonly entries;
27
+ constructor(options?: InMemoryNonceStoreOptions);
28
+ tryUse(nonce: string, timestampEpochSeconds: number): boolean;
29
+ private evictExpired;
30
+ }
@@ -0,0 +1,32 @@
1
+ const DEFAULT_TTL_SECONDS = 300;
2
+ /**
3
+ * Small in-memory nonce store for local development and single-process
4
+ * deployments. This is enough for the first runtime loop and can later be
5
+ * replaced with Redis or another shared backing store without changing the
6
+ * caller contract.
7
+ */
8
+ export class InMemoryNonceStore {
9
+ ttlSeconds;
10
+ nowEpochSeconds;
11
+ entries = new Map();
12
+ constructor(options = {}) {
13
+ this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
14
+ this.nowEpochSeconds = options.nowEpochSeconds ?? (() => Math.floor(Date.now() / 1000));
15
+ }
16
+ tryUse(nonce, timestampEpochSeconds) {
17
+ this.evictExpired();
18
+ if (this.entries.has(nonce)) {
19
+ return false;
20
+ }
21
+ this.entries.set(nonce, timestampEpochSeconds);
22
+ return true;
23
+ }
24
+ evictExpired() {
25
+ const cutoff = this.nowEpochSeconds() - this.ttlSeconds;
26
+ for (const [nonce, timestamp] of this.entries.entries()) {
27
+ if (timestamp < cutoff) {
28
+ this.entries.delete(nonce);
29
+ }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,10 @@
1
+ export interface SignatureInput {
2
+ method: string;
3
+ path: string;
4
+ timestamp: string;
5
+ nonce: string;
6
+ rawBody?: string | null;
7
+ }
8
+ export declare function sha256Hex(rawBody?: string | null): string;
9
+ export declare function buildCanonicalString(input: SignatureInput): string;
10
+ export declare function signPayload(secret: string, input: SignatureInput): string;
@@ -0,0 +1,20 @@
1
+ import { createHash, createHmac } from "node:crypto";
2
+ export function sha256Hex(rawBody) {
3
+ return createHash("sha256").update(rawBody ?? "", "utf8").digest("hex");
4
+ }
5
+ export function buildCanonicalString(input) {
6
+ // Keep canonicalization byte-for-byte aligned with the published protocol spec
7
+ // so every SDK and plugin implementation produces the same signature.
8
+ return [
9
+ input.method.toUpperCase(),
10
+ input.path,
11
+ input.timestamp,
12
+ input.nonce,
13
+ sha256Hex(input.rawBody)
14
+ ].join("\n");
15
+ }
16
+ export function signPayload(secret, input) {
17
+ return createHmac("sha256", secret)
18
+ .update(buildCanonicalString(input), "utf8")
19
+ .digest("hex");
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type SignatureInput } from "./signer.js";
2
+ export declare function verifyPayload(expectedSignature: string, secret: string, input: SignatureInput): boolean;
@@ -0,0 +1,20 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { signPayload } from "./signer.js";
3
+ function normalizeHex(value) {
4
+ if (!/^[0-9a-f]+$/i.test(value) || value.length % 2 !== 0) {
5
+ return null;
6
+ }
7
+ return Buffer.from(value.toLowerCase(), "hex");
8
+ }
9
+ export function verifyPayload(expectedSignature, secret, input) {
10
+ const provided = normalizeHex(expectedSignature);
11
+ if (provided === null) {
12
+ return false;
13
+ }
14
+ const computed = normalizeHex(signPayload(secret, input));
15
+ if (computed === null || provided.length !== computed.length) {
16
+ return false;
17
+ }
18
+ // Avoid leaking signature match information through early-return timing.
19
+ return timingSafeEqual(provided, computed);
20
+ }