@openmarket/rooms-client 0.5.1 → 0.7.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.
@@ -100,14 +100,6 @@ export declare function rearmedRoomState(roomId: string, retired: ArmedRoomState
100
100
  /** Seqs of the user's own posts in a window of entries; the reply-trigger
101
101
  * check tests `inReplyTo` against this set. Pure. */
102
102
  export declare function ownSeqsOf(entries: ReadonlyArray<RoomLedgerEntry>, ownUserId: string): Set<number>;
103
- /**
104
- * Whether a single incoming entry is a qualifying TRIGGER: a NEW human POST in
105
- * the room that @mentions the user's own handle, OR (when `ownSeqs` is given)
106
- * directly replies to one of the user's own messages. Reply semantics match a
107
- * mention (replying pings the author), so armed mode treats both as addressed
108
- * to the user. Agent posts, system entries, the user's own posts, and
109
- * non-mentions never trigger. Pure.
110
- */
111
103
  export declare function isTriggeringEntry(entry: RoomLedgerEntry, ownHandle: string, ownUserId: string, ownSeqs?: ReadonlySet<number>): boolean;
112
104
  /**
113
105
  * The newest qualifying trigger among entries strictly newer than
@@ -117,12 +117,24 @@ export function ownSeqsOf(entries, ownUserId) {
117
117
  * to the user. Agent posts, system entries, the user's own posts, and
118
118
  * non-mentions never trigger. Pure.
119
119
  */
120
+ /** Webhook-ingested entries are machine posts even though isAgent is
121
+ * false (the flag marks agent USERS). They must never count as the human
122
+ * in any autonomy gate: a webhook @mention cannot start an unreviewed
123
+ * autonomous turn, and webhook chatter cannot satisfy the
124
+ * human-activity brake. */
125
+ function isWebhookEntry(entry) {
126
+ return Boolean(entry.metadata?.webhook);
127
+ }
120
128
  export function isTriggeringEntry(entry, ownHandle, ownUserId, ownSeqs) {
121
129
  if (entry.type !== RoomLedgerEntryType.POST)
122
130
  return false;
123
131
  if (entry.isAgent)
124
132
  return false;
125
- if (!entry.text?.trim())
133
+ if (isWebhookEntry(entry))
134
+ return false;
135
+ // A poll is substantive content even with no caption, and a captionless
136
+ // poll REPLY is addressed at us like any other reply.
137
+ if (!entry.text?.trim() && entry.poll == null)
126
138
  return false;
127
139
  // Never trigger on our own message (we post as this handle/userId).
128
140
  const fromHandle = normalizeHandle(entry.handle).toLowerCase();
@@ -139,7 +151,9 @@ export function isTriggeringEntry(entry, ownHandle, ownUserId, ownSeqs) {
139
151
  // old mentionsAgent) fired on ordinary words for short handles like "may" /
140
152
  // "btc"; armed posting has no human review, so it triggers ONLY on an `@handle`
141
153
  // token. Empty handle never matches (mentionsAgentExplicit guards it).
142
- return mentionsAgentExplicit(entry.text, ownHandle);
154
+ // Mentions can live in the poll question too (member-authored content).
155
+ const mentionSource = [entry.text ?? "", entry.poll?.question ?? ""].filter(Boolean).join(" ");
156
+ return mentionsAgentExplicit(mentionSource, ownHandle);
143
157
  }
144
158
  /**
145
159
  * The newest qualifying trigger among entries strictly newer than
@@ -312,13 +326,15 @@ export function isHumanResetEntry(entry, ownHandle, ownUserId) {
312
326
  return false;
313
327
  if (entry.isAgent)
314
328
  return false;
329
+ if (isWebhookEntry(entry))
330
+ return false;
315
331
  if (entry.userId === ownUserId)
316
332
  return false;
317
333
  const fromHandle = normalizeHandle(entry.handle).toLowerCase();
318
334
  const me = normalizeHandle(ownHandle).toLowerCase();
319
335
  if (me.length > 0 && fromHandle === me)
320
336
  return false;
321
- return Boolean(entry.text?.trim());
337
+ return Boolean(entry.text?.trim()) || entry.poll != null;
322
338
  }
323
339
  /** Note a turn/post error against the kill switch; returns true once the room
324
340
  * should auto-disarm (>= maxConsecutiveErrors in a row). Mutates `state`. */
@@ -1,5 +1,12 @@
1
1
  import { type KnownProvider } from "@earendil-works/pi-ai";
2
- export type SurfaceId = "cli" | "telegram" | "discord" | "slack" | "webui";
2
+ /** Every chat surface, as a runtime list. The `SurfaceId` union derives from
3
+ * this literal so per-surface policy checklists (packages/cli/test/
4
+ * harness-budgets.test.ts) can iterate the surfaces at runtime while a new
5
+ * entry still breaks type-exhaustive tables at compile time. Adding a
6
+ * surface here requires classifying it in EVERY policy set (see AGENTS.md,
7
+ * "Harness invariants"). */
8
+ export declare const SURFACE_IDS: readonly ["cli", "telegram", "discord", "slack", "webui"];
9
+ export type SurfaceId = (typeof SURFACE_IDS)[number];
3
10
  export interface AgentToolCall {
4
11
  id: string;
5
12
  name: string;
@@ -21,6 +28,11 @@ export type AgentMessage = {
21
28
  export interface ToolDispatchResult {
22
29
  isError?: true;
23
30
  content: string;
31
+ /** Full-fidelity render payload for in-process SURFACES (e.g. the TUI's
32
+ * backtest card). Present only on the streamed copy channels consume —
33
+ * the agent runtime strips it before the result reaches the model's
34
+ * messages or the conversation store, so it never costs context. */
35
+ artifact?: unknown;
24
36
  }
25
37
  export interface LlmTool {
26
38
  name: string;
@@ -1,4 +1,11 @@
1
1
  import { getProviders } from "@earendil-works/pi-ai";
2
+ /** Every chat surface, as a runtime list. The `SurfaceId` union derives from
3
+ * this literal so per-surface policy checklists (packages/cli/test/
4
+ * harness-budgets.test.ts) can iterate the surfaces at runtime while a new
5
+ * entry still breaks type-exhaustive tables at compile time. Adding a
6
+ * surface here requires classifying it in EVERY policy set (see AGENTS.md,
7
+ * "Harness invariants"). */
8
+ export const SURFACE_IDS = ["cli", "telegram", "discord", "slack", "webui"];
2
9
  /** OM-level provider for any OpenAI-compatible /v1/chat/completions endpoint
3
10
  * (Ollama, vLLM, LM Studio, llama.cpp, custom proxies). Not a pi-ai
4
11
  * KnownProvider: the model object is constructed at turn time from the
@@ -32,7 +39,10 @@ export function stringifyToolContent(value) {
32
39
  return "";
33
40
  if (typeof value === "string")
34
41
  return value;
35
- return JSON.stringify(value, null, 2);
42
+ // Compact on purpose: this string is LLM input, and 2-space pretty-printing
43
+ // inflated every structured tool result by roughly a fifth in tokens for
44
+ // zero model benefit.
45
+ return JSON.stringify(value);
36
46
  }
37
47
  export function parseToolArguments(raw) {
38
48
  if (raw === undefined || raw === null)
@@ -18,6 +18,12 @@
18
18
  import type { RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicsResultPayload } from "../shared/rooms-protocol.js";
19
19
  /** The reserved bucket key for untopicked (linear) messages. */
20
20
  export declare const UNTOPICKED_BUCKET = "";
21
+ /** Effective attention for a bucket: an elapsed timed mute reads as the
22
+ * base attention ('default'). The store expires timed mutes lazily (no
23
+ * deadline-triggered frame), so a hydrated inbox would otherwise stay
24
+ * muted past the deadline until the next hydration. A null or absent
25
+ * deadline keeps the stored attention (permanent mute). */
26
+ export declare function resolveTopicAttention(state: Pick<RoomTopicBucketState, "attention" | "muteUntil">, nowMs?: number): RoomTopicAttention;
21
27
  export interface TopicInboxItem {
22
28
  /** Bucket key: a topicId, or UNTOPICKED_BUCKET for the linear lane. */
23
29
  key: string;
@@ -17,6 +17,17 @@
17
17
  */
18
18
  /** The reserved bucket key for untopicked (linear) messages. */
19
19
  export const UNTOPICKED_BUCKET = "";
20
+ /** Effective attention for a bucket: an elapsed timed mute reads as the
21
+ * base attention ('default'). The store expires timed mutes lazily (no
22
+ * deadline-triggered frame), so a hydrated inbox would otherwise stay
23
+ * muted past the deadline until the next hydration. A null or absent
24
+ * deadline keeps the stored attention (permanent mute). */
25
+ export function resolveTopicAttention(state, nowMs = Date.now()) {
26
+ if (state.attention !== "mute" || state.muteUntil == null)
27
+ return state.attention;
28
+ const deadline = Date.parse(state.muteUntil);
29
+ return Number.isFinite(deadline) && deadline <= nowMs ? "default" : "mute";
30
+ }
20
31
  function emptyBucket() {
21
32
  return { lastSeq: 0, unreadCount: 0, cursor: 0, attention: "default" };
22
33
  }
@@ -111,7 +122,7 @@ export class RoomTopicInbox {
111
122
  return rows;
112
123
  }
113
124
  attentionFor(topicId) {
114
- return this.bucketFor(topicId).attention;
125
+ return resolveTopicAttention(this.bucketFor(topicId));
115
126
  }
116
127
  /** A registry mutation arrived (TOPIC_UPDATED, or a verb result echo). */
117
128
  applyTopicUpdated(topic) {
@@ -160,6 +171,10 @@ export class RoomTopicInbox {
160
171
  this.buckets.set(topicId, bucket);
161
172
  }
162
173
  bucket.attention = attention;
174
+ // The mirror knows only the new attention: a stale deadline from a
175
+ // previous timed mute must not expire it. The next hydration restores
176
+ // any server-side deadline.
177
+ delete bucket.muteUntil;
163
178
  }
164
179
  /**
165
180
  * The viewer saw a bucket up to seq (the view batcher owns the durable
@@ -188,7 +203,7 @@ export class RoomTopicInbox {
188
203
  channelUnread() {
189
204
  let total = 0;
190
205
  for (const bucket of this.buckets.values()) {
191
- if (bucket.attention === "mute")
206
+ if (resolveTopicAttention(bucket) === "mute")
192
207
  continue;
193
208
  total += bucket.unreadCount;
194
209
  }
@@ -205,7 +220,7 @@ export class RoomTopicInbox {
205
220
  inboxItems() {
206
221
  const items = [];
207
222
  const untopicked = this.bucketFor(UNTOPICKED_BUCKET);
208
- if (untopicked.unreadCount > 0 && untopicked.attention !== "mute") {
223
+ if (untopicked.unreadCount > 0 && resolveTopicAttention(untopicked) !== "mute") {
209
224
  items.push({
210
225
  key: UNTOPICKED_BUCKET,
211
226
  topic: undefined,
@@ -218,12 +233,12 @@ export class RoomTopicInbox {
218
233
  if (topic.mergedInto)
219
234
  continue;
220
235
  const state = this.bucketFor(topic.topicId);
221
- if (state.attention === "mute")
236
+ if (resolveTopicAttention(state) === "mute")
222
237
  continue;
223
238
  let aliasUnread = 0;
224
239
  for (const alias of this.aliasesOf(topic.topicId)) {
225
240
  const aliasState = this.bucketFor(alias);
226
- if (aliasState.attention === "mute")
241
+ if (resolveTopicAttention(aliasState) === "mute")
227
242
  continue;
228
243
  aliasUnread += aliasState.unreadCount;
229
244
  }
@@ -222,6 +222,19 @@ export interface RoomLedgerEntry {
222
222
  count: number;
223
223
  }> | undefined;
224
224
  editedAt?: string | number | Date | undefined;
225
+ /** Database-serialized edit counter: positive integer, strictly
226
+ * increasing per event row, incremented on every successful text or
227
+ * caption edit; absent means never edited. Orders caption edits where
228
+ * wall-clock editedAt stamps can tie or skew across store replicas.
229
+ * Missing on stores older than edit revisions. */
230
+ editRevision?: number | undefined;
231
+ /** Database-serialized card-mutation counter: positive integer,
232
+ * strictly increasing per event row, bumped on every unfurl card
233
+ * mutation (attach, edit-drop, purge); absent means cards were never
234
+ * mutated. Consumers apply card patches only at or above their known
235
+ * revision, making stale post-removal unfurl frames inert. Missing on
236
+ * stores older than unfurl revisions. */
237
+ unfurlsRevision?: number | undefined;
225
238
  deletedAt?: string | number | Date | undefined;
226
239
  deletedBy?: string | undefined;
227
240
  replyCount?: number | undefined;
@@ -230,8 +243,14 @@ export interface RoomLedgerEntry {
230
243
  pinnedBy?: string | null | undefined;
231
244
  pinnedAt?: string | number | Date | null | undefined;
232
245
  attachments?: RoomAttachment[] | undefined;
246
+ /** Counts-only poll summary when this message carries a poll. Additive:
247
+ * non-poll entries omit it, and live updates ride ENTRY_UPDATED. */
248
+ poll?: RoomPollSummary | null | undefined;
249
+ /** Server-rendered link unfurl cards. Additive: entries without links
250
+ * (and servers older than the unfurl worker) omit it. */
251
+ unfurls?: RoomUnfurl[] | undefined;
233
252
  roomRole?: RoomRole | null | undefined;
234
- metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
253
+ metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
235
254
  }
236
255
  export type RoomForwardedFrom = {
237
256
  kind: "room";
@@ -245,6 +264,18 @@ export type RoomForwardedFrom = {
245
264
  export interface RoomForwardMetadata {
246
265
  forwardedFrom: RoomForwardedFrom;
247
266
  }
267
+ /** Sender identity for messages posted through a channel webhook (the
268
+ * Discord-compatible ingest). Entries carrying it render the webhook's
269
+ * name and avatar with an APP-style badge; the entry's handle mirrors
270
+ * the display name and its userId is the synthetic `webhook:<id>`, so a
271
+ * webhook can never impersonate a member. */
272
+ export interface RoomWebhookMetadata {
273
+ webhook: {
274
+ id: string;
275
+ name: string;
276
+ avatarUrl?: string | undefined;
277
+ };
278
+ }
248
279
  export interface RoomAttachment {
249
280
  /**
250
281
  * Object key under room-attachments/; clients sign it at render time.
@@ -267,6 +298,46 @@ export interface RoomAttachment {
267
298
  export type RoomAttachmentInput = RoomAttachment & {
268
299
  key: string;
269
300
  };
301
+ export interface RoomPollOption {
302
+ id: string;
303
+ text: string;
304
+ /** Vote count for this option. Counts only, never voter identities
305
+ * (voters are fetched on demand, exactly like reactors). */
306
+ count: number;
307
+ }
308
+ /**
309
+ * Counts-only poll summary denormalized onto the carrying message (a poll
310
+ * IS a message). Vote/close/edit updates ride the existing ENTRY_UPDATED
311
+ * frame with the refreshed summary; the server's side collections stay the
312
+ * authoritative copy.
313
+ */
314
+ export interface RoomPollSummary {
315
+ question: string;
316
+ options: RoomPollOption[];
317
+ multi: boolean;
318
+ /** ISO date when the poll auto-closes; null or absent means open-ended. */
319
+ closesAt?: string | null | undefined;
320
+ closed: boolean;
321
+ /** Store-serialized poll ordering revision, strictly increasing per
322
+ * poll: consumers reject frames below their known revision so
323
+ * bus-reordered vote/close updates cannot regress tallies. Present
324
+ * where the relay forwards it; missing on older relays. */
325
+ revision?: number | undefined;
326
+ }
327
+ /**
328
+ * Server-rendered link unfurl card on an entry. `url` is the canonical
329
+ * URL; `thumbnailKey` is the re-hosted copy in the attachments bucket,
330
+ * signed through the member-scoped attachment path. The origin's image URL
331
+ * never crosses the wire. Cards appear via ENTRY_UPDATED when the unfurl
332
+ * worker lands.
333
+ */
334
+ export interface RoomUnfurl {
335
+ url: string;
336
+ title?: string | undefined;
337
+ description?: string | undefined;
338
+ siteName?: string | undefined;
339
+ thumbnailKey?: string | undefined;
340
+ }
270
341
  /**
271
342
  * A registry-backed topic inside a channel. The id is permanent: rename
272
343
  * changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
@@ -330,6 +401,21 @@ export interface RoomReadStatePayload {
330
401
  * Present when the store surfaces it and on ROOM_SET_ATTENTION acks
331
402
  * (which reply READ_STATE scoped to the one room, readState empty). */
332
403
  attention?: Record<string, RoomTopicAttention> | undefined;
404
+ /** Per-room timed-mute expiry (ISO date), keyed like attention. Rides
405
+ * beside a 'mute' attention entry when the mute carries an expiry; null
406
+ * marks an explicitly cleared or permanent mute. A lapsed expiry never
407
+ * appears: the server resolves it at read time and the attention value
408
+ * reads as its base ('default'). Missing on servers older than
409
+ * mute-with-duration. */
410
+ muteUntil?: Record<string, string | null> | undefined;
411
+ /** Per-room attention commit revision, keyed like attention: a
412
+ * database-serialized, strictly increasing counter per (user, room),
413
+ * present only for rooms whose attention has ever been committed.
414
+ * ORDERING CONTRACT: a consumer that has synced revision R for a room
415
+ * must ignore snapshot attention entries whose revision is <= R (a
416
+ * snapshot read before a newer commit can arrive after that commit's
417
+ * sync frame). Missing on stores older than attention revisions. */
418
+ attentionRevision?: Record<string, number> | undefined;
333
419
  /** Unread mention counts by room, server-computed from the notify
334
420
  * worker's ROOM_MENTION rows so badges survive reload and clear through
335
421
  * the ack path. Unlike readState/attention the map is SPARSE AND
@@ -501,7 +587,7 @@ export interface RoomDmMessage {
501
587
  createdAt?: string | number | undefined;
502
588
  /** Sealed DM: content is '' and the opaque envelope rides here. */
503
589
  secret?: RoomLedgerSecretPayload | null | undefined;
504
- metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
590
+ metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
505
591
  /** Legacy public chat-image URL (renders as-is, no signing). */
506
592
  imageUrl?: string | null | undefined;
507
593
  /** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
@@ -852,6 +938,14 @@ export interface RoomTopicBucketState {
852
938
  unreadCount: number;
853
939
  cursor: number;
854
940
  attention: RoomTopicAttention;
941
+ /** Standing timed-mute deadline (ISO date; null = permanent mute).
942
+ * Present only on the room-wide '' bucket and only while its attention
943
+ * is an active 'mute'; never on topic buckets. Same semantics as the
944
+ * read-state muteUntil: the store expires timed mutes lazily, so
945
+ * consumers resolve an elapsed deadline back to the base attention
946
+ * ('default') at read time (see resolveTopicAttention). Missing on
947
+ * servers older than mute-with-duration. */
948
+ muteUntil?: string | null | undefined;
855
949
  }
856
950
  export interface RoomTopicResultPayload {
857
951
  room: string;
@@ -1,5 +1,6 @@
1
- import type { RoomDmConversation, RoomDmHistory, RoomDmSendResult, RoomDmUnreadConversation, RoomDoc, RoomDocAttribution, RoomDocChanges, RoomDocConflictHead, RoomDocEditingLease, RoomDocProvenance, RoomDocRevision, RoomDocRunRevertResult, RoomDocWriteResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequests, RoomNotificationInboxResult, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomSocialConfig, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSuggestion, RoomSuggestionRequest } from "./social-types.js";
2
- export type { RoomAttachment, RoomDmConversation, RoomDmHistory, RoomDmMessage, RoomDmParticipant, RoomDmSendResult, RoomDmUnreadConversation, RoomDocAttribution, RoomDocEditingLease, RoomDocMergeRegion, RoomDocRevisionSource, RoomDocRunRevertResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequest, RoomFriendRequests, RoomNotificationInboxItem, RoomNotificationInboxResult, RoomNotificationKind, RoomNotificationMessageData, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomReplyInboxItem, RoomSocialConfig, RoomSpaceRestoreAction, RoomSpaceRestoreActionKind, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSpaceRestoreSummary, RoomSuggestion, RoomSuggestionIntent, RoomSuggestionRequest, RoomSuggestionSource, } from "./social-types.js";
1
+ import type { RoomTopicAttention } from "./shared/rooms-protocol.js";
2
+ import type { RoomBookmarkSaved, RoomBookmarksResult, RoomDmConversation, RoomDmHistory, RoomDmSendResult, RoomDmUnreadConversation, RoomDoc, RoomDocAttribution, RoomDocChanges, RoomDocConflictHead, RoomDocEditingLease, RoomDocProvenance, RoomDocRevision, RoomDocRunRevertResult, RoomDocWriteResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequests, RoomNotificationInboxResult, RoomPollVotersResult, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomRestEvent, RoomSocialConfig, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSuggestion, RoomSuggestionRequest, RoomUserBlock, RoomUserBlocksResult } from "./social-types.js";
3
+ export type { RoomAttachment, RoomBookmark, RoomBookmarkSaved, RoomBookmarksResult, RoomDmConversation, RoomDmHistory, RoomDmMessage, RoomDmParticipant, RoomDmSendResult, RoomDmUnreadConversation, RoomDocAttribution, RoomDocEditingLease, RoomDocMergeRegion, RoomDocRevisionSource, RoomDocRunRevertResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequest, RoomFriendRequests, RoomNotificationInboxItem, RoomNotificationInboxResult, RoomNotificationKind, RoomNotificationMessageData, RoomNotifyQuietHours, RoomPollOptionVoters, RoomPollVotersResult, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomReplyInboxItem, RoomRestEvent, RoomSocialConfig, RoomSpaceNotificationOverride, RoomSpaceRestoreAction, RoomSpaceRestoreActionKind, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSpaceRestoreSummary, RoomSuggestion, RoomSuggestionIntent, RoomSuggestionRequest, RoomSuggestionSource, RoomsNotificationSettings, RoomUserBlock, RoomUserBlocksResult, } from "./social-types.js";
3
4
  /** Largest per-file size any tier accepts (plus tier). The server enforces
4
5
  * the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
5
6
  * constant only pre-flights uploads no tier could ever accept. */
@@ -210,6 +211,31 @@ export declare function updateSpaceRole(config: RoomSocialConfig, spaceId: strin
210
211
  position?: number;
211
212
  }): Promise<RoomSpaceRole>;
212
213
  export declare function deleteSpaceRole(config: RoomSocialConfig, spaceId: string, roleId: string): Promise<void>;
214
+ /** A channel webhook: the Discord-compatible ingest capability. The
215
+ * posting URL is `<chat-api>/webhooks/<webhookId>/<token>`; the token is
216
+ * the whole credential, so treat listed webhooks like secrets. */
217
+ export interface RoomWebhook {
218
+ webhookId: string;
219
+ room: string;
220
+ name: string;
221
+ avatarUrl: string | null;
222
+ token: string;
223
+ createdBy: string;
224
+ /** Creator's handle at create time (display; createdBy is the id). */
225
+ createdByHandle?: string | null;
226
+ createdAt: string;
227
+ lastUsedAt: string | null;
228
+ deliveryCount: number;
229
+ disabled: boolean;
230
+ }
231
+ export declare function listRoomWebhooks(config: RoomSocialConfig, room: string): Promise<RoomWebhook[]>;
232
+ export declare function createRoomWebhook(config: RoomSocialConfig, room: string, input: {
233
+ name: string;
234
+ avatarUrl?: string | null;
235
+ }): Promise<RoomWebhook>;
236
+ export declare function deleteRoomWebhook(config: RoomSocialConfig, room: string, webhookId: string): Promise<void>;
237
+ /** Rotate the capability: the old URL dies immediately. */
238
+ export declare function regenerateRoomWebhookToken(config: RoomSocialConfig, room: string, webhookId: string): Promise<RoomWebhook>;
213
239
  export declare function setSpaceMemberRoles(config: RoomSocialConfig, spaceId: string, targetUserId: string, roleIds: string[]): Promise<{
214
240
  spaceId: string;
215
241
  userId: string;
@@ -229,12 +255,20 @@ export interface RoomSpaceInvite {
229
255
  invitedBy?: string;
230
256
  role: "admin" | "member";
231
257
  createdAt?: string;
258
+ /** Expiry deadline (ISO date); null or absent means the invite stands
259
+ * until accepted or declined. Additive (0.6.0 servers). */
260
+ expiresAt?: string | null;
232
261
  }
233
- /** Create (or refresh) a pending invitation. Membership needs their accept. */
262
+ /** Create (or refresh) a pending invitation. Membership needs their accept.
263
+ * `expiresAt` (ISO date, future, at most 30 days out) puts a deadline on
264
+ * it; re-inviting without one clears any previous deadline. Fail-closed on
265
+ * servers older than 0.6.0: they persist the invite but drop the deadline,
266
+ * so a deadline the echo omits revokes the invite and throws. */
234
267
  export declare function createSpaceInvite(config: RoomSocialConfig, spaceId: string, input: {
235
268
  targetUserId?: string;
236
269
  targetUsername?: string;
237
270
  role?: "admin" | "member";
271
+ expiresAt?: string;
238
272
  }): Promise<RoomSpaceInvite>;
239
273
  /** The caller's own pending invitations. */
240
274
  export declare function listMySpaceInvites(config: RoomSocialConfig): Promise<RoomSpaceInvite[]>;
@@ -249,10 +283,22 @@ export declare function acceptSpaceInvite(config: RoomSocialConfig, inviteId: st
249
283
  export declare function declineSpaceInvite(config: RoomSocialConfig, inviteId: string): Promise<{
250
284
  declined?: boolean;
251
285
  }>;
252
- /** Mint (or rotate) the shareable join code; plaintext comes back once. */
253
- export declare function setSpaceJoinCode(config: RoomSocialConfig, spaceId: string): Promise<{
286
+ /** Mint (or rotate) the shareable join code; plaintext comes back once.
287
+ * Optional caps: `expiresAt` (ISO date, future, at most 30 days out) and
288
+ * `maxUses` (1..10000). Rotation is a fresh grant: the new code starts at
289
+ * zero uses and takes exactly the caps given here (omitted caps clear the
290
+ * previous ones). Servers older than 0.6.0 echo neither cap field; they
291
+ * mint the code but drop the caps, so a requested cap the echo omits
292
+ * fail-closes: the fresh code is revoked and the call throws rather than
293
+ * hand back a live UNLIMITED code the caller believes capped. */
294
+ export declare function setSpaceJoinCode(config: RoomSocialConfig, spaceId: string, options?: {
295
+ expiresAt?: string | undefined;
296
+ maxUses?: number | undefined;
297
+ }): Promise<{
254
298
  spaceId: string;
255
299
  code: string;
300
+ expiresAt?: string | null;
301
+ maxUses?: number | null;
256
302
  }>;
257
303
  export declare function clearSpaceJoinCode(config: RoomSocialConfig, spaceId: string): Promise<{
258
304
  cleared?: boolean;
@@ -386,6 +432,154 @@ export declare function listRoomNotifications(config: RoomSocialConfig, options?
386
432
  unread?: boolean | undefined;
387
433
  signal?: AbortSignal | undefined;
388
434
  }): Promise<RoomNotificationInboxResult>;
435
+ /**
436
+ * Mark a room unread from `seq` (Discord-style): the room-wide read cursor
437
+ * drops to seq - 1 (server-clamped at 0) so the unread divider and pill
438
+ * return. The one sanctioned cursor-regression path; mention rows an
439
+ * earlier ack swept stay swept, and nothing is re-notified.
440
+ */
441
+ export declare function markRoomUnread(config: RoomSocialConfig, room: string, seq: number): Promise<{
442
+ success: boolean;
443
+ }>;
444
+ /**
445
+ * Set the caller's room-level attention over REST, the path that carries
446
+ * mute-with-duration: `untilMs` (epoch ms, valid only with 'mute') stamps
447
+ * the expiry that read state later surfaces as `muteUntil`; the mute lapses
448
+ * server-side and reads as 'default' afterwards. Every write settles the
449
+ * expiry, so follow/default (and a plain permanent mute) clear a previous
450
+ * one. The pinned ROOM_SET_ATTENTION frame (setRoomAttentionOnClient)
451
+ * predates `untilMs` and stays the live-session path for permanent
452
+ * postures; servers older than mute-with-duration strip the field and land
453
+ * the mute permanent, so a requested `untilMs` the echo omits fail-closes:
454
+ * the attention is reset and the call throws rather than leave an
455
+ * unbounded mute the caller believes timed. Echoes the persisted
456
+ * attention; `muteUntil` is the ISO expiry, null when none stands.
457
+ */
458
+ export declare function setRoomAttentionRest(config: RoomSocialConfig, room: string, attention: RoomTopicAttention, options?: {
459
+ untilMs?: number | undefined;
460
+ }): Promise<{
461
+ attention: RoomTopicAttention;
462
+ muteUntil: string | null;
463
+ }>;
464
+ /**
465
+ * Save a message for later (personal, per-user; nothing broadcasts).
466
+ * Idempotent: re-saving an already bookmarked event echoes the standing
467
+ * row with its original createdAt.
468
+ */
469
+ export declare function addRoomBookmark(config: RoomSocialConfig, room: string, seq: number): Promise<RoomBookmarkSaved>;
470
+ export declare function removeRoomBookmark(config: RoomSocialConfig, room: string, seq: number): Promise<{
471
+ removed: boolean;
472
+ }>;
473
+ /**
474
+ * The caller's bookmarks across every room (GET /users/bookmarks), newest
475
+ * first with the notification inbox's composite cursor: pass a page's
476
+ * lastCursor and lastCursorId back TOGETHER for the next page. Rows in
477
+ * rooms the caller can no longer read are subtracted server-side.
478
+ */
479
+ export declare function listUserBookmarks(config: RoomSocialConfig, options?: {
480
+ limit?: number | undefined;
481
+ /** Feed a page's lastCursor back here (explicit undefined reads as
482
+ * "first page", so `page.lastCursor ?? undefined` composes under
483
+ * exactOptionalPropertyTypes). */
484
+ cursor?: string | undefined;
485
+ cursorId?: string | undefined;
486
+ signal?: AbortSignal | undefined;
487
+ }): Promise<RoomBookmarksResult>;
488
+ /**
489
+ * The caller's user-block list (GET /users/blocks), newest first with the
490
+ * same composite-cursor paging as the notification inbox. Blocks are
491
+ * directional and the blocker is always the verified caller.
492
+ */
493
+ export declare function getUserBlocks(config: RoomSocialConfig, options?: {
494
+ limit?: number | undefined;
495
+ cursor?: string | undefined;
496
+ cursorId?: string | undefined;
497
+ signal?: AbortSignal | undefined;
498
+ }): Promise<RoomUserBlocksResult>;
499
+ /** Block a user by userId (PUT, idempotent: re-blocking echoes the standing
500
+ * row with its original createdAt). Server-enforced effects: DM doors shut
501
+ * both directions, friend requests refuse, and the blocked sender produces
502
+ * no notifications for the caller. Room visibility is unchanged. */
503
+ export declare function blockUser(config: RoomSocialConfig, userId: string): Promise<RoomUserBlock>;
504
+ /** Unblock a user (idempotent: unblocking a non-blocked user is a no-op). */
505
+ export declare function unblockUser(config: RoomSocialConfig, userId: string): Promise<{
506
+ success: boolean;
507
+ }>;
508
+ /**
509
+ * Post a poll message. There is no separate create route and no WS input
510
+ * path (the relay's post frame carries text only): a poll rides the normal
511
+ * REST append as the strict body's `poll` block. `options` are option TEXT
512
+ * (2..10 entries; ids are minted server-side) and `closesAt` is an ISO
513
+ * deadline that must sit in the future. `text` is the optional caption;
514
+ * absent is fine, the poll IS the content. Echoes the created store event
515
+ * row with its counts-only summary; the room hears the same message
516
+ * through the ordinary live echo. Additive (0.6.0 servers): older stores
517
+ * reject the unknown block.
518
+ */
519
+ export declare function createRoomPoll(config: RoomSocialConfig, room: string, input: {
520
+ question: string;
521
+ options: string[];
522
+ multi?: boolean | undefined;
523
+ closesAt?: string | undefined;
524
+ text?: string | undefined;
525
+ /** Exact-case workspace id: lets a COLD relay replica materialize a
526
+ * virtual workspace room without guessing the id's casing from the
527
+ * lowercased room name (owners resolve server-side; non-owner first
528
+ * writers need this). */
529
+ workspaceId?: string | undefined;
530
+ /** Route into a topic lane like any other append. */
531
+ topicId?: string | undefined;
532
+ /** Post the poll as a threaded reply. */
533
+ replyToSeq?: number | undefined;
534
+ /** Server-side idempotency: retries after an ambiguous failure reuse
535
+ * the key instead of minting a duplicate poll. */
536
+ clientMsgId?: string | undefined;
537
+ }): Promise<RoomRestEvent>;
538
+ /**
539
+ * Vote on a poll (the poll is keyed by its carrying message's seq). Echoes
540
+ * the store's public event row (RoomRestEvent, not the relay's mapped
541
+ * ledger entry) with the refreshed counts-only summary; the room hears the
542
+ * same update over ENTRY_UPDATED. Single-choice polls replace the caller's
543
+ * previous ballot; multi-choice polls union into it.
544
+ */
545
+ export declare function voteRoomPoll(config: RoomSocialConfig, room: string, seq: number, optionIds: string[]): Promise<RoomRestEvent>;
546
+ /** Clear the caller's vote. Echoes the refreshed store event row. */
547
+ export declare function unvoteRoomPoll(config: RoomSocialConfig, room: string, seq: number): Promise<RoomRestEvent>;
548
+ /** Close a poll (idempotent on an already-closed poll). Echoes the store
549
+ * event row with `poll.closed` set. */
550
+ export declare function closeRoomPoll(config: RoomSocialConfig, room: string, seq: number): Promise<RoomRestEvent>;
551
+ /** Edit a poll's question/options (author-only, and locked once any vote
552
+ * exists or the poll closes; at least one field required). Options are
553
+ * full replacement TEXT, like creation. */
554
+ /** Purge an event's frozen unfurl cards without editing or deleting the
555
+ * message. Author or a delete-any moderation power; the store unsets the
556
+ * cards, schedules thumbnail cleanup, and returns the mutated event for
557
+ * ENTRY_UPDATED fan-out. An edit that removes a card's URL from the text
558
+ * drops that card automatically; this route is the explicit purge for
559
+ * everything else (a capability URL that must go without rewording the
560
+ * message, a moderator acting on someone else's post). */
561
+ export declare function unsetRoomEventUnfurls(config: RoomSocialConfig, room: string, seq: number): Promise<void>;
562
+ /** Edit an event's text over REST: the cold-replica-safe mirror of the WS
563
+ * EDIT frame, and the ONE shape the WS path cannot express: an empty text
564
+ * on a poll-bearing message legally clears the optional caption (the poll
565
+ * stays the content; the store rejects empty edits on plain messages).
566
+ * Viewers reconcile through the store's ENTRY_UPDATED fan-out exactly
567
+ * like the REST poll verbs. */
568
+ export declare function editRoomEventRest(config: RoomSocialConfig, room: string, seq: number, text: string): Promise<void>;
569
+ export declare function editRoomPoll(config: RoomSocialConfig, room: string, seq: number, patch: {
570
+ question?: string | undefined;
571
+ options?: string[] | undefined;
572
+ }): Promise<RoomRestEvent>;
573
+ /**
574
+ * Voter identities, on demand (the entry's summary carries counts only).
575
+ * Grouped per option; `optionId` narrows to one option and `limit` caps the
576
+ * identities per option (counts stay exact).
577
+ */
578
+ export declare function listRoomPollVoters(config: RoomSocialConfig, room: string, seq: number, options?: {
579
+ optionId?: string | undefined;
580
+ limit?: number | undefined;
581
+ signal?: AbortSignal | undefined;
582
+ }): Promise<RoomPollVotersResult>;
389
583
  export declare function suggestRooms(config: RoomSocialConfig, request: RoomSuggestionRequest, options?: {
390
584
  signal?: AbortSignal;
391
585
  }): Promise<RoomSuggestion[]>;
@@ -395,6 +589,9 @@ export declare function socialRequest<T>(config: RoomSocialConfig, path: string,
395
589
  signal?: AbortSignal;
396
590
  formData?: FormData;
397
591
  headers?: Record<string, string>;
592
+ /** Fetch cache mode; token-bearing reads pass "no-store" so posting
593
+ * credentials never persist in a browser HTTP cache. */
594
+ cache?: RequestCache;
398
595
  }): Promise<T>;
399
596
  export declare function cleanUsername(username: string): string;
400
597
  export declare function cleanRoomId(room: string): string;