@titan-design/matrix-bus 0.0.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.
- package/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +414 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Jewkes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @titan-design/matrix-bus
|
|
2
|
+
|
|
3
|
+
The Matrix mechanics behind the human queue: a client-server API client over global
|
|
4
|
+
`fetch`, the `io.titan.item` codec, the owner-only resolution fold, the `#queue` power
|
|
5
|
+
levels and bootstrap, and the per-machine appservice registration renderer.
|
|
6
|
+
|
|
7
|
+
Tier 1 of the titan-platform DAG. No dependencies. The root entry has no `node:` import,
|
|
8
|
+
so it loads in Node 20+, Workers and Deno alike.
|
|
9
|
+
|
|
10
|
+
Reference: [site/reference/matrix-bus.md](../../site/reference/matrix-bus.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
declare class MatrixError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly errcode: string | undefined;
|
|
5
|
+
readonly body: unknown;
|
|
6
|
+
constructor(status: number, errcode: string | undefined, body: unknown, message: string);
|
|
7
|
+
}
|
|
8
|
+
interface RequestOptions {
|
|
9
|
+
token?: string;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
query?: Record<string, string | undefined>;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MatrixEvent {
|
|
16
|
+
type: string;
|
|
17
|
+
event_id: string;
|
|
18
|
+
sender: string;
|
|
19
|
+
content: Record<string, unknown>;
|
|
20
|
+
origin_server_ts?: number;
|
|
21
|
+
state_key?: string;
|
|
22
|
+
room_id?: string;
|
|
23
|
+
unsigned?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
interface MessagesPage {
|
|
26
|
+
chunk: MatrixEvent[];
|
|
27
|
+
start: string;
|
|
28
|
+
end?: string;
|
|
29
|
+
state?: MatrixEvent[];
|
|
30
|
+
}
|
|
31
|
+
interface SyncBatch {
|
|
32
|
+
since: string;
|
|
33
|
+
events: MatrixEvent[];
|
|
34
|
+
}
|
|
35
|
+
interface Session {
|
|
36
|
+
userId: string;
|
|
37
|
+
accessToken: string;
|
|
38
|
+
deviceId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SyncOptions {
|
|
42
|
+
since?: string;
|
|
43
|
+
filter?: string | Record<string, unknown>;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AppserviceClientOptions {
|
|
49
|
+
baseUrl: string;
|
|
50
|
+
asToken: string;
|
|
51
|
+
/** The user to act as; masquerades with ?user_id= unless it equals `sender`. */
|
|
52
|
+
userId?: string;
|
|
53
|
+
/** The user the token itself belongs to, when known (the appservice sender or a password session). */
|
|
54
|
+
sender?: string;
|
|
55
|
+
fetch?: FetchLike;
|
|
56
|
+
}
|
|
57
|
+
interface MessagesOptions {
|
|
58
|
+
from?: string;
|
|
59
|
+
dir?: "b" | "f";
|
|
60
|
+
limit?: number;
|
|
61
|
+
}
|
|
62
|
+
declare class AppserviceClient {
|
|
63
|
+
private readonly options;
|
|
64
|
+
readonly userId: string | undefined;
|
|
65
|
+
private readonly txnPrefix;
|
|
66
|
+
private txnCounter;
|
|
67
|
+
constructor(options: AppserviceClientOptions);
|
|
68
|
+
private get masqueradeAs();
|
|
69
|
+
request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
70
|
+
private nextTxnId;
|
|
71
|
+
send(roomId: string, type: string, content: Record<string, unknown>, txnId?: string): Promise<{
|
|
72
|
+
event_id: string;
|
|
73
|
+
}>;
|
|
74
|
+
sendState(roomId: string, type: string, stateKey: string, content: Record<string, unknown>): Promise<{
|
|
75
|
+
event_id: string;
|
|
76
|
+
}>;
|
|
77
|
+
state(roomId: string): Promise<MatrixEvent[]>;
|
|
78
|
+
messages(roomId: string, { from, dir, limit }?: MessagesOptions): Promise<MessagesPage>;
|
|
79
|
+
whoami(): Promise<{
|
|
80
|
+
user_id: string;
|
|
81
|
+
device_id?: string;
|
|
82
|
+
}>;
|
|
83
|
+
joinRoom(roomIdOrAlias: string): Promise<{
|
|
84
|
+
room_id: string;
|
|
85
|
+
}>;
|
|
86
|
+
/** Registers a namespace user; an existing user is not an error. */
|
|
87
|
+
register(localpart: string): Promise<void>;
|
|
88
|
+
loginAs(localpart: string): Promise<Session>;
|
|
89
|
+
/** Yields one batch per /sync response; persist `since` from each batch before handling its events. */
|
|
90
|
+
syncLoop(options?: SyncOptions): AsyncGenerator<SyncBatch>;
|
|
91
|
+
}
|
|
92
|
+
/** Logs in an ordinary account; the password is used once and not kept. */
|
|
93
|
+
declare function loginPassword(baseUrl: string, user: string, password: string, fetch?: FetchLike): Promise<AppserviceClient>;
|
|
94
|
+
|
|
95
|
+
declare const ITEM_KEY = "io.titan.item";
|
|
96
|
+
declare const ITEM_VERSION = 1;
|
|
97
|
+
declare const ITEM_KINDS: readonly ["approval_request", "endorse_request", "question", "notice", "message"];
|
|
98
|
+
type ItemKind = (typeof ITEM_KINDS)[number];
|
|
99
|
+
/** The structured record carried inside an item's m.room.message content. */
|
|
100
|
+
interface TitanItem {
|
|
101
|
+
v: 1;
|
|
102
|
+
kind: ItemKind;
|
|
103
|
+
machine: string;
|
|
104
|
+
session: string;
|
|
105
|
+
agent_id?: string;
|
|
106
|
+
msg_id: string;
|
|
107
|
+
at: number;
|
|
108
|
+
tool_name?: string;
|
|
109
|
+
input_preview?: string;
|
|
110
|
+
recipient?: string;
|
|
111
|
+
truncated: boolean;
|
|
112
|
+
redacted: boolean;
|
|
113
|
+
}
|
|
114
|
+
/** An item plus the free text it shows; the text travels in `body` only. */
|
|
115
|
+
interface ItemInput extends Omit<TitanItem, "v"> {
|
|
116
|
+
text?: string;
|
|
117
|
+
}
|
|
118
|
+
type ItemContent = {
|
|
119
|
+
msgtype: "m.text";
|
|
120
|
+
body: string;
|
|
121
|
+
format?: "org.matrix.custom.html";
|
|
122
|
+
formatted_body?: string;
|
|
123
|
+
[ITEM_KEY]: TitanItem;
|
|
124
|
+
};
|
|
125
|
+
declare const TRUNCATED_LINE = "too large to approve from the phone; answer at the terminal";
|
|
126
|
+
declare const REDACTED_LINE = "secrets redacted; answer at the terminal";
|
|
127
|
+
declare function renderItemBody(item: ItemInput): string;
|
|
128
|
+
declare function encodeItem(item: ItemInput, formattedBody?: string): ItemContent;
|
|
129
|
+
/** Returns null for content without the key, an unknown version, or a malformed record. */
|
|
130
|
+
declare function decodeItem(content: Record<string, unknown>): TitanItem | null;
|
|
131
|
+
|
|
132
|
+
declare const RESOLUTION_EVENT = "io.titan.resolution";
|
|
133
|
+
type Verdict = "allow" | "deny" | "approve" | "dismiss" | "answer";
|
|
134
|
+
interface Resolution {
|
|
135
|
+
itemEventId: string;
|
|
136
|
+
verdict: Verdict;
|
|
137
|
+
text?: string;
|
|
138
|
+
}
|
|
139
|
+
interface FoldContext {
|
|
140
|
+
ownerUserId: string;
|
|
141
|
+
/** Open items the caller posted, keyed by event id; the kind picks the verdict vocabulary. */
|
|
142
|
+
itemEventIds: ReadonlyMap<string, ItemKind>;
|
|
143
|
+
}
|
|
144
|
+
declare function stripReplyFallback(body: string): string;
|
|
145
|
+
/** Folds an owner's reaction, reply or io.titan.resolution into a verdict on an open item; anything else is null. */
|
|
146
|
+
declare function foldResolution(event: MatrixEvent, { ownerUserId, itemEventIds }: FoldContext): Resolution | null;
|
|
147
|
+
|
|
148
|
+
declare const MAX_CONTENT_BYTES = 60000;
|
|
149
|
+
declare class ContentTooLargeError extends Error {
|
|
150
|
+
readonly bytes: number;
|
|
151
|
+
constructor(bytes: number);
|
|
152
|
+
}
|
|
153
|
+
declare function contentBytes(content: unknown): number;
|
|
154
|
+
declare function assertSendable(content: unknown): void;
|
|
155
|
+
|
|
156
|
+
interface PowerLevels {
|
|
157
|
+
users: Record<string, number>;
|
|
158
|
+
users_default: number;
|
|
159
|
+
events_default: number;
|
|
160
|
+
state_default: number;
|
|
161
|
+
invite: number;
|
|
162
|
+
kick: number;
|
|
163
|
+
ban: number;
|
|
164
|
+
redact: number;
|
|
165
|
+
events: Record<string, number>;
|
|
166
|
+
}
|
|
167
|
+
/** #queue levels: members may post items, only the creator (the owner) may react or resolve. */
|
|
168
|
+
declare function queuePowerLevels(): PowerLevels;
|
|
169
|
+
interface BootstrapQueueOptions {
|
|
170
|
+
/** Full alias, e.g. `#queue:chat.example.org`. */
|
|
171
|
+
alias: string;
|
|
172
|
+
mirrorUserIds: string[];
|
|
173
|
+
name?: string;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Creates the owner's #queue at the server's default room version (v12), so the owner is creator.
|
|
177
|
+
* An alias that already resolves returns its room id unchanged.
|
|
178
|
+
*/
|
|
179
|
+
declare function bootstrapQueueRoom(owner: AppserviceClient, options: BootstrapQueueOptions): Promise<string>;
|
|
180
|
+
|
|
181
|
+
interface RegistrationOptions {
|
|
182
|
+
id: string;
|
|
183
|
+
asToken: string;
|
|
184
|
+
hsToken: string;
|
|
185
|
+
/** Where the homeserver pushes transactions; null for a receive-only edge that pulls with /sync. */
|
|
186
|
+
url: string | null;
|
|
187
|
+
senderLocalpart: string;
|
|
188
|
+
/** Machine name inside the namespace: `edge1` owns `@ac-edge1-.*`. */
|
|
189
|
+
machine: string;
|
|
190
|
+
serverName: string;
|
|
191
|
+
}
|
|
192
|
+
declare function escapeRegex(text: string): string;
|
|
193
|
+
declare function machineUserRegex(machine: string, serverName: string): string;
|
|
194
|
+
declare function renderRegistration(options: RegistrationOptions): string;
|
|
195
|
+
|
|
196
|
+
export { AppserviceClient, type AppserviceClientOptions, type BootstrapQueueOptions, ContentTooLargeError, type FetchLike, type FoldContext, ITEM_KEY, ITEM_KINDS, ITEM_VERSION, type ItemContent, type ItemInput, type ItemKind, MAX_CONTENT_BYTES, MatrixError, type MatrixEvent, type MessagesOptions, type MessagesPage, type PowerLevels, REDACTED_LINE, RESOLUTION_EVENT, type RegistrationOptions, type Resolution, type Session, type SyncBatch, type SyncOptions, TRUNCATED_LINE, type TitanItem, type Verdict, assertSendable, bootstrapQueueRoom, contentBytes, decodeItem, encodeItem, escapeRegex, foldResolution, loginPassword, machineUserRegex, queuePowerLevels, renderItemBody, renderRegistration, stripReplyFallback };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
var MatrixError = class extends Error {
|
|
3
|
+
constructor(status, errcode, body, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.errcode = errcode;
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.name = "MatrixError";
|
|
9
|
+
}
|
|
10
|
+
status;
|
|
11
|
+
errcode;
|
|
12
|
+
body;
|
|
13
|
+
};
|
|
14
|
+
function buildUrl(baseUrl, path, query = {}) {
|
|
15
|
+
const url = new URL(path, baseUrl);
|
|
16
|
+
for (const [key, value] of Object.entries(query)) {
|
|
17
|
+
if (value !== void 0) url.searchParams.set(key, value);
|
|
18
|
+
}
|
|
19
|
+
return url.toString();
|
|
20
|
+
}
|
|
21
|
+
function parseBody(text) {
|
|
22
|
+
if (!text) return null;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(text);
|
|
25
|
+
} catch {
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function errcodeOf(body) {
|
|
30
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
31
|
+
const code = body.errcode;
|
|
32
|
+
return typeof code === "string" ? code : void 0;
|
|
33
|
+
}
|
|
34
|
+
async function matrixRequest(transport, method, path, options = {}) {
|
|
35
|
+
const doFetch = transport.fetch ?? globalThis.fetch;
|
|
36
|
+
const headers = { "content-type": "application/json" };
|
|
37
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
38
|
+
const res = await doFetch(buildUrl(transport.baseUrl, path, options.query), {
|
|
39
|
+
method,
|
|
40
|
+
headers,
|
|
41
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
42
|
+
signal: options.signal
|
|
43
|
+
});
|
|
44
|
+
const body = parseBody(await res.text());
|
|
45
|
+
if (res.status >= 400) {
|
|
46
|
+
const errcode = errcodeOf(body);
|
|
47
|
+
throw new MatrixError(res.status, errcode, body, `${method} ${path} -> ${res.status} ${errcode ?? ""}`.trim());
|
|
48
|
+
}
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/size.ts
|
|
53
|
+
var MAX_CONTENT_BYTES = 6e4;
|
|
54
|
+
var ContentTooLargeError = class extends Error {
|
|
55
|
+
constructor(bytes) {
|
|
56
|
+
super(`event content is ${bytes} bytes, over the ${MAX_CONTENT_BYTES}-byte limit`);
|
|
57
|
+
this.bytes = bytes;
|
|
58
|
+
this.name = "ContentTooLargeError";
|
|
59
|
+
}
|
|
60
|
+
bytes;
|
|
61
|
+
};
|
|
62
|
+
function contentBytes(content) {
|
|
63
|
+
return new TextEncoder().encode(JSON.stringify(content)).byteLength;
|
|
64
|
+
}
|
|
65
|
+
function assertSendable(content) {
|
|
66
|
+
const bytes = contentBytes(content);
|
|
67
|
+
if (bytes > MAX_CONTENT_BYTES) throw new ContentTooLargeError(bytes);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/sync.ts
|
|
71
|
+
function timelineEvents(res) {
|
|
72
|
+
return Object.entries(res.rooms?.join ?? {}).flatMap(
|
|
73
|
+
([roomId, room2]) => (room2.timeline?.events ?? []).map((event) => ({ ...event, room_id: roomId }))
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function syncQuery(since, options) {
|
|
77
|
+
const { filter, timeoutMs = 3e4 } = options;
|
|
78
|
+
return {
|
|
79
|
+
since,
|
|
80
|
+
timeout: String(timeoutMs),
|
|
81
|
+
filter: typeof filter === "object" ? JSON.stringify(filter) : filter
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function* syncBatches(request, options = {}) {
|
|
85
|
+
const { signal } = options;
|
|
86
|
+
let since = options.since;
|
|
87
|
+
while (!signal?.aborted) {
|
|
88
|
+
let res;
|
|
89
|
+
try {
|
|
90
|
+
res = await request(syncQuery(since, options), signal);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (signal?.aborted) return;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
since = res.next_batch;
|
|
96
|
+
yield { since, events: timelineEvents(res) };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/client.ts
|
|
101
|
+
var CLIENT = "/_matrix/client/v3";
|
|
102
|
+
var room = (roomId) => `${CLIENT}/rooms/${encodeURIComponent(roomId)}`;
|
|
103
|
+
function newTxnPrefix() {
|
|
104
|
+
return globalThis.crypto.randomUUID();
|
|
105
|
+
}
|
|
106
|
+
var AppserviceClient = class {
|
|
107
|
+
constructor(options) {
|
|
108
|
+
this.options = options;
|
|
109
|
+
this.userId = options.userId ?? options.sender;
|
|
110
|
+
}
|
|
111
|
+
options;
|
|
112
|
+
userId;
|
|
113
|
+
txnPrefix = newTxnPrefix();
|
|
114
|
+
txnCounter = 0;
|
|
115
|
+
get masqueradeAs() {
|
|
116
|
+
const { userId, sender } = this.options;
|
|
117
|
+
return userId && userId !== sender ? userId : void 0;
|
|
118
|
+
}
|
|
119
|
+
request(method, path, options = {}) {
|
|
120
|
+
const { baseUrl, asToken, fetch } = this.options;
|
|
121
|
+
const query = { ...options.query, user_id: this.masqueradeAs };
|
|
122
|
+
return matrixRequest({ baseUrl, fetch }, method, path, { ...options, token: asToken, query });
|
|
123
|
+
}
|
|
124
|
+
nextTxnId() {
|
|
125
|
+
this.txnCounter += 1;
|
|
126
|
+
return `${this.txnPrefix}-${this.txnCounter}`;
|
|
127
|
+
}
|
|
128
|
+
async send(roomId, type, content, txnId = this.nextTxnId()) {
|
|
129
|
+
assertSendable(content);
|
|
130
|
+
const path = `${room(roomId)}/send/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`;
|
|
131
|
+
return this.request("PUT", path, { body: content });
|
|
132
|
+
}
|
|
133
|
+
async sendState(roomId, type, stateKey, content) {
|
|
134
|
+
assertSendable(content);
|
|
135
|
+
const path = `${room(roomId)}/state/${encodeURIComponent(type)}/${encodeURIComponent(stateKey)}`;
|
|
136
|
+
return this.request("PUT", path, { body: content });
|
|
137
|
+
}
|
|
138
|
+
state(roomId) {
|
|
139
|
+
return this.request("GET", `${room(roomId)}/state`);
|
|
140
|
+
}
|
|
141
|
+
messages(roomId, { from, dir = "b", limit } = {}) {
|
|
142
|
+
const query = { from, dir, limit: limit === void 0 ? void 0 : String(limit) };
|
|
143
|
+
return this.request("GET", `${room(roomId)}/messages`, { query });
|
|
144
|
+
}
|
|
145
|
+
whoami() {
|
|
146
|
+
return this.request("GET", `${CLIENT}/account/whoami`);
|
|
147
|
+
}
|
|
148
|
+
joinRoom(roomIdOrAlias) {
|
|
149
|
+
return this.request("POST", `${CLIENT}/join/${encodeURIComponent(roomIdOrAlias)}`, { body: {} });
|
|
150
|
+
}
|
|
151
|
+
/** Registers a namespace user; an existing user is not an error. */
|
|
152
|
+
async register(localpart) {
|
|
153
|
+
const body = { type: "m.login.application_service", username: localpart, inhibit_login: true };
|
|
154
|
+
try {
|
|
155
|
+
await this.request("POST", `${CLIENT}/register`, { body });
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (!(err instanceof MatrixError && err.errcode === "M_USER_IN_USE")) throw err;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async loginAs(localpart) {
|
|
161
|
+
const body = { type: "m.login.application_service", identifier: { type: "m.id.user", user: localpart } };
|
|
162
|
+
const res = await this.request("POST", `${CLIENT}/login`, { body });
|
|
163
|
+
return toSession(res);
|
|
164
|
+
}
|
|
165
|
+
/** Yields one batch per /sync response; persist `since` from each batch before handling its events. */
|
|
166
|
+
syncLoop(options = {}) {
|
|
167
|
+
const request = (query, signal) => this.request("GET", `${CLIENT}/sync`, { query, signal });
|
|
168
|
+
return syncBatches(request, options);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function toSession(res) {
|
|
172
|
+
return { userId: res.user_id, accessToken: res.access_token, deviceId: res.device_id };
|
|
173
|
+
}
|
|
174
|
+
async function loginPassword(baseUrl, user, password, fetch) {
|
|
175
|
+
const body = { type: "m.login.password", identifier: { type: "m.id.user", user }, password };
|
|
176
|
+
const res = await matrixRequest({ baseUrl, fetch }, "POST", `${CLIENT}/login`, { body });
|
|
177
|
+
return new AppserviceClient({ baseUrl, asToken: res.access_token, sender: res.user_id, fetch });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/item.ts
|
|
181
|
+
var ITEM_KEY = "io.titan.item";
|
|
182
|
+
var ITEM_VERSION = 1;
|
|
183
|
+
var ITEM_KINDS = ["approval_request", "endorse_request", "question", "notice", "message"];
|
|
184
|
+
var LABELS = {
|
|
185
|
+
approval_request: "APPR",
|
|
186
|
+
endorse_request: "ENDORSE",
|
|
187
|
+
question: "QUESTION",
|
|
188
|
+
notice: "NOTICE",
|
|
189
|
+
message: "MSG"
|
|
190
|
+
};
|
|
191
|
+
var PROMPTS = {
|
|
192
|
+
approval_request: "react \u2705 allow, \u274C deny",
|
|
193
|
+
endorse_request: "react \u2705 approve, \u274C dismiss",
|
|
194
|
+
question: "reply to answer, or reply dismiss",
|
|
195
|
+
notice: "react \u2705 or \u274C to dismiss",
|
|
196
|
+
message: "react \u2705 or \u274C to dismiss"
|
|
197
|
+
};
|
|
198
|
+
var TRUNCATED_LINE = "too large to approve from the phone; answer at the terminal";
|
|
199
|
+
var REDACTED_LINE = "secrets redacted; answer at the terminal";
|
|
200
|
+
function headline(item) {
|
|
201
|
+
const to = item.recipient ? ` to ${item.recipient}` : "";
|
|
202
|
+
return `${LABELS[item.kind]} from ${item.session} (${item.machine})${to}`;
|
|
203
|
+
}
|
|
204
|
+
function detail(item) {
|
|
205
|
+
if (item.kind === "approval_request") return `${item.tool_name ?? "tool"}: ${item.input_preview ?? ""}`;
|
|
206
|
+
return item.text ?? "";
|
|
207
|
+
}
|
|
208
|
+
function footer(item) {
|
|
209
|
+
if (item.truncated) return TRUNCATED_LINE;
|
|
210
|
+
if (item.redacted) return REDACTED_LINE;
|
|
211
|
+
return PROMPTS[item.kind];
|
|
212
|
+
}
|
|
213
|
+
function renderItemBody(item) {
|
|
214
|
+
return `${headline(item)}
|
|
215
|
+
${detail(item)}
|
|
216
|
+
|
|
217
|
+
${footer(item)}`;
|
|
218
|
+
}
|
|
219
|
+
function recordOf(item) {
|
|
220
|
+
const fields = Object.entries(item).filter(([key, value]) => key !== "text" && value !== void 0);
|
|
221
|
+
return { v: ITEM_VERSION, ...Object.fromEntries(fields) };
|
|
222
|
+
}
|
|
223
|
+
function encodeItem(item, formattedBody) {
|
|
224
|
+
const content = { msgtype: "m.text", body: renderItemBody(item), [ITEM_KEY]: recordOf(item) };
|
|
225
|
+
if (formattedBody === void 0) return content;
|
|
226
|
+
return { ...content, format: "org.matrix.custom.html", formatted_body: formattedBody };
|
|
227
|
+
}
|
|
228
|
+
var REQUIRED_STRINGS = ["machine", "session", "msg_id"];
|
|
229
|
+
var OPTIONAL_STRINGS = ["agent_id", "tool_name", "input_preview", "recipient"];
|
|
230
|
+
function isItem(value) {
|
|
231
|
+
if (value.v !== ITEM_VERSION || !ITEM_KINDS.includes(value.kind)) return false;
|
|
232
|
+
if (!REQUIRED_STRINGS.every((key) => typeof value[key] === "string")) return false;
|
|
233
|
+
if (!OPTIONAL_STRINGS.every((key) => value[key] === void 0 || typeof value[key] === "string")) return false;
|
|
234
|
+
return typeof value.at === "number" && typeof value.truncated === "boolean" && typeof value.redacted === "boolean";
|
|
235
|
+
}
|
|
236
|
+
function decodeItem(content) {
|
|
237
|
+
const value = content[ITEM_KEY];
|
|
238
|
+
if (typeof value !== "object" || value === null) return null;
|
|
239
|
+
const record = value;
|
|
240
|
+
if (!isItem(record)) return null;
|
|
241
|
+
const pick = (keys) => keys.filter((key) => record[key] !== void 0).map((key) => [key, record[key]]);
|
|
242
|
+
const base = ["v", "kind", ...REQUIRED_STRINGS, "at", "truncated", "redacted"];
|
|
243
|
+
return Object.fromEntries([...pick(base), ...pick(OPTIONAL_STRINGS)]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/fold.ts
|
|
247
|
+
var RESOLUTION_EVENT = "io.titan.resolution";
|
|
248
|
+
var REACTION_FAMILY = { "\u2705": "yes", "\u{1F44D}": "yes", "\u274C": "no", "\u{1F44E}": "no" };
|
|
249
|
+
var WORD_FAMILY = { allow: "yes", approve: "yes", deny: "no", dismiss: "no" };
|
|
250
|
+
var VERDICTS = {
|
|
251
|
+
approval_request: { yes: "allow", no: "deny" },
|
|
252
|
+
endorse_request: { yes: "approve", no: "dismiss" },
|
|
253
|
+
question: { no: "dismiss" },
|
|
254
|
+
notice: { yes: "dismiss", no: "dismiss" },
|
|
255
|
+
message: { yes: "dismiss", no: "dismiss" }
|
|
256
|
+
};
|
|
257
|
+
var EMOJI_MODIFIERS = /\uFE0E|\uFE0F|\p{Emoji_Modifier}/gu;
|
|
258
|
+
function relatesTo(content) {
|
|
259
|
+
const rel = content["m.relates_to"];
|
|
260
|
+
return typeof rel === "object" && rel !== null ? rel : {};
|
|
261
|
+
}
|
|
262
|
+
function stringField(value, key) {
|
|
263
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
264
|
+
const field = value[key];
|
|
265
|
+
return typeof field === "string" ? field : void 0;
|
|
266
|
+
}
|
|
267
|
+
function foldReaction(content, kind, itemEventId) {
|
|
268
|
+
const key = stringField(relatesTo(content), "key")?.replace(EMOJI_MODIFIERS, "");
|
|
269
|
+
const family = key === void 0 ? void 0 : REACTION_FAMILY[key];
|
|
270
|
+
const verdict = family && VERDICTS[kind][family];
|
|
271
|
+
return verdict ? { itemEventId, verdict } : null;
|
|
272
|
+
}
|
|
273
|
+
function stripReplyFallback(body) {
|
|
274
|
+
const lines = body.split("\n");
|
|
275
|
+
let start = 0;
|
|
276
|
+
while (start < lines.length && lines[start]?.startsWith(">")) start += 1;
|
|
277
|
+
if (start > 0 && lines[start]?.trim() === "") start += 1;
|
|
278
|
+
return lines.slice(start).join("\n").trim();
|
|
279
|
+
}
|
|
280
|
+
function lastLine(text) {
|
|
281
|
+
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
282
|
+
return (lines.at(-1) ?? "").toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
function foldWords(text, kind, itemEventId) {
|
|
285
|
+
const family = WORD_FAMILY[lastLine(text)];
|
|
286
|
+
const verdict = family && VERDICTS[kind][family];
|
|
287
|
+
if (verdict) return { itemEventId, verdict };
|
|
288
|
+
if (kind === "question" && text) return { itemEventId, verdict: "answer", text };
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
function foldReply(content, kind, itemEventId) {
|
|
292
|
+
const body = stringField(content, "body");
|
|
293
|
+
return body === void 0 ? null : foldWords(stripReplyFallback(body), kind, itemEventId);
|
|
294
|
+
}
|
|
295
|
+
function foldDecision(content, kind, itemEventId) {
|
|
296
|
+
const decision = stringField(content, "decision");
|
|
297
|
+
if (decision === "answer") {
|
|
298
|
+
const text = stringField(content, "text")?.trim();
|
|
299
|
+
return kind === "question" && text ? { itemEventId, verdict: "answer", text } : null;
|
|
300
|
+
}
|
|
301
|
+
const family = decision === void 0 ? void 0 : WORD_FAMILY[decision];
|
|
302
|
+
const verdict = family && VERDICTS[kind][family];
|
|
303
|
+
return verdict ? { itemEventId, verdict } : null;
|
|
304
|
+
}
|
|
305
|
+
function targetOf(event) {
|
|
306
|
+
const rel = relatesTo(event.content);
|
|
307
|
+
if (event.type === "m.reaction") return rel.rel_type === "m.annotation" ? stringField(rel, "event_id") : void 0;
|
|
308
|
+
if (event.type === "m.room.message") return stringField(rel["m.in_reply_to"], "event_id");
|
|
309
|
+
if (event.type === RESOLUTION_EVENT) return stringField(rel, "event_id");
|
|
310
|
+
return void 0;
|
|
311
|
+
}
|
|
312
|
+
function foldResolution(event, { ownerUserId, itemEventIds }) {
|
|
313
|
+
if (event.sender !== ownerUserId || event.state_key !== void 0) return null;
|
|
314
|
+
const itemEventId = targetOf(event);
|
|
315
|
+
const kind = itemEventId === void 0 ? void 0 : itemEventIds.get(itemEventId);
|
|
316
|
+
if (itemEventId === void 0 || kind === void 0) return null;
|
|
317
|
+
if (event.type === "m.reaction") return foldReaction(event.content, kind, itemEventId);
|
|
318
|
+
if (event.type === "m.room.message") return foldReply(event.content, kind, itemEventId);
|
|
319
|
+
return foldDecision(event.content, kind, itemEventId);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/room.ts
|
|
323
|
+
function queuePowerLevels() {
|
|
324
|
+
return {
|
|
325
|
+
users: {},
|
|
326
|
+
users_default: 0,
|
|
327
|
+
events_default: 0,
|
|
328
|
+
state_default: 100,
|
|
329
|
+
invite: 100,
|
|
330
|
+
kick: 100,
|
|
331
|
+
ban: 100,
|
|
332
|
+
redact: 100,
|
|
333
|
+
events: { "m.reaction": 100, "io.titan.resolution": 100, "m.room.power_levels": 100 }
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
var directoryPath = (alias) => `/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`;
|
|
337
|
+
async function resolveAlias(client, alias) {
|
|
338
|
+
try {
|
|
339
|
+
return (await client.request("GET", directoryPath(alias))).room_id;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (err instanceof MatrixError && err.status === 404) return void 0;
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function bootstrapQueueRoom(owner, options) {
|
|
346
|
+
const existing = await resolveAlias(owner, options.alias);
|
|
347
|
+
if (existing) return existing;
|
|
348
|
+
const { room_id: roomId } = await owner.request("POST", "/_matrix/client/v3/createRoom", {
|
|
349
|
+
body: {
|
|
350
|
+
preset: "private_chat",
|
|
351
|
+
name: options.name ?? "queue",
|
|
352
|
+
invite: options.mirrorUserIds,
|
|
353
|
+
power_level_content_override: queuePowerLevels()
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
await owner.request("PUT", directoryPath(options.alias), { body: { room_id: roomId } });
|
|
357
|
+
return roomId;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/registration.ts
|
|
361
|
+
var MACHINE = /^[a-z0-9]+$/;
|
|
362
|
+
var LOCALPART = /^[a-z0-9._=/-]+$/;
|
|
363
|
+
function escapeRegex(text) {
|
|
364
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
365
|
+
}
|
|
366
|
+
function machineUserRegex(machine, serverName) {
|
|
367
|
+
if (!MACHINE.test(machine)) throw new Error(`machine must match ${MACHINE}: ${JSON.stringify(machine)}`);
|
|
368
|
+
return `^@ac-${machine}-.*:${escapeRegex(serverName)}$`;
|
|
369
|
+
}
|
|
370
|
+
var quote = (value) => JSON.stringify(value);
|
|
371
|
+
function renderRegistration(options) {
|
|
372
|
+
if (!LOCALPART.test(options.senderLocalpart)) throw new Error(`invalid sender localpart: ${JSON.stringify(options.senderLocalpart)}`);
|
|
373
|
+
return [
|
|
374
|
+
`id: ${quote(options.id)}`,
|
|
375
|
+
`url: ${options.url === null ? "null" : quote(options.url)}`,
|
|
376
|
+
`as_token: ${quote(options.asToken)}`,
|
|
377
|
+
`hs_token: ${quote(options.hsToken)}`,
|
|
378
|
+
`sender_localpart: ${options.senderLocalpart}`,
|
|
379
|
+
"rate_limited: false",
|
|
380
|
+
"namespaces:",
|
|
381
|
+
" users:",
|
|
382
|
+
" - exclusive: true",
|
|
383
|
+
` regex: ${quote(machineUserRegex(options.machine, options.serverName))}`,
|
|
384
|
+
" aliases: []",
|
|
385
|
+
" rooms: []",
|
|
386
|
+
""
|
|
387
|
+
].join("\n");
|
|
388
|
+
}
|
|
389
|
+
export {
|
|
390
|
+
AppserviceClient,
|
|
391
|
+
ContentTooLargeError,
|
|
392
|
+
ITEM_KEY,
|
|
393
|
+
ITEM_KINDS,
|
|
394
|
+
ITEM_VERSION,
|
|
395
|
+
MAX_CONTENT_BYTES,
|
|
396
|
+
MatrixError,
|
|
397
|
+
REDACTED_LINE,
|
|
398
|
+
RESOLUTION_EVENT,
|
|
399
|
+
TRUNCATED_LINE,
|
|
400
|
+
assertSendable,
|
|
401
|
+
bootstrapQueueRoom,
|
|
402
|
+
contentBytes,
|
|
403
|
+
decodeItem,
|
|
404
|
+
encodeItem,
|
|
405
|
+
escapeRegex,
|
|
406
|
+
foldResolution,
|
|
407
|
+
loginPassword,
|
|
408
|
+
machineUserRegex,
|
|
409
|
+
queuePowerLevels,
|
|
410
|
+
renderItemBody,
|
|
411
|
+
renderRegistration,
|
|
412
|
+
stripReplyFallback
|
|
413
|
+
};
|
|
414
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/http.ts","../src/size.ts","../src/sync.ts","../src/client.ts","../src/item.ts","../src/fold.ts","../src/room.ts","../src/registration.ts"],"sourcesContent":["export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\nexport class MatrixError extends Error {\n constructor(\n readonly status: number,\n readonly errcode: string | undefined,\n readonly body: unknown,\n message: string,\n ) {\n super(message);\n this.name = \"MatrixError\";\n }\n}\n\nexport interface RequestOptions {\n token?: string;\n body?: unknown;\n query?: Record<string, string | undefined>;\n signal?: AbortSignal;\n}\n\nexport interface Transport {\n baseUrl: string;\n fetch?: FetchLike;\n}\n\nexport function buildUrl(baseUrl: string, path: string, query: RequestOptions[\"query\"] = {}): string {\n const url = new URL(path, baseUrl);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n return url.toString();\n}\n\nfunction parseBody(text: string): unknown {\n if (!text) return null;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction errcodeOf(body: unknown): string | undefined {\n if (typeof body !== \"object\" || body === null) return undefined;\n const code = (body as { errcode?: unknown }).errcode;\n return typeof code === \"string\" ? code : undefined;\n}\n\nexport async function matrixRequest<T>(transport: Transport, method: string, path: string, options: RequestOptions = {}): Promise<T> {\n const doFetch = transport.fetch ?? globalThis.fetch;\n const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n if (options.token) headers.authorization = `Bearer ${options.token}`;\n const res = await doFetch(buildUrl(transport.baseUrl, path, options.query), {\n method,\n headers,\n body: options.body === undefined ? undefined : JSON.stringify(options.body),\n signal: options.signal,\n });\n const body = parseBody(await res.text());\n if (res.status >= 400) {\n const errcode = errcodeOf(body);\n throw new MatrixError(res.status, errcode, body, `${method} ${path} -> ${res.status} ${errcode ?? \"\"}`.trim());\n }\n return body as T;\n}\n","// The server rejects a PDU over 65,535 bytes; the envelope around content needs the headroom.\nexport const MAX_CONTENT_BYTES = 60_000;\n\nexport class ContentTooLargeError extends Error {\n constructor(readonly bytes: number) {\n super(`event content is ${bytes} bytes, over the ${MAX_CONTENT_BYTES}-byte limit`);\n this.name = \"ContentTooLargeError\";\n }\n}\n\nexport function contentBytes(content: unknown): number {\n return new TextEncoder().encode(JSON.stringify(content)).byteLength;\n}\n\nexport function assertSendable(content: unknown): void {\n const bytes = contentBytes(content);\n if (bytes > MAX_CONTENT_BYTES) throw new ContentTooLargeError(bytes);\n}\n","import type { MatrixEvent, SyncBatch } from \"./types.js\";\n\nexport interface SyncOptions {\n since?: string;\n filter?: string | Record<string, unknown>;\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\nexport interface SyncResponse {\n next_batch: string;\n rooms?: { join?: Record<string, { timeline?: { events?: MatrixEvent[] } }> };\n}\n\nexport type SyncRequest = (query: Record<string, string | undefined>, signal?: AbortSignal) => Promise<SyncResponse>;\n\nexport function timelineEvents(res: SyncResponse): MatrixEvent[] {\n return Object.entries(res.rooms?.join ?? {}).flatMap(([roomId, room]) =>\n (room.timeline?.events ?? []).map((event) => ({ ...event, room_id: roomId })),\n );\n}\n\nfunction syncQuery(since: string | undefined, options: SyncOptions): Record<string, string | undefined> {\n const { filter, timeoutMs = 30_000 } = options;\n return {\n since,\n timeout: String(timeoutMs),\n filter: typeof filter === \"object\" ? JSON.stringify(filter) : filter,\n };\n}\n\n// Ends quietly on abort and rethrows anything else, so the caller owns retry and backoff.\nexport async function* syncBatches(request: SyncRequest, options: SyncOptions = {}): AsyncGenerator<SyncBatch> {\n const { signal } = options;\n let since = options.since;\n while (!signal?.aborted) {\n let res: SyncResponse;\n try {\n res = await request(syncQuery(since, options), signal);\n } catch (err) {\n if (signal?.aborted) return;\n throw err;\n }\n since = res.next_batch;\n yield { since, events: timelineEvents(res) };\n }\n}\n","import { matrixRequest, MatrixError, type FetchLike, type RequestOptions } from \"./http.js\";\nimport { assertSendable } from \"./size.js\";\nimport { syncBatches, type SyncOptions, type SyncResponse } from \"./sync.js\";\nimport type { MatrixEvent, MessagesPage, Session, SyncBatch } from \"./types.js\";\n\nexport interface AppserviceClientOptions {\n baseUrl: string;\n asToken: string;\n /** The user to act as; masquerades with ?user_id= unless it equals `sender`. */\n userId?: string;\n /** The user the token itself belongs to, when known (the appservice sender or a password session). */\n sender?: string;\n fetch?: FetchLike;\n}\n\nexport interface MessagesOptions {\n from?: string;\n dir?: \"b\" | \"f\";\n limit?: number;\n}\n\nconst CLIENT = \"/_matrix/client/v3\";\nconst room = (roomId: string) => `${CLIENT}/rooms/${encodeURIComponent(roomId)}`;\n\nfunction newTxnPrefix(): string {\n return globalThis.crypto.randomUUID();\n}\n\nexport class AppserviceClient {\n readonly userId: string | undefined;\n private readonly txnPrefix = newTxnPrefix();\n private txnCounter = 0;\n\n constructor(private readonly options: AppserviceClientOptions) {\n this.userId = options.userId ?? options.sender;\n }\n\n private get masqueradeAs(): string | undefined {\n const { userId, sender } = this.options;\n return userId && userId !== sender ? userId : undefined;\n }\n\n request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {\n const { baseUrl, asToken, fetch } = this.options;\n const query = { ...options.query, user_id: this.masqueradeAs };\n return matrixRequest<T>({ baseUrl, fetch }, method, path, { ...options, token: asToken, query });\n }\n\n private nextTxnId(): string {\n this.txnCounter += 1;\n return `${this.txnPrefix}-${this.txnCounter}`;\n }\n\n async send(roomId: string, type: string, content: Record<string, unknown>, txnId = this.nextTxnId()): Promise<{ event_id: string }> {\n assertSendable(content);\n const path = `${room(roomId)}/send/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`;\n return this.request(\"PUT\", path, { body: content });\n }\n\n async sendState(roomId: string, type: string, stateKey: string, content: Record<string, unknown>): Promise<{ event_id: string }> {\n assertSendable(content);\n const path = `${room(roomId)}/state/${encodeURIComponent(type)}/${encodeURIComponent(stateKey)}`;\n return this.request(\"PUT\", path, { body: content });\n }\n\n state(roomId: string): Promise<MatrixEvent[]> {\n return this.request(\"GET\", `${room(roomId)}/state`);\n }\n\n messages(roomId: string, { from, dir = \"b\", limit }: MessagesOptions = {}): Promise<MessagesPage> {\n const query = { from, dir, limit: limit === undefined ? undefined : String(limit) };\n return this.request(\"GET\", `${room(roomId)}/messages`, { query });\n }\n\n whoami(): Promise<{ user_id: string; device_id?: string }> {\n return this.request(\"GET\", `${CLIENT}/account/whoami`);\n }\n\n joinRoom(roomIdOrAlias: string): Promise<{ room_id: string }> {\n return this.request(\"POST\", `${CLIENT}/join/${encodeURIComponent(roomIdOrAlias)}`, { body: {} });\n }\n\n /** Registers a namespace user; an existing user is not an error. */\n async register(localpart: string): Promise<void> {\n const body = { type: \"m.login.application_service\", username: localpart, inhibit_login: true };\n try {\n await this.request(\"POST\", `${CLIENT}/register`, { body });\n } catch (err) {\n if (!(err instanceof MatrixError && err.errcode === \"M_USER_IN_USE\")) throw err;\n }\n }\n\n async loginAs(localpart: string): Promise<Session> {\n const body = { type: \"m.login.application_service\", identifier: { type: \"m.id.user\", user: localpart } };\n const res = await this.request<LoginResponse>(\"POST\", `${CLIENT}/login`, { body });\n return toSession(res);\n }\n\n /** Yields one batch per /sync response; persist `since` from each batch before handling its events. */\n syncLoop(options: SyncOptions = {}): AsyncGenerator<SyncBatch> {\n const request = (query: Record<string, string | undefined>, signal?: AbortSignal) =>\n this.request<SyncResponse>(\"GET\", `${CLIENT}/sync`, { query, signal });\n return syncBatches(request, options);\n }\n}\n\ninterface LoginResponse {\n user_id: string;\n access_token: string;\n device_id?: string;\n}\n\nfunction toSession(res: LoginResponse): Session {\n return { userId: res.user_id, accessToken: res.access_token, deviceId: res.device_id };\n}\n\n/** Logs in an ordinary account; the password is used once and not kept. */\nexport async function loginPassword(baseUrl: string, user: string, password: string, fetch?: FetchLike): Promise<AppserviceClient> {\n const body = { type: \"m.login.password\", identifier: { type: \"m.id.user\", user }, password };\n const res = await matrixRequest<LoginResponse>({ baseUrl, fetch }, \"POST\", `${CLIENT}/login`, { body });\n return new AppserviceClient({ baseUrl, asToken: res.access_token, sender: res.user_id, fetch });\n}\n","export const ITEM_KEY = \"io.titan.item\";\nexport const ITEM_VERSION = 1;\n\nexport const ITEM_KINDS = [\"approval_request\", \"endorse_request\", \"question\", \"notice\", \"message\"] as const;\nexport type ItemKind = (typeof ITEM_KINDS)[number];\n\n/** The structured record carried inside an item's m.room.message content. */\nexport interface TitanItem {\n v: 1;\n kind: ItemKind;\n machine: string;\n session: string;\n agent_id?: string;\n msg_id: string;\n at: number;\n tool_name?: string;\n input_preview?: string;\n recipient?: string;\n truncated: boolean;\n redacted: boolean;\n}\n\n/** An item plus the free text it shows; the text travels in `body` only. */\nexport interface ItemInput extends Omit<TitanItem, \"v\"> {\n text?: string;\n}\n\n// A type alias, not an interface, so it stays assignable to Record<string, unknown> for send().\nexport type ItemContent = {\n msgtype: \"m.text\";\n body: string;\n format?: \"org.matrix.custom.html\";\n formatted_body?: string;\n [ITEM_KEY]: TitanItem;\n};\n\nconst LABELS: Record<ItemKind, string> = {\n approval_request: \"APPR\",\n endorse_request: \"ENDORSE\",\n question: \"QUESTION\",\n notice: \"NOTICE\",\n message: \"MSG\",\n};\n\nconst PROMPTS: Record<ItemKind, string> = {\n approval_request: \"react ✅ allow, ❌ deny\",\n endorse_request: \"react ✅ approve, ❌ dismiss\",\n question: \"reply to answer, or reply dismiss\",\n notice: \"react ✅ or ❌ to dismiss\",\n message: \"react ✅ or ❌ to dismiss\",\n};\n\nexport const TRUNCATED_LINE = \"too large to approve from the phone; answer at the terminal\";\nexport const REDACTED_LINE = \"secrets redacted; answer at the terminal\";\n\nfunction headline(item: ItemInput): string {\n const to = item.recipient ? ` to ${item.recipient}` : \"\";\n return `${LABELS[item.kind]} from ${item.session} (${item.machine})${to}`;\n}\n\nfunction detail(item: ItemInput): string {\n if (item.kind === \"approval_request\") return `${item.tool_name ?? \"tool\"}: ${item.input_preview ?? \"\"}`;\n return item.text ?? \"\";\n}\n\nfunction footer(item: ItemInput): string {\n if (item.truncated) return TRUNCATED_LINE;\n if (item.redacted) return REDACTED_LINE;\n return PROMPTS[item.kind];\n}\n\nexport function renderItemBody(item: ItemInput): string {\n return `${headline(item)}\\n${detail(item)}\\n\\n${footer(item)}`;\n}\n\nfunction recordOf(item: ItemInput): TitanItem {\n const fields = Object.entries(item).filter(([key, value]) => key !== \"text\" && value !== undefined);\n return { v: ITEM_VERSION, ...Object.fromEntries(fields) } as TitanItem;\n}\n\nexport function encodeItem(item: ItemInput, formattedBody?: string): ItemContent {\n const content: ItemContent = { msgtype: \"m.text\", body: renderItemBody(item), [ITEM_KEY]: recordOf(item) };\n if (formattedBody === undefined) return content;\n return { ...content, format: \"org.matrix.custom.html\", formatted_body: formattedBody };\n}\n\nconst REQUIRED_STRINGS = [\"machine\", \"session\", \"msg_id\"] as const;\nconst OPTIONAL_STRINGS = [\"agent_id\", \"tool_name\", \"input_preview\", \"recipient\"] as const;\n\nfunction isItem(value: Record<string, unknown>): boolean {\n if (value.v !== ITEM_VERSION || !ITEM_KINDS.includes(value.kind as ItemKind)) return false;\n if (!REQUIRED_STRINGS.every((key) => typeof value[key] === \"string\")) return false;\n if (!OPTIONAL_STRINGS.every((key) => value[key] === undefined || typeof value[key] === \"string\")) return false;\n return typeof value.at === \"number\" && typeof value.truncated === \"boolean\" && typeof value.redacted === \"boolean\";\n}\n\n/** Returns null for content without the key, an unknown version, or a malformed record. */\nexport function decodeItem(content: Record<string, unknown>): TitanItem | null {\n const value = content[ITEM_KEY];\n if (typeof value !== \"object\" || value === null) return null;\n const record = value as Record<string, unknown>;\n if (!isItem(record)) return null;\n const pick = (keys: readonly string[]) => keys.filter((key) => record[key] !== undefined).map((key) => [key, record[key]]);\n const base = [\"v\", \"kind\", ...REQUIRED_STRINGS, \"at\", \"truncated\", \"redacted\"];\n return Object.fromEntries([...pick(base), ...pick(OPTIONAL_STRINGS)]) as TitanItem;\n}\n","import type { ItemKind } from \"./item.js\";\nimport type { MatrixEvent } from \"./types.js\";\n\nexport const RESOLUTION_EVENT = \"io.titan.resolution\";\n\nexport type Verdict = \"allow\" | \"deny\" | \"approve\" | \"dismiss\" | \"answer\";\n\nexport interface Resolution {\n itemEventId: string;\n verdict: Verdict;\n text?: string;\n}\n\nexport interface FoldContext {\n ownerUserId: string;\n /** Open items the caller posted, keyed by event id; the kind picks the verdict vocabulary. */\n itemEventIds: ReadonlyMap<string, ItemKind>;\n}\n\ntype Family = \"yes\" | \"no\";\n\nconst REACTION_FAMILY: Record<string, Family> = { \"✅\": \"yes\", \"👍\": \"yes\", \"❌\": \"no\", \"👎\": \"no\" };\nconst WORD_FAMILY: Record<string, Family> = { allow: \"yes\", approve: \"yes\", deny: \"no\", dismiss: \"no\" };\n\n// Section 4.3: what each family means per kind; a missing entry means the action does not resolve that kind.\nconst VERDICTS: Record<ItemKind, Partial<Record<Family, Verdict>>> = {\n approval_request: { yes: \"allow\", no: \"deny\" },\n endorse_request: { yes: \"approve\", no: \"dismiss\" },\n question: { no: \"dismiss\" },\n notice: { yes: \"dismiss\", no: \"dismiss\" },\n message: { yes: \"dismiss\", no: \"dismiss\" },\n};\n\n// Variation selectors and skin-tone modifiers, so 👍🏽 counts as 👍.\nconst EMOJI_MODIFIERS = /\\uFE0E|\\uFE0F|\\p{Emoji_Modifier}/gu;\n\nfunction relatesTo(content: Record<string, unknown>): Record<string, unknown> {\n const rel = content[\"m.relates_to\"];\n return typeof rel === \"object\" && rel !== null ? (rel as Record<string, unknown>) : {};\n}\n\nfunction stringField(value: unknown, key: string): string | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n const field = (value as Record<string, unknown>)[key];\n return typeof field === \"string\" ? field : undefined;\n}\n\nfunction foldReaction(content: Record<string, unknown>, kind: ItemKind, itemEventId: string): Resolution | null {\n const key = stringField(relatesTo(content), \"key\")?.replace(EMOJI_MODIFIERS, \"\");\n const family = key === undefined ? undefined : REACTION_FAMILY[key];\n const verdict = family && VERDICTS[kind][family];\n return verdict ? { itemEventId, verdict } : null;\n}\n\n// Drops a legacy reply fallback (\"> <@owner> quoted\" lines plus one blank line) from the top of a body.\nexport function stripReplyFallback(body: string): string {\n const lines = body.split(\"\\n\");\n let start = 0;\n while (start < lines.length && lines[start]?.startsWith(\">\")) start += 1;\n if (start > 0 && lines[start]?.trim() === \"\") start += 1;\n return lines.slice(start).join(\"\\n\").trim();\n}\n\nfunction lastLine(text: string): string {\n const lines = text.split(\"\\n\").map((line) => line.trim()).filter(Boolean);\n return (lines.at(-1) ?? \"\").toLowerCase();\n}\n\nfunction foldWords(text: string, kind: ItemKind, itemEventId: string): Resolution | null {\n const family = WORD_FAMILY[lastLine(text)];\n const verdict = family && VERDICTS[kind][family];\n if (verdict) return { itemEventId, verdict };\n if (kind === \"question\" && text) return { itemEventId, verdict: \"answer\", text };\n return null;\n}\n\nfunction foldReply(content: Record<string, unknown>, kind: ItemKind, itemEventId: string): Resolution | null {\n const body = stringField(content, \"body\");\n return body === undefined ? null : foldWords(stripReplyFallback(body), kind, itemEventId);\n}\n\nfunction foldDecision(content: Record<string, unknown>, kind: ItemKind, itemEventId: string): Resolution | null {\n const decision = stringField(content, \"decision\");\n if (decision === \"answer\") {\n const text = stringField(content, \"text\")?.trim();\n return kind === \"question\" && text ? { itemEventId, verdict: \"answer\", text } : null;\n }\n const family = decision === undefined ? undefined : WORD_FAMILY[decision];\n const verdict = family && VERDICTS[kind][family];\n return verdict ? { itemEventId, verdict } : null;\n}\n\nfunction targetOf(event: MatrixEvent): string | undefined {\n const rel = relatesTo(event.content);\n if (event.type === \"m.reaction\") return rel.rel_type === \"m.annotation\" ? stringField(rel, \"event_id\") : undefined;\n if (event.type === \"m.room.message\") return stringField(rel[\"m.in_reply_to\"], \"event_id\");\n if (event.type === RESOLUTION_EVENT) return stringField(rel, \"event_id\");\n return undefined;\n}\n\n/** Folds an owner's reaction, reply or io.titan.resolution into a verdict on an open item; anything else is null. */\nexport function foldResolution(event: MatrixEvent, { ownerUserId, itemEventIds }: FoldContext): Resolution | null {\n if (event.sender !== ownerUserId || event.state_key !== undefined) return null;\n const itemEventId = targetOf(event);\n const kind = itemEventId === undefined ? undefined : itemEventIds.get(itemEventId);\n if (itemEventId === undefined || kind === undefined) return null;\n if (event.type === \"m.reaction\") return foldReaction(event.content, kind, itemEventId);\n if (event.type === \"m.room.message\") return foldReply(event.content, kind, itemEventId);\n return foldDecision(event.content, kind, itemEventId);\n}\n","import { MatrixError } from \"./http.js\";\nimport type { AppserviceClient } from \"./client.js\";\n\nexport interface PowerLevels {\n users: Record<string, number>;\n users_default: number;\n events_default: number;\n state_default: number;\n invite: number;\n kick: number;\n ban: number;\n redact: number;\n events: Record<string, number>;\n}\n\n/** #queue levels: members may post items, only the creator (the owner) may react or resolve. */\nexport function queuePowerLevels(): PowerLevels {\n return {\n users: {},\n users_default: 0,\n events_default: 0,\n state_default: 100,\n invite: 100,\n kick: 100,\n ban: 100,\n redact: 100,\n events: { \"m.reaction\": 100, \"io.titan.resolution\": 100, \"m.room.power_levels\": 100 },\n };\n}\n\nexport interface BootstrapQueueOptions {\n /** Full alias, e.g. `#queue:chat.example.org`. */\n alias: string;\n mirrorUserIds: string[];\n name?: string;\n}\n\nconst directoryPath = (alias: string) => `/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`;\n\nasync function resolveAlias(client: AppserviceClient, alias: string): Promise<string | undefined> {\n try {\n return (await client.request<{ room_id: string }>(\"GET\", directoryPath(alias))).room_id;\n } catch (err) {\n if (err instanceof MatrixError && err.status === 404) return undefined;\n throw err;\n }\n}\n\n/**\n * Creates the owner's #queue at the server's default room version (v12), so the owner is creator.\n * An alias that already resolves returns its room id unchanged.\n */\nexport async function bootstrapQueueRoom(owner: AppserviceClient, options: BootstrapQueueOptions): Promise<string> {\n const existing = await resolveAlias(owner, options.alias);\n if (existing) return existing;\n const { room_id: roomId } = await owner.request<{ room_id: string }>(\"POST\", \"/_matrix/client/v3/createRoom\", {\n body: {\n preset: \"private_chat\",\n name: options.name ?? \"queue\",\n invite: options.mirrorUserIds,\n power_level_content_override: queuePowerLevels(),\n },\n });\n await owner.request(\"PUT\", directoryPath(options.alias), { body: { room_id: roomId } });\n return roomId;\n}\n","export interface RegistrationOptions {\n id: string;\n asToken: string;\n hsToken: string;\n /** Where the homeserver pushes transactions; null for a receive-only edge that pulls with /sync. */\n url: string | null;\n senderLocalpart: string;\n /** Machine name inside the namespace: `edge1` owns `@ac-edge1-.*`. */\n machine: string;\n serverName: string;\n}\n\n// Letters and digits only, so one machine's prefix can never be a prefix of another's.\nconst MACHINE = /^[a-z0-9]+$/;\nconst LOCALPART = /^[a-z0-9._=/-]+$/;\n\nexport function escapeRegex(text: string): string {\n return text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nexport function machineUserRegex(machine: string, serverName: string): string {\n if (!MACHINE.test(machine)) throw new Error(`machine must match ${MACHINE}: ${JSON.stringify(machine)}`);\n return `^@ac-${machine}-.*:${escapeRegex(serverName)}$`;\n}\n\n// JSON strings are valid YAML double-quoted scalars, which also doubles the regex backslashes.\nconst quote = (value: string) => JSON.stringify(value);\n\nexport function renderRegistration(options: RegistrationOptions): string {\n if (!LOCALPART.test(options.senderLocalpart)) throw new Error(`invalid sender localpart: ${JSON.stringify(options.senderLocalpart)}`);\n return [\n `id: ${quote(options.id)}`,\n `url: ${options.url === null ? \"null\" : quote(options.url)}`,\n `as_token: ${quote(options.asToken)}`,\n `hs_token: ${quote(options.hsToken)}`,\n `sender_localpart: ${options.senderLocalpart}`,\n \"rate_limited: false\",\n \"namespaces:\",\n \" users:\",\n \" - exclusive: true\",\n ` regex: ${quote(machineUserRegex(options.machine, options.serverName))}`,\n \" aliases: []\",\n \" rooms: []\",\n \"\",\n ].join(\"\\n\");\n}\n"],"mappings":"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","names":["room"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@titan-design/matrix-bus",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Matrix client-server API over fetch: appservice client, io.titan.item codec, owner resolution fold, #queue bootstrap",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"author": "Henry Jewkes",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/HJewkes/titan-platform#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/HJewkes/titan-platform.git",
|
|
24
|
+
"directory": "packages/matrix-bus"
|
|
25
|
+
},
|
|
26
|
+
"bugs": "https://github.com/HJewkes/titan-platform/issues",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"lint": "eslint src"
|
|
37
|
+
}
|
|
38
|
+
}
|