@absol-labs/agent 0.7.2 → 0.8.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/README.md +19 -4
- package/dist/gateway/caller-auth-gateway.d.ts +103 -2
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +176 -19
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +12 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +45 -1
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/wallet/autonomous-wallet-store.d.ts +123 -0
- package/dist/wallet/autonomous-wallet-store.d.ts.map +1 -0
- package/dist/wallet/autonomous-wallet-store.js +318 -0
- package/dist/wallet/autonomous-wallet-store.js.map +1 -0
- package/dist/wallet/autonomous-wallet.d.ts +14 -39
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet.js +12 -145
- package/dist/wallet/autonomous-wallet.js.map +1 -1
- package/dist/wallet/encrypted-file-credential-store.d.ts +41 -0
- package/dist/wallet/encrypted-file-credential-store.d.ts.map +1 -0
- package/dist/wallet/encrypted-file-credential-store.js +221 -0
- package/dist/wallet/encrypted-file-credential-store.js.map +1 -0
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +22 -2
- package/dist/wallet/provider.js.map +1 -1
- package/dist/wallet/secret-service-probe.d.ts +56 -0
- package/dist/wallet/secret-service-probe.d.ts.map +1 -0
- package/dist/wallet/secret-service-probe.js +407 -0
- package/dist/wallet/secret-service-probe.js.map +1 -0
- package/package.json +1 -1
- package/src/gateway/caller-auth-gateway.ts +281 -15
- package/src/gateway/http-server.ts +64 -0
- package/src/index.ts +19 -0
- package/src/wallet/autonomous-wallet-store.ts +487 -0
- package/src/wallet/autonomous-wallet.ts +57 -224
- package/src/wallet/encrypted-file-credential-store.ts +341 -0
- package/src/wallet/provider.ts +23 -1
- package/src/wallet/secret-service-probe.ts +487 -0
package/src/wallet/provider.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
custom,
|
|
9
9
|
toHex,
|
|
10
|
+
getTypesForEIP712Domain,
|
|
10
11
|
serializeTypedData,
|
|
11
12
|
type Account,
|
|
12
13
|
type Address,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type SignableMessage,
|
|
15
16
|
type TypedData,
|
|
16
17
|
type TypedDataDefinition,
|
|
18
|
+
type TypedDataDomain,
|
|
17
19
|
isAddress,
|
|
18
20
|
} from "viem";
|
|
19
21
|
import { parseAccount, privateKeyToAccount, toAccount } from "viem/accounts";
|
|
@@ -235,7 +237,27 @@ export async function createPrivyEmbeddedAccount(
|
|
|
235
237
|
parameters: TypedDataDefinition<typedData, primaryType>,
|
|
236
238
|
) => {
|
|
237
239
|
await assertPrivySession(config, chainId);
|
|
238
|
-
|
|
240
|
+
// viem's `serializeTypedData` only carries a domain field over the
|
|
241
|
+
// wire when it is declared in `types.EIP712Domain` (see
|
|
242
|
+
// node_modules/viem/utils/typedData.ts). Callers that build typed
|
|
243
|
+
// data directly (e.g. `buildInvocationCapabilityTypedData`) never
|
|
244
|
+
// declare that struct - they rely on viem's own `signTypedData`
|
|
245
|
+
// wallet action to inject it - so a payload signed by calling this
|
|
246
|
+
// account's `signTypedData` directly (bypassing that wallet action)
|
|
247
|
+
// would otherwise serialize with an empty `domain: {}`, and a
|
|
248
|
+
// domain-bound verifier (e.g. a scoped Privy broker policy keyed on
|
|
249
|
+
// `domain.chainId` / `domain.verifyingContract`) would fail closed.
|
|
250
|
+
// Declare it here so the domain always survives serialization. A
|
|
251
|
+
// caller-supplied `types.EIP712Domain` is preserved as-is: it is
|
|
252
|
+
// spread last, so it wins over the derived declaration below.
|
|
253
|
+
const domain = parameters.domain as TypedDataDomain | undefined;
|
|
254
|
+
const serialized = serializeTypedData({
|
|
255
|
+
...parameters,
|
|
256
|
+
types: {
|
|
257
|
+
EIP712Domain: getTypesForEIP712Domain({ domain }),
|
|
258
|
+
...parameters.types,
|
|
259
|
+
},
|
|
260
|
+
} as TypedDataDefinition<typedData, primaryType>);
|
|
239
261
|
return requireSignature(
|
|
240
262
|
await requestPrivy(config.provider, "eth_signTypedData_v4", [
|
|
241
263
|
config.address,
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { connect, type Socket } from "node:net";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detects whether a freedesktop Secret Service provider (gnome-keyring,
|
|
6
|
+
* KeePassXC, KWallet, ...) is reachable on the D-Bus session bus.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: `@napi-rs/keyring` ships BOTH a `dbus-secret-service`
|
|
9
|
+
* backend and a `linux-keyutils` backend in the same native module and picks
|
|
10
|
+
* one at runtime. When no Secret Service provider answers on the session bus
|
|
11
|
+
* it silently falls through to `linux-keyutils` — the Linux *kernel* keyring,
|
|
12
|
+
* which lives in RAM and is destroyed on reboot. A credential written there
|
|
13
|
+
* looks durable (it round-trips across processes and SSH sessions) but is not,
|
|
14
|
+
* so an agent that provisioned a funded wallet loses its owner key on reboot.
|
|
15
|
+
*
|
|
16
|
+
* The keyring module exposes no API to report which backend it resolved, so the
|
|
17
|
+
* only way to know is to ask the session bus the same question it asks: does
|
|
18
|
+
* anyone own the well-known name `org.freedesktop.secrets`?
|
|
19
|
+
*
|
|
20
|
+
* This talks the D-Bus wire protocol directly over `node:net` (SASL EXTERNAL
|
|
21
|
+
* handshake, then `org.freedesktop.DBus.Hello` and `NameHasOwner`) so no extra
|
|
22
|
+
* dependency and no external binary is required. It is deliberately
|
|
23
|
+
* FAIL-CLOSED: any missing bus, malformed reply, protocol error or timeout
|
|
24
|
+
* reports "not available", which callers must treat as "not durable".
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const SECRET_SERVICE_BUS_NAME = "org.freedesktop.secrets";
|
|
28
|
+
const DBUS_PATH = "/org/freedesktop/DBus";
|
|
29
|
+
const DBUS_INTERFACE = "org.freedesktop.DBus";
|
|
30
|
+
const DBUS_DESTINATION = "org.freedesktop.DBus";
|
|
31
|
+
|
|
32
|
+
const MESSAGE_TYPE_METHOD_RETURN = 2;
|
|
33
|
+
const MESSAGE_TYPE_ERROR = 3;
|
|
34
|
+
|
|
35
|
+
const HEADER_FIELD_ERROR_NAME = 4;
|
|
36
|
+
const HEADER_FIELD_REPLY_SERIAL = 5;
|
|
37
|
+
const HEADER_FIELD_SIGNATURE = 8;
|
|
38
|
+
|
|
39
|
+
const HELLO_SERIAL = 1;
|
|
40
|
+
const NAME_HAS_OWNER_SERIAL = 2;
|
|
41
|
+
|
|
42
|
+
export interface SecretServiceProbeResult {
|
|
43
|
+
readonly available: boolean;
|
|
44
|
+
/** Human-readable reason, safe to surface in an error message. */
|
|
45
|
+
readonly detail: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ProbeSecretServiceOptions {
|
|
49
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
50
|
+
readonly timeoutMs?: number;
|
|
51
|
+
readonly uid?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function alignTo(value: number, alignment: number): number {
|
|
55
|
+
const remainder = value % alignment;
|
|
56
|
+
return remainder === 0 ? value : value + (alignment - remainder);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Minimal little-endian D-Bus marshaller. Messages here are a few dozen bytes. */
|
|
60
|
+
class ByteWriter {
|
|
61
|
+
private readonly bytes: number[] = [];
|
|
62
|
+
|
|
63
|
+
get length(): number {
|
|
64
|
+
return this.bytes.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
byte(value: number): void {
|
|
68
|
+
this.bytes.push(value & 0xff);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
align(alignment: number): void {
|
|
72
|
+
while (this.bytes.length % alignment !== 0) this.bytes.push(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
uint32(value: number): void {
|
|
76
|
+
this.align(4);
|
|
77
|
+
this.bytes.push(
|
|
78
|
+
value & 0xff,
|
|
79
|
+
(value >>> 8) & 0xff,
|
|
80
|
+
(value >>> 16) & 0xff,
|
|
81
|
+
(value >>> 24) & 0xff,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
raw(buffer: Buffer): void {
|
|
86
|
+
for (const byte of buffer) this.bytes.push(byte);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** DBus STRING / OBJECT_PATH: uint32 length, utf8 bytes, NUL. */
|
|
90
|
+
string(value: string): void {
|
|
91
|
+
const encoded = Buffer.from(value, "utf8");
|
|
92
|
+
this.uint32(encoded.length);
|
|
93
|
+
this.raw(encoded);
|
|
94
|
+
this.byte(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** DBus SIGNATURE: single length byte, ascii bytes, NUL. */
|
|
98
|
+
signature(value: string): void {
|
|
99
|
+
const encoded = Buffer.from(value, "ascii");
|
|
100
|
+
this.byte(encoded.length);
|
|
101
|
+
this.raw(encoded);
|
|
102
|
+
this.byte(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
toBuffer(): Buffer {
|
|
106
|
+
return Buffer.from(this.bytes);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type HeaderFieldValue =
|
|
111
|
+
| { readonly type: "s" | "o" | "g"; readonly value: string }
|
|
112
|
+
| { readonly type: "u"; readonly value: number };
|
|
113
|
+
|
|
114
|
+
function encodeHeaderFields(
|
|
115
|
+
fields: ReadonlyArray<readonly [number, HeaderFieldValue]>,
|
|
116
|
+
): Buffer {
|
|
117
|
+
const writer = new ByteWriter();
|
|
118
|
+
for (const [code, field] of fields) {
|
|
119
|
+
// Each ARRAY-of-STRUCT element is 8-byte aligned. The field array always
|
|
120
|
+
// starts at message offset 16, which is itself 8-aligned, so writing this
|
|
121
|
+
// buffer standalone produces identical padding.
|
|
122
|
+
writer.align(8);
|
|
123
|
+
writer.byte(code);
|
|
124
|
+
writer.signature(field.type);
|
|
125
|
+
if (field.type === "u") writer.uint32(field.value);
|
|
126
|
+
else if (field.type === "g") writer.signature(field.value);
|
|
127
|
+
else writer.string(field.value);
|
|
128
|
+
}
|
|
129
|
+
return writer.toBuffer();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface EncodeMethodCallInput {
|
|
133
|
+
readonly serial: number;
|
|
134
|
+
readonly path: string;
|
|
135
|
+
readonly interfaceName: string;
|
|
136
|
+
readonly member: string;
|
|
137
|
+
readonly destination: string;
|
|
138
|
+
readonly signature?: string;
|
|
139
|
+
readonly body?: Buffer;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Exported for unit tests: encodes one METHOD_CALL message. */
|
|
143
|
+
export function encodeMethodCall(input: EncodeMethodCallInput): Buffer {
|
|
144
|
+
const body = input.body ?? Buffer.alloc(0);
|
|
145
|
+
const fields: Array<readonly [number, HeaderFieldValue]> = [
|
|
146
|
+
[1, { type: "o", value: input.path }],
|
|
147
|
+
[2, { type: "s", value: input.interfaceName }],
|
|
148
|
+
[3, { type: "s", value: input.member }],
|
|
149
|
+
[6, { type: "s", value: input.destination }],
|
|
150
|
+
];
|
|
151
|
+
if (input.signature !== undefined) {
|
|
152
|
+
fields.push([
|
|
153
|
+
HEADER_FIELD_SIGNATURE,
|
|
154
|
+
{ type: "g", value: input.signature },
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
const encodedFields = encodeHeaderFields(fields);
|
|
158
|
+
|
|
159
|
+
const message = new ByteWriter();
|
|
160
|
+
message.byte(0x6c); // 'l' — little endian
|
|
161
|
+
message.byte(1); // METHOD_CALL
|
|
162
|
+
message.byte(0); // no flags
|
|
163
|
+
message.byte(1); // protocol version
|
|
164
|
+
message.uint32(body.length);
|
|
165
|
+
message.uint32(input.serial);
|
|
166
|
+
message.uint32(encodedFields.length);
|
|
167
|
+
message.raw(encodedFields);
|
|
168
|
+
message.align(8);
|
|
169
|
+
message.raw(body);
|
|
170
|
+
return message.toBuffer();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Exported for unit tests: encodes a single-STRING message body. */
|
|
174
|
+
export function encodeStringBody(value: string): Buffer {
|
|
175
|
+
const writer = new ByteWriter();
|
|
176
|
+
writer.string(value);
|
|
177
|
+
return writer.toBuffer();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface DBusMessage {
|
|
181
|
+
readonly type: number;
|
|
182
|
+
readonly serial: number;
|
|
183
|
+
readonly replySerial?: number;
|
|
184
|
+
readonly signature?: string;
|
|
185
|
+
readonly errorName?: string;
|
|
186
|
+
readonly body: Buffer;
|
|
187
|
+
readonly littleEndian: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Exported for unit tests: decodes as many complete messages as `buffer` holds
|
|
192
|
+
* and returns the undecoded remainder. Throws on a malformed stream.
|
|
193
|
+
*/
|
|
194
|
+
export function decodeMessages(buffer: Buffer): {
|
|
195
|
+
readonly messages: readonly DBusMessage[];
|
|
196
|
+
readonly rest: Buffer;
|
|
197
|
+
} {
|
|
198
|
+
const messages: DBusMessage[] = [];
|
|
199
|
+
let offset = 0;
|
|
200
|
+
while (buffer.length - offset >= 16) {
|
|
201
|
+
const start = offset;
|
|
202
|
+
const endian = buffer[start];
|
|
203
|
+
if (endian !== 0x6c && endian !== 0x42) {
|
|
204
|
+
throw new Error("invalid D-Bus endianness marker");
|
|
205
|
+
}
|
|
206
|
+
const littleEndian = endian === 0x6c;
|
|
207
|
+
const readUint32 = (at: number): number =>
|
|
208
|
+
littleEndian ? buffer.readUInt32LE(at) : buffer.readUInt32BE(at);
|
|
209
|
+
const type = buffer[start + 1]!;
|
|
210
|
+
const bodyLength = readUint32(start + 4);
|
|
211
|
+
const serial = readUint32(start + 8);
|
|
212
|
+
const fieldsLength = readUint32(start + 12);
|
|
213
|
+
const bodyOffset = alignTo(16 + fieldsLength, 8);
|
|
214
|
+
const total = bodyOffset + bodyLength;
|
|
215
|
+
if (buffer.length - start < total) break;
|
|
216
|
+
|
|
217
|
+
let replySerial: number | undefined;
|
|
218
|
+
let signature: string | undefined;
|
|
219
|
+
let errorName: string | undefined;
|
|
220
|
+
const fieldsEnd = 16 + fieldsLength;
|
|
221
|
+
let cursor = 16;
|
|
222
|
+
while (cursor < fieldsEnd) {
|
|
223
|
+
cursor = alignTo(cursor, 8);
|
|
224
|
+
if (cursor >= fieldsEnd) break;
|
|
225
|
+
const code = buffer[start + cursor]!;
|
|
226
|
+
cursor += 1;
|
|
227
|
+
const typeLength = buffer[start + cursor]!;
|
|
228
|
+
cursor += 1;
|
|
229
|
+
const fieldType = buffer.toString(
|
|
230
|
+
"ascii",
|
|
231
|
+
start + cursor,
|
|
232
|
+
start + cursor + typeLength,
|
|
233
|
+
);
|
|
234
|
+
cursor += typeLength + 1;
|
|
235
|
+
if (fieldType === "u") {
|
|
236
|
+
cursor = alignTo(cursor, 4);
|
|
237
|
+
const value = readUint32(start + cursor);
|
|
238
|
+
cursor += 4;
|
|
239
|
+
if (code === HEADER_FIELD_REPLY_SERIAL) replySerial = value;
|
|
240
|
+
} else if (fieldType === "s" || fieldType === "o") {
|
|
241
|
+
cursor = alignTo(cursor, 4);
|
|
242
|
+
const length = readUint32(start + cursor);
|
|
243
|
+
cursor += 4;
|
|
244
|
+
const value = buffer.toString(
|
|
245
|
+
"utf8",
|
|
246
|
+
start + cursor,
|
|
247
|
+
start + cursor + length,
|
|
248
|
+
);
|
|
249
|
+
cursor += length + 1;
|
|
250
|
+
if (code === HEADER_FIELD_ERROR_NAME) errorName = value;
|
|
251
|
+
} else if (fieldType === "g") {
|
|
252
|
+
const length = buffer[start + cursor]!;
|
|
253
|
+
cursor += 1;
|
|
254
|
+
const value = buffer.toString(
|
|
255
|
+
"ascii",
|
|
256
|
+
start + cursor,
|
|
257
|
+
start + cursor + length,
|
|
258
|
+
);
|
|
259
|
+
cursor += length + 1;
|
|
260
|
+
if (code === HEADER_FIELD_SIGNATURE) signature = value;
|
|
261
|
+
} else {
|
|
262
|
+
throw new Error(`unsupported D-Bus header field type "${fieldType}"`);
|
|
263
|
+
}
|
|
264
|
+
if (cursor > fieldsEnd) {
|
|
265
|
+
throw new Error("malformed D-Bus header fields");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
messages.push({
|
|
270
|
+
type,
|
|
271
|
+
serial,
|
|
272
|
+
...(replySerial === undefined ? {} : { replySerial }),
|
|
273
|
+
...(signature === undefined ? {} : { signature }),
|
|
274
|
+
...(errorName === undefined ? {} : { errorName }),
|
|
275
|
+
body: buffer.subarray(start + bodyOffset, start + total),
|
|
276
|
+
littleEndian,
|
|
277
|
+
});
|
|
278
|
+
offset = start + total;
|
|
279
|
+
}
|
|
280
|
+
return { messages, rest: buffer.subarray(offset) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
type BusAddress =
|
|
284
|
+
| { readonly kind: "path"; readonly path: string }
|
|
285
|
+
| { readonly kind: "error"; readonly detail: string };
|
|
286
|
+
|
|
287
|
+
/** Exported for unit tests. */
|
|
288
|
+
export function resolveSessionBusPath(env: NodeJS.ProcessEnv): BusAddress {
|
|
289
|
+
const address = env.DBUS_SESSION_BUS_ADDRESS;
|
|
290
|
+
if (typeof address === "string" && address.trim() !== "") {
|
|
291
|
+
for (const entry of address.split(";")) {
|
|
292
|
+
const trimmed = entry.trim();
|
|
293
|
+
if (!trimmed.startsWith("unix:")) continue;
|
|
294
|
+
const parameters = new Map<string, string>();
|
|
295
|
+
for (const pair of trimmed.slice("unix:".length).split(",")) {
|
|
296
|
+
const separator = pair.indexOf("=");
|
|
297
|
+
if (separator > 0) {
|
|
298
|
+
parameters.set(pair.slice(0, separator), pair.slice(separator + 1));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const path = parameters.get("path");
|
|
302
|
+
if (path !== undefined && path !== "") return { kind: "path", path };
|
|
303
|
+
const abstract = parameters.get("abstract");
|
|
304
|
+
if (abstract !== undefined && abstract !== "") {
|
|
305
|
+
// Linux abstract namespace sockets are addressed with a leading NUL.
|
|
306
|
+
return { kind: "path", path: `\0${abstract}` };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
kind: "error",
|
|
311
|
+
detail: `DBUS_SESSION_BUS_ADDRESS has no usable unix transport (${address})`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const runtimeDirectory = env.XDG_RUNTIME_DIR;
|
|
315
|
+
if (typeof runtimeDirectory === "string" && runtimeDirectory !== "") {
|
|
316
|
+
const candidate = `${runtimeDirectory.replace(/\/$/, "")}/bus`;
|
|
317
|
+
if (existsSync(candidate)) return { kind: "path", path: candidate };
|
|
318
|
+
return {
|
|
319
|
+
kind: "error",
|
|
320
|
+
detail: `no D-Bus session socket at ${candidate}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
kind: "error",
|
|
325
|
+
detail:
|
|
326
|
+
"no D-Bus session bus (DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are unset)",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Asks the D-Bus session bus whether `org.freedesktop.secrets` has an owner.
|
|
332
|
+
* Never throws; every failure path resolves to `available: false`.
|
|
333
|
+
*/
|
|
334
|
+
export async function probeSecretServiceAvailable(
|
|
335
|
+
options: ProbeSecretServiceOptions = {},
|
|
336
|
+
): Promise<SecretServiceProbeResult> {
|
|
337
|
+
const env = options.env ?? process.env;
|
|
338
|
+
const timeoutMs = options.timeoutMs ?? 2_000;
|
|
339
|
+
const address = resolveSessionBusPath(env);
|
|
340
|
+
if (address.kind === "error") {
|
|
341
|
+
return { available: false, detail: address.detail };
|
|
342
|
+
}
|
|
343
|
+
const uid =
|
|
344
|
+
options.uid ??
|
|
345
|
+
(typeof process.getuid === "function" ? process.getuid() : 0);
|
|
346
|
+
|
|
347
|
+
return new Promise<SecretServiceProbeResult>((resolve) => {
|
|
348
|
+
let socket: Socket;
|
|
349
|
+
try {
|
|
350
|
+
socket = connect({ path: address.path });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
resolve({
|
|
353
|
+
available: false,
|
|
354
|
+
detail: `could not connect to the D-Bus session bus: ${describe(error)}`,
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let settled = false;
|
|
359
|
+
let authenticated = false;
|
|
360
|
+
let buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
|
361
|
+
|
|
362
|
+
const finish = (result: SecretServiceProbeResult): void => {
|
|
363
|
+
if (settled) return;
|
|
364
|
+
settled = true;
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
socket.removeAllListeners();
|
|
367
|
+
socket.destroy();
|
|
368
|
+
resolve(result);
|
|
369
|
+
};
|
|
370
|
+
const timer = setTimeout(() => {
|
|
371
|
+
finish({
|
|
372
|
+
available: false,
|
|
373
|
+
detail: `D-Bus session bus did not answer within ${timeoutMs}ms`,
|
|
374
|
+
});
|
|
375
|
+
}, timeoutMs);
|
|
376
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
377
|
+
|
|
378
|
+
socket.on("error", (error) => {
|
|
379
|
+
finish({
|
|
380
|
+
available: false,
|
|
381
|
+
detail: `D-Bus session bus connection failed: ${describe(error)}`,
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
socket.on("close", () => {
|
|
385
|
+
finish({
|
|
386
|
+
available: false,
|
|
387
|
+
detail: "D-Bus session bus closed the connection before replying",
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
socket.on("connect", () => {
|
|
391
|
+
// SASL EXTERNAL: the NUL byte, then the uid as ASCII hex.
|
|
392
|
+
const identity = Buffer.from(String(uid), "ascii").toString("hex");
|
|
393
|
+
socket.write(`\0AUTH EXTERNAL ${identity}\r\n`);
|
|
394
|
+
});
|
|
395
|
+
socket.on("data", (chunk: Buffer) => {
|
|
396
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
397
|
+
if (!authenticated) {
|
|
398
|
+
const terminator = buffer.indexOf("\r\n");
|
|
399
|
+
if (terminator < 0) return;
|
|
400
|
+
const line = buffer.toString("ascii", 0, terminator);
|
|
401
|
+
buffer = buffer.subarray(terminator + 2);
|
|
402
|
+
if (!line.startsWith("OK")) {
|
|
403
|
+
finish({
|
|
404
|
+
available: false,
|
|
405
|
+
detail: `D-Bus session bus rejected authentication (${line})`,
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
authenticated = true;
|
|
410
|
+
socket.write("BEGIN\r\n");
|
|
411
|
+
socket.write(
|
|
412
|
+
encodeMethodCall({
|
|
413
|
+
serial: HELLO_SERIAL,
|
|
414
|
+
path: DBUS_PATH,
|
|
415
|
+
interfaceName: DBUS_INTERFACE,
|
|
416
|
+
member: "Hello",
|
|
417
|
+
destination: DBUS_DESTINATION,
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
socket.write(
|
|
421
|
+
encodeMethodCall({
|
|
422
|
+
serial: NAME_HAS_OWNER_SERIAL,
|
|
423
|
+
path: DBUS_PATH,
|
|
424
|
+
interfaceName: DBUS_INTERFACE,
|
|
425
|
+
member: "NameHasOwner",
|
|
426
|
+
destination: DBUS_DESTINATION,
|
|
427
|
+
signature: "s",
|
|
428
|
+
body: encodeStringBody(SECRET_SERVICE_BUS_NAME),
|
|
429
|
+
}),
|
|
430
|
+
);
|
|
431
|
+
if (buffer.length === 0) return;
|
|
432
|
+
}
|
|
433
|
+
let decoded: ReturnType<typeof decodeMessages>;
|
|
434
|
+
try {
|
|
435
|
+
decoded = decodeMessages(buffer);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
finish({
|
|
438
|
+
available: false,
|
|
439
|
+
detail: `could not decode the D-Bus reply: ${describe(error)}`,
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
buffer = decoded.rest;
|
|
444
|
+
for (const message of decoded.messages) {
|
|
445
|
+
if (message.replySerial !== NAME_HAS_OWNER_SERIAL) continue;
|
|
446
|
+
if (message.type === MESSAGE_TYPE_ERROR) {
|
|
447
|
+
finish({
|
|
448
|
+
available: false,
|
|
449
|
+
detail: `D-Bus NameHasOwner failed (${message.errorName ?? "unknown error"})`,
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (
|
|
454
|
+
message.type !== MESSAGE_TYPE_METHOD_RETURN ||
|
|
455
|
+
message.signature !== "b" ||
|
|
456
|
+
message.body.length < 4
|
|
457
|
+
) {
|
|
458
|
+
finish({
|
|
459
|
+
available: false,
|
|
460
|
+
detail: "D-Bus returned an unexpected NameHasOwner reply",
|
|
461
|
+
});
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const owned =
|
|
465
|
+
(message.littleEndian
|
|
466
|
+
? message.body.readUInt32LE(0)
|
|
467
|
+
: message.body.readUInt32BE(0)) !== 0;
|
|
468
|
+
finish(
|
|
469
|
+
owned
|
|
470
|
+
? {
|
|
471
|
+
available: true,
|
|
472
|
+
detail: `${SECRET_SERVICE_BUS_NAME} is owned on the D-Bus session bus`,
|
|
473
|
+
}
|
|
474
|
+
: {
|
|
475
|
+
available: false,
|
|
476
|
+
detail: `no process owns ${SECRET_SERVICE_BUS_NAME} on the D-Bus session bus`,
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function describe(error: unknown): string {
|
|
486
|
+
return error instanceof Error ? error.message : String(error);
|
|
487
|
+
}
|