@poncho-ai/harness 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import {
5
+ ensureAgentIdentity,
6
+ getAgentStoreDirectory,
7
+ slugifyStorageComponent,
8
+ STORAGE_SCHEMA_VERSION,
9
+ } from "./agent-identity.js";
10
+ import { createRawKVStore, type RawKVStore } from "./kv-store.js";
11
+ import type { StateConfig } from "./state.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Interface
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface SecretsStore {
18
+ get(tenantId: string): Promise<Record<string, string>>;
19
+ set(tenantId: string, key: string, value: string): Promise<void>;
20
+ delete(tenantId: string, key: string): Promise<void>;
21
+ list(tenantId: string): Promise<string[]>;
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Encryption helpers (AES-256-GCM)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ type EncryptedBlob = { iv: string; ct: string; tag: string };
29
+
30
+ function deriveKey(signingKey: string): Buffer {
31
+ // HKDF-like derivation: SHA-256 of fixed salt + signing key
32
+ return createHash("sha256")
33
+ .update("poncho-secrets-v1:" + signingKey)
34
+ .digest();
35
+ }
36
+
37
+ function encrypt(plaintext: string, key: Buffer): EncryptedBlob {
38
+ const iv = randomBytes(12);
39
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
40
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
41
+ const tag = cipher.getAuthTag();
42
+ return {
43
+ iv: iv.toString("base64"),
44
+ ct: ct.toString("base64"),
45
+ tag: tag.toString("base64"),
46
+ };
47
+ }
48
+
49
+ function decrypt(blob: EncryptedBlob, key: Buffer): string {
50
+ const decipher = createDecipheriv(
51
+ "aes-256-gcm",
52
+ key,
53
+ Buffer.from(blob.iv, "base64"),
54
+ );
55
+ decipher.setAuthTag(Buffer.from(blob.tag, "base64"));
56
+ return Buffer.concat([
57
+ decipher.update(Buffer.from(blob.ct, "base64")),
58
+ decipher.final(),
59
+ ]).toString("utf8");
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // File-based implementation
64
+ // ---------------------------------------------------------------------------
65
+
66
+ class FileSecretsStore implements SecretsStore {
67
+ private readonly workingDir: string;
68
+ private readonly encKey: Buffer;
69
+
70
+ constructor(workingDir: string, signingKey: string) {
71
+ this.workingDir = workingDir;
72
+ this.encKey = deriveKey(signingKey);
73
+ }
74
+
75
+ private async filePath(tenantId: string): Promise<string> {
76
+ const identity = await ensureAgentIdentity(this.workingDir);
77
+ const dir = resolve(
78
+ getAgentStoreDirectory(identity),
79
+ "tenants",
80
+ slugifyStorageComponent(tenantId),
81
+ );
82
+ return resolve(dir, "secrets.json");
83
+ }
84
+
85
+ private async readAll(tenantId: string): Promise<Record<string, EncryptedBlob>> {
86
+ try {
87
+ const raw = await readFile(await this.filePath(tenantId), "utf8");
88
+ return JSON.parse(raw) as Record<string, EncryptedBlob>;
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ private async writeAll(
95
+ tenantId: string,
96
+ data: Record<string, EncryptedBlob>,
97
+ ): Promise<void> {
98
+ const fp = await this.filePath(tenantId);
99
+ await mkdir(dirname(fp), { recursive: true });
100
+ await writeFile(fp, JSON.stringify(data, null, 2), "utf8");
101
+ }
102
+
103
+ async get(tenantId: string): Promise<Record<string, string>> {
104
+ const data = await this.readAll(tenantId);
105
+ const result: Record<string, string> = {};
106
+ for (const [k, blob] of Object.entries(data)) {
107
+ try {
108
+ result[k] = decrypt(blob, this.encKey);
109
+ } catch {
110
+ // Skip entries that can't be decrypted (key rotation)
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+
116
+ async set(tenantId: string, key: string, value: string): Promise<void> {
117
+ const data = await this.readAll(tenantId);
118
+ data[key] = encrypt(value, this.encKey);
119
+ await this.writeAll(tenantId, data);
120
+ }
121
+
122
+ async delete(tenantId: string, key: string): Promise<void> {
123
+ const data = await this.readAll(tenantId);
124
+ delete data[key];
125
+ await this.writeAll(tenantId, data);
126
+ }
127
+
128
+ async list(tenantId: string): Promise<string[]> {
129
+ const data = await this.readAll(tenantId);
130
+ return Object.keys(data);
131
+ }
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // KV-backed implementation
136
+ // ---------------------------------------------------------------------------
137
+
138
+ class KVSecretsStore implements SecretsStore {
139
+ private readonly kv: RawKVStore;
140
+ private readonly baseKey: string;
141
+ private readonly encKey: Buffer;
142
+ private readonly ttl?: number;
143
+
144
+ constructor(kv: RawKVStore, baseKey: string, signingKey: string, ttl?: number) {
145
+ this.kv = kv;
146
+ this.baseKey = baseKey;
147
+ this.encKey = deriveKey(signingKey);
148
+ this.ttl = ttl;
149
+ }
150
+
151
+ private kvKey(tenantId: string): string {
152
+ return `${this.baseKey}:t:${slugifyStorageComponent(tenantId)}:secrets`;
153
+ }
154
+
155
+ private async readAll(tenantId: string): Promise<Record<string, EncryptedBlob>> {
156
+ try {
157
+ const raw = await this.kv.get(this.kvKey(tenantId));
158
+ if (!raw) return {};
159
+ return JSON.parse(raw) as Record<string, EncryptedBlob>;
160
+ } catch {
161
+ return {};
162
+ }
163
+ }
164
+
165
+ private async writeAll(
166
+ tenantId: string,
167
+ data: Record<string, EncryptedBlob>,
168
+ ): Promise<void> {
169
+ const key = this.kvKey(tenantId);
170
+ const value = JSON.stringify(data);
171
+ if (this.ttl) {
172
+ await this.kv.setWithTtl(key, value, this.ttl);
173
+ } else {
174
+ await this.kv.set(key, value);
175
+ }
176
+ }
177
+
178
+ async get(tenantId: string): Promise<Record<string, string>> {
179
+ const data = await this.readAll(tenantId);
180
+ const result: Record<string, string> = {};
181
+ for (const [k, blob] of Object.entries(data)) {
182
+ try {
183
+ result[k] = decrypt(blob, this.encKey);
184
+ } catch {
185
+ // Skip entries that can't be decrypted
186
+ }
187
+ }
188
+ return result;
189
+ }
190
+
191
+ async set(tenantId: string, key: string, value: string): Promise<void> {
192
+ const data = await this.readAll(tenantId);
193
+ data[key] = encrypt(value, this.encKey);
194
+ await this.writeAll(tenantId, data);
195
+ }
196
+
197
+ async delete(tenantId: string, key: string): Promise<void> {
198
+ const data = await this.readAll(tenantId);
199
+ delete data[key];
200
+ await this.writeAll(tenantId, data);
201
+ }
202
+
203
+ async list(tenantId: string): Promise<string[]> {
204
+ const data = await this.readAll(tenantId);
205
+ return Object.keys(data);
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Factory
211
+ // ---------------------------------------------------------------------------
212
+
213
+ export const createSecretsStore = (
214
+ agentId: string,
215
+ signingKey: string,
216
+ config?: StateConfig,
217
+ options?: { workingDir?: string },
218
+ ): SecretsStore => {
219
+ const provider = config?.provider ?? "local";
220
+ const ttl = typeof config?.ttl === "number" ? config.ttl : undefined;
221
+ const workingDir = options?.workingDir ?? process.cwd();
222
+
223
+ if (provider === "local" || provider === "memory") {
224
+ return new FileSecretsStore(workingDir, signingKey);
225
+ }
226
+
227
+ const kv = createRawKVStore(config);
228
+ if (kv) {
229
+ const baseKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
230
+ return new KVSecretsStore(kv, baseKey, signingKey, ttl);
231
+ }
232
+ return new FileSecretsStore(workingDir, signingKey);
233
+ };
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Env resolution helper
237
+ // ---------------------------------------------------------------------------
238
+
239
+ /**
240
+ * Resolve an env var name: check tenant secrets first, then process.env.
241
+ */
242
+ export async function resolveEnv(
243
+ secretsStore: SecretsStore | undefined,
244
+ tenantId: string | null | undefined,
245
+ envName: string,
246
+ ): Promise<string | undefined> {
247
+ if (tenantId && secretsStore) {
248
+ const secrets = await secretsStore.get(tenantId);
249
+ if (secrets[envName]) return secrets[envName];
250
+ }
251
+ return process.env[envName];
252
+ }
package/src/state.ts CHANGED
@@ -97,10 +97,16 @@ export interface Conversation {
97
97
  }
98
98
 
99
99
  export interface ConversationStore {
100
- list(ownerId?: string): Promise<Conversation[]>;
101
- listSummaries(ownerId?: string): Promise<ConversationSummary[]>;
100
+ /**
101
+ * List conversations. tenantId semantics:
102
+ * undefined = no filter (builder/admin sees everything)
103
+ * null = legacy single-user only
104
+ * string = tenant-scoped
105
+ */
106
+ list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]>;
107
+ listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]>;
102
108
  get(conversationId: string): Promise<Conversation | undefined>;
103
- create(ownerId?: string, title?: string): Promise<Conversation>;
109
+ create(ownerId?: string, title?: string, tenantId?: string | null): Promise<Conversation>;
104
110
  update(conversation: Conversation): Promise<void>;
105
111
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
106
112
  delete(conversationId: string): Promise<boolean>;
@@ -275,17 +281,19 @@ export class InMemoryConversationStore implements ConversationStore {
275
281
  }
276
282
  }
277
283
 
278
- async list(ownerId?: string): Promise<Conversation[]> {
284
+ async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
279
285
  this.purgeExpired();
280
286
  return Array.from(this.conversations.values())
281
287
  .filter((conversation) => !ownerId || conversation.ownerId === ownerId)
288
+ .filter((c) => tenantId === undefined || c.tenantId === tenantId)
282
289
  .sort((a, b) => b.updatedAt - a.updatedAt);
283
290
  }
284
291
 
285
- async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
292
+ async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
286
293
  this.purgeExpired();
287
294
  return Array.from(this.conversations.values())
288
295
  .filter((c) => !ownerId || c.ownerId === ownerId)
296
+ .filter((c) => tenantId === undefined || c.tenantId === tenantId)
289
297
  .sort((a, b) => b.updatedAt - a.updatedAt)
290
298
  .map((c) => ({
291
299
  conversationId: c.conversationId,
@@ -293,6 +301,7 @@ export class InMemoryConversationStore implements ConversationStore {
293
301
  updatedAt: c.updatedAt,
294
302
  createdAt: c.createdAt,
295
303
  ownerId: c.ownerId,
304
+ tenantId: c.tenantId,
296
305
  parentConversationId: c.parentConversationId,
297
306
  messageCount: c.messages.length,
298
307
  hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
@@ -305,14 +314,14 @@ export class InMemoryConversationStore implements ConversationStore {
305
314
  return this.conversations.get(conversationId);
306
315
  }
307
316
 
308
- async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
317
+ async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
309
318
  const now = Date.now();
310
319
  const conversation: Conversation = {
311
320
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
312
321
  title: normalizeTitle(title),
313
322
  messages: [],
314
323
  ownerId,
315
- tenantId: null,
324
+ tenantId,
316
325
  createdAt: now,
317
326
  updatedAt: now,
318
327
  };
@@ -368,6 +377,7 @@ export type ConversationSummary = {
368
377
  updatedAt: number;
369
378
  createdAt?: number;
370
379
  ownerId: string;
380
+ tenantId?: string | null;
371
381
  parentConversationId?: string;
372
382
  messageCount?: number;
373
383
  hasPendingApprovals?: boolean;
@@ -386,6 +396,7 @@ type ConversationStoreFile = {
386
396
  updatedAt: number;
387
397
  createdAt?: number;
388
398
  ownerId: string;
399
+ tenantId?: string | null;
389
400
  fileName: string;
390
401
  parentConversationId?: string;
391
402
  messageCount?: number;
@@ -465,6 +476,7 @@ class FileConversationStore implements ConversationStore {
465
476
  updatedAt: conversation.updatedAt,
466
477
  createdAt: conversation.createdAt,
467
478
  ownerId: conversation.ownerId,
479
+ tenantId: conversation.tenantId,
468
480
  fileName: entry.name,
469
481
  parentConversationId: conversation.parentConversationId,
470
482
  messageCount: conversation.messages.length,
@@ -523,6 +535,7 @@ class FileConversationStore implements ConversationStore {
523
535
  updatedAt: conversation.updatedAt,
524
536
  createdAt: conversation.createdAt,
525
537
  ownerId: conversation.ownerId,
538
+ tenantId: conversation.tenantId,
526
539
  fileName,
527
540
  parentConversationId: conversation.parentConversationId,
528
541
  messageCount: conversation.messages.length,
@@ -534,10 +547,11 @@ class FileConversationStore implements ConversationStore {
534
547
  await this.writing;
535
548
  }
536
549
 
537
- async list(ownerId?: string): Promise<Conversation[]> {
550
+ async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
538
551
  await this.ensureLoaded();
539
552
  const summaries = Array.from(this.conversations.values())
540
553
  .filter((conversation) => !ownerId || conversation.ownerId === ownerId)
554
+ .filter((c) => tenantId === undefined || (c.tenantId ?? null) === tenantId)
541
555
  .sort((a, b) => b.updatedAt - a.updatedAt);
542
556
  const conversations: Conversation[] = [];
543
557
  for (const summary of summaries) {
@@ -549,10 +563,11 @@ class FileConversationStore implements ConversationStore {
549
563
  return conversations;
550
564
  }
551
565
 
552
- async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
566
+ async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
553
567
  await this.ensureLoaded();
554
568
  return Array.from(this.conversations.values())
555
569
  .filter((c) => !ownerId || c.ownerId === ownerId)
570
+ .filter((c) => tenantId === undefined || (c.tenantId ?? null) === tenantId)
556
571
  .sort((a, b) => b.updatedAt - a.updatedAt)
557
572
  .map((c) => ({
558
573
  conversationId: c.conversationId,
@@ -560,6 +575,7 @@ class FileConversationStore implements ConversationStore {
560
575
  updatedAt: c.updatedAt,
561
576
  createdAt: c.createdAt,
562
577
  ownerId: c.ownerId,
578
+ tenantId: c.tenantId,
563
579
  parentConversationId: c.parentConversationId,
564
580
  messageCount: c.messageCount,
565
581
  hasPendingApprovals: c.hasPendingApprovals,
@@ -576,7 +592,7 @@ class FileConversationStore implements ConversationStore {
576
592
  return await this.readConversationFile(summary.fileName);
577
593
  }
578
594
 
579
- async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
595
+ async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
580
596
  await this.ensureLoaded();
581
597
  const now = Date.now();
582
598
  const conversation: Conversation = {
@@ -584,7 +600,7 @@ class FileConversationStore implements ConversationStore {
584
600
  title: normalizeTitle(title),
585
601
  messages: [],
586
602
  ownerId,
587
- tenantId: null,
603
+ tenantId,
588
604
  createdAt: now,
589
605
  updatedAt: now,
590
606
  };
@@ -770,6 +786,7 @@ type ConversationMeta = {
770
786
  updatedAt: number;
771
787
  createdAt?: number;
772
788
  ownerId: string;
789
+ tenantId?: string | null;
773
790
  parentConversationId?: string;
774
791
  messageCount?: number;
775
792
  hasPendingApprovals?: boolean;
@@ -887,10 +904,10 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
887
904
  }
888
905
  }
889
906
 
890
- async list(ownerId?: string): Promise<Conversation[]> {
907
+ async list(ownerId?: string, tenantId?: string | null): Promise<Conversation[]> {
891
908
  const kv = await this.client();
892
909
  if (!kv) {
893
- return await this.memoryFallback.list(ownerId);
910
+ return await this.memoryFallback.list(ownerId, tenantId);
894
911
  }
895
912
  if (!ownerId) {
896
913
  return [];
@@ -903,16 +920,19 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
903
920
  for (const raw of rawValues) {
904
921
  if (!raw) continue;
905
922
  try {
906
- conversations.push(JSON.parse(raw) as Conversation);
923
+ const conv = JSON.parse(raw) as Conversation;
924
+ if (tenantId === undefined || conv.tenantId === tenantId) {
925
+ conversations.push(conv);
926
+ }
907
927
  } catch { /* skip invalid records */ }
908
928
  }
909
929
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
910
930
  }
911
931
 
912
- async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
932
+ async listSummaries(ownerId?: string, tenantId?: string | null): Promise<ConversationSummary[]> {
913
933
  const kv = await this.client();
914
934
  if (!kv) {
915
- return await this.memoryFallback.listSummaries(ownerId);
935
+ return await this.memoryFallback.listSummaries(ownerId, tenantId);
916
936
  }
917
937
  if (!ownerId) {
918
938
  return [];
@@ -926,13 +946,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
926
946
  if (!raw) continue;
927
947
  try {
928
948
  const meta = JSON.parse(raw) as ConversationMeta;
929
- if (meta.ownerId === ownerId) {
949
+ if (meta.ownerId === ownerId && (tenantId === undefined || (meta.tenantId ?? null) === tenantId)) {
930
950
  summaries.push({
931
951
  conversationId: meta.conversationId,
932
952
  title: meta.title,
933
953
  updatedAt: meta.updatedAt,
934
954
  createdAt: meta.createdAt,
935
955
  ownerId: meta.ownerId,
956
+ tenantId: meta.tenantId,
936
957
  parentConversationId: meta.parentConversationId,
937
958
  messageCount: meta.messageCount,
938
959
  hasPendingApprovals: meta.hasPendingApprovals,
@@ -960,14 +981,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
960
981
  }
961
982
  }
962
983
 
963
- async create(ownerId = DEFAULT_OWNER, title?: string): Promise<Conversation> {
984
+ async create(ownerId = DEFAULT_OWNER, title?: string, tenantId: string | null = null): Promise<Conversation> {
964
985
  const now = Date.now();
965
986
  const conversation: Conversation = {
966
987
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now}-${Math.random()}`,
967
988
  title: normalizeTitle(title),
968
989
  messages: [],
969
990
  ownerId,
970
- tenantId: null,
991
+ tenantId,
971
992
  createdAt: now,
972
993
  updatedAt: now,
973
994
  };
@@ -997,6 +1018,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
997
1018
  updatedAt: nextConversation.updatedAt,
998
1019
  createdAt: nextConversation.createdAt,
999
1020
  ownerId: nextConversation.ownerId,
1021
+ tenantId: nextConversation.tenantId,
1000
1022
  parentConversationId: nextConversation.parentConversationId,
1001
1023
  messageCount: nextConversation.messages.length,
1002
1024
  hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
@@ -24,6 +24,7 @@ export interface SubagentManager {
24
24
  task: string;
25
25
  parentConversationId: string;
26
26
  ownerId: string;
27
+ tenantId?: string | null;
27
28
  }): Promise<SubagentSpawnResult>;
28
29
 
29
30
  sendMessage(subagentId: string, message: string): Promise<SubagentSpawnResult>;
@@ -44,6 +44,7 @@ export const createSubagentTools = (
44
44
  task: task.trim(),
45
45
  parentConversationId: conversationId,
46
46
  ownerId,
47
+ tenantId: context.tenantId,
47
48
  });
48
49
  return { subagentId, status: "running" };
49
50
  },
package/src/telemetry.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import type { AgentEvent } from "@poncho-ai/sdk";
2
2
 
3
3
  const MAX_FIELD_LENGTH = 200;
4
+ const OMIT_FROM_LOG = new Set(["continuationMessages", "_harnessMessages", "messages", "compactedHistory"]);
4
5
 
5
6
  function sanitizeEventForLog(event: AgentEvent): string {
6
- return JSON.stringify(event, (_key, value) => {
7
+ return JSON.stringify(event, (key, value) => {
8
+ if (OMIT_FROM_LOG.has(key) && Array.isArray(value)) {
9
+ return `[${value.length} messages]`;
10
+ }
7
11
  if (typeof value === "string" && value.length > MAX_FIELD_LENGTH) {
8
12
  return `${value.slice(0, 80)}...[${value.length} chars]`;
9
13
  }
@@ -0,0 +1,42 @@
1
+ import { jwtVerify, type JWTPayload } from "jose";
2
+
3
+ export interface TenantTokenPayload {
4
+ tenantId: string;
5
+ metadata?: Record<string, unknown>;
6
+ }
7
+
8
+ /**
9
+ * Verify a tenant JWT (HS256) signed with the given key.
10
+ * Returns the decoded payload on success, or undefined on any failure.
11
+ */
12
+ export async function verifyTenantToken(
13
+ signingKey: string,
14
+ token: string,
15
+ ): Promise<TenantTokenPayload | undefined> {
16
+ try {
17
+ const secret = new TextEncoder().encode(signingKey);
18
+ const { payload } = await jwtVerify(token, secret, {
19
+ algorithms: ["HS256"],
20
+ });
21
+
22
+ const tenantId = payload.sub;
23
+ if (!tenantId || typeof tenantId !== "string") {
24
+ return undefined;
25
+ }
26
+
27
+ const metadata = extractMetadata(payload);
28
+ return { tenantId, metadata };
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function extractMetadata(
35
+ payload: JWTPayload,
36
+ ): Record<string, unknown> | undefined {
37
+ const meta = payload.meta;
38
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
39
+ return meta as Record<string, unknown>;
40
+ }
41
+ return undefined;
42
+ }