@indigoai-us/hq-cli 5.108.0 → 5.108.2
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/CHANGELOG.md +79 -0
- package/assets/mesh-daemon/README.md +23 -0
- package/assets/mesh-daemon/launchd.plist.template +33 -0
- package/assets/mesh-daemon/systemd.system.service.template +27 -0
- package/assets/mesh-daemon/systemd.user.service.template +18 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
- package/dist/commands/cloud.d.ts +6 -0
- package/dist/commands/cloud.js +26 -3
- package/dist/commands/core.js +5 -4
- package/dist/commands/mesh.d.ts +15 -0
- package/dist/commands/mesh.js +732 -4
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.js +42 -1
- package/dist/lib/doctor/checks/work-context.d.ts +23 -0
- package/dist/lib/doctor/checks/work-context.js +195 -0
- package/dist/lib/doctor/registry.js +4 -0
- package/dist/lib/mesh/api.d.ts +18 -0
- package/dist/lib/mesh/api.js +55 -0
- package/dist/lib/mesh/cache.js +35 -4
- package/dist/lib/mesh/client.d.ts +118 -0
- package/dist/lib/mesh/client.js +268 -0
- package/dist/lib/mesh/live/backoff.d.ts +16 -0
- package/dist/lib/mesh/live/backoff.js +20 -0
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
- package/dist/lib/mesh/live/daemon/credentials.js +227 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/doctor.js +139 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/index.js +14 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
- package/dist/lib/mesh/live/daemon/install.js +330 -0
- package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/log.js +79 -0
- package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/paths.js +31 -0
- package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
- package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
- package/dist/lib/mesh/live/daemon/presence.js +250 -0
- package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/presign.js +65 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
- package/dist/lib/mesh/live/daemon/run.js +400 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/state.js +57 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
- package/dist/lib/mesh/live/enqueue.d.ts +48 -0
- package/dist/lib/mesh/live/enqueue.js +100 -0
- package/dist/lib/mesh/live/flush.d.ts +56 -0
- package/dist/lib/mesh/live/flush.js +360 -0
- package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
- package/dist/lib/mesh/live/format-spool-line.js +106 -0
- package/dist/lib/mesh/live/index.d.ts +22 -0
- package/dist/lib/mesh/live/index.js +16 -0
- package/dist/lib/mesh/live/paths.d.ts +13 -0
- package/dist/lib/mesh/live/paths.js +30 -0
- package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
- package/dist/lib/mesh/live/session-events-client.js +120 -0
- package/dist/lib/mesh/live/session-identity.d.ts +39 -0
- package/dist/lib/mesh/live/session-identity.js +91 -0
- package/dist/lib/mesh/live/spool.d.ts +52 -0
- package/dist/lib/mesh/live/spool.js +193 -0
- package/dist/lib/mesh/live/ulid.d.ts +12 -0
- package/dist/lib/mesh/live/ulid.js +39 -0
- package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
- package/dist/lib/mesh/live/validate-session-event.js +128 -0
- package/dist/lib/search-index/index.d.ts +45 -0
- package/dist/lib/search-index/index.js +76 -0
- package/dist/lib/work-context/atomic.d.ts +18 -0
- package/dist/lib/work-context/atomic.js +132 -0
- package/dist/lib/work-context/company.d.ts +103 -0
- package/dist/lib/work-context/company.js +274 -0
- package/dist/lib/work-context/config.d.ts +59 -0
- package/dist/lib/work-context/config.js +172 -0
- package/dist/lib/work-context/contract.d.ts +81 -0
- package/dist/lib/work-context/contract.js +65 -0
- package/dist/lib/work-context/errors.d.ts +27 -0
- package/dist/lib/work-context/errors.js +46 -0
- package/dist/lib/work-context/index.d.ts +23 -0
- package/dist/lib/work-context/index.js +20 -0
- package/dist/lib/work-context/migrate.d.ts +84 -0
- package/dist/lib/work-context/migrate.js +287 -0
- package/dist/lib/work-context/organize.d.ts +130 -0
- package/dist/lib/work-context/organize.js +471 -0
- package/dist/lib/work-context/outbox.d.ts +97 -0
- package/dist/lib/work-context/outbox.js +357 -0
- package/dist/lib/work-context/paths.d.ts +14 -0
- package/dist/lib/work-context/paths.js +39 -0
- package/dist/lib/work-context/project.d.ts +78 -0
- package/dist/lib/work-context/project.js +247 -0
- package/dist/lib/work-context/reconcile.d.ts +85 -0
- package/dist/lib/work-context/reconcile.js +651 -0
- package/dist/lib/work-context/repo-remote.d.ts +43 -0
- package/dist/lib/work-context/repo-remote.js +200 -0
- package/dist/lib/work-context/state.d.ts +112 -0
- package/dist/lib/work-context/state.js +162 -0
- package/dist/main.js +40 -3
- package/dist/utils/cognito-session.d.ts +85 -9
- package/dist/utils/cognito-session.js +216 -9
- package/dist/utils/qmd-model-download-error.d.ts +27 -0
- package/dist/utils/qmd-model-download-error.js +114 -0
- package/dist/utils/qmd-query-document-error.d.ts +23 -0
- package/dist/utils/qmd-query-document-error.js +117 -0
- package/dist/utils/sentry-fingerprint.js +2 -0
- package/package.json +5 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQTT presence client for the work-mesh daemon.
|
|
3
|
+
*
|
|
4
|
+
* - Connects over WSS with mqtt.js using a SigV4-presigned URL
|
|
5
|
+
* - keepalive 15s, clean session, NO last-will
|
|
6
|
+
* - Publishes retained online ONLY to own presence topics
|
|
7
|
+
* - Offline is server-only (IoT lifecycle → PresenceIngestFunction)
|
|
8
|
+
* - Never subscribes to thread topics
|
|
9
|
+
* - Reconnects with jittered backoff 1s–60s on close / network change
|
|
10
|
+
*/
|
|
11
|
+
import { type IClientOptions, type MqttClient } from "mqtt";
|
|
12
|
+
import { type Contract3Bundle, type CredentialsFetcher, type TimerHost } from "./credentials.js";
|
|
13
|
+
export type MqttConnectionState = "idle" | "connecting" | "connected" | "reconnecting" | "closed";
|
|
14
|
+
export interface PresencePayload {
|
|
15
|
+
v: 1;
|
|
16
|
+
status: "online" | "offline";
|
|
17
|
+
actorUid: string;
|
|
18
|
+
actorType: "human" | "agent";
|
|
19
|
+
at: string;
|
|
20
|
+
}
|
|
21
|
+
/** Structural mqtt client surface used by PresenceClient (test seam). */
|
|
22
|
+
export interface MeshMqttClientLike {
|
|
23
|
+
on(event: string, handler: (...args: any[]) => void): unknown;
|
|
24
|
+
publish(topic: string, message: string | Buffer, opts: {
|
|
25
|
+
qos: 0 | 1 | 2;
|
|
26
|
+
retain?: boolean;
|
|
27
|
+
}, callback?: (err?: Error | null) => void): unknown;
|
|
28
|
+
end(force?: boolean, opts?: unknown, cb?: () => void): unknown;
|
|
29
|
+
/** Guard: PresenceClient must never call subscribe. */
|
|
30
|
+
subscribe?: unknown;
|
|
31
|
+
}
|
|
32
|
+
export type MqttConnectFn = (url: string, opts: IClientOptions) => MeshMqttClientLike;
|
|
33
|
+
export interface PresenceClientOptions {
|
|
34
|
+
fetchCredentials: CredentialsFetcher;
|
|
35
|
+
mqttConnect?: MqttConnectFn;
|
|
36
|
+
timers?: TimerHost;
|
|
37
|
+
random?: () => number;
|
|
38
|
+
baseBackoffMs?: number;
|
|
39
|
+
maxBackoffMs?: number;
|
|
40
|
+
/** Invoked when online is (re)published after connect. */
|
|
41
|
+
onOnline?: (companies: string[]) => void;
|
|
42
|
+
onState?: (state: MqttConnectionState) => void;
|
|
43
|
+
onError?: (err: unknown) => void;
|
|
44
|
+
now?: () => Date;
|
|
45
|
+
}
|
|
46
|
+
/** Full-jitter capped exponential backoff (1s base → 60s cap by default). */
|
|
47
|
+
export declare function backoffDelayMs(attempt: number, baseMs: number, maxMs: number, random: () => number): number;
|
|
48
|
+
export declare function buildPresencePayload(input: {
|
|
49
|
+
status: "online" | "offline";
|
|
50
|
+
actorUid: string;
|
|
51
|
+
actorType: "human" | "agent";
|
|
52
|
+
at?: string;
|
|
53
|
+
}): PresencePayload;
|
|
54
|
+
/**
|
|
55
|
+
* Assert a topic is an own presence topic for the given actor.
|
|
56
|
+
* PresenceClient refuses to publish anything else.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isOwnPresenceTopic(topic: string, actorUid: string, companyUids: ReadonlySet<string>): boolean;
|
|
59
|
+
export declare class PresenceClient {
|
|
60
|
+
private readonly options;
|
|
61
|
+
private client;
|
|
62
|
+
private bundle;
|
|
63
|
+
private state;
|
|
64
|
+
private attempt;
|
|
65
|
+
private reconnectHandle;
|
|
66
|
+
private stopped;
|
|
67
|
+
private generation;
|
|
68
|
+
private publishedTopics;
|
|
69
|
+
private readonly mqttConnect;
|
|
70
|
+
private readonly timers;
|
|
71
|
+
private readonly random;
|
|
72
|
+
private readonly baseBackoffMs;
|
|
73
|
+
private readonly maxBackoffMs;
|
|
74
|
+
private readonly renewal;
|
|
75
|
+
private readonly now;
|
|
76
|
+
constructor(options: PresenceClientOptions);
|
|
77
|
+
getConnectionState(): MqttConnectionState;
|
|
78
|
+
getBundle(): Contract3Bundle | null;
|
|
79
|
+
/** Topics this client has published to (for doctor / tests). */
|
|
80
|
+
getPublishedTopics(): readonly string[];
|
|
81
|
+
private setState;
|
|
82
|
+
start(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Graceful stop: disconnect cleanly (MQTT DISCONNECT so the broker emits
|
|
85
|
+
* the lifecycle event immediately). Offline is published server-side —
|
|
86
|
+
* the client must not overwrite retained presence with offline.
|
|
87
|
+
*/
|
|
88
|
+
stop(): Promise<void>;
|
|
89
|
+
/** Force a reconnect cycle (network change / sleep-wake). */
|
|
90
|
+
reconnectNow(): void;
|
|
91
|
+
private teardownClient;
|
|
92
|
+
private onCredentialsRenewed;
|
|
93
|
+
private connectOnce;
|
|
94
|
+
/** Publish retained online to every own presence topic (connect / renew). */
|
|
95
|
+
private publishAll;
|
|
96
|
+
private scheduleReconnect;
|
|
97
|
+
private clearReconnect;
|
|
98
|
+
}
|
|
99
|
+
/** Production mqtt.connect wrapper (typed). */
|
|
100
|
+
export declare function defaultMqttConnect(url: string, opts: IClientOptions): MqttClient;
|
|
101
|
+
//# sourceMappingURL=presence.d.ts.map
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQTT presence client for the work-mesh daemon.
|
|
3
|
+
*
|
|
4
|
+
* - Connects over WSS with mqtt.js using a SigV4-presigned URL
|
|
5
|
+
* - keepalive 15s, clean session, NO last-will
|
|
6
|
+
* - Publishes retained online ONLY to own presence topics
|
|
7
|
+
* - Offline is server-only (IoT lifecycle → PresenceIngestFunction)
|
|
8
|
+
* - Never subscribes to thread topics
|
|
9
|
+
* - Reconnects with jittered backoff 1s–60s on close / network change
|
|
10
|
+
*/
|
|
11
|
+
import mqtt from "mqtt";
|
|
12
|
+
import { CredentialRenewalManager, MQTT_KEEPALIVE_SECONDS, realTimerHost, } from "./credentials.js";
|
|
13
|
+
import { presignIotWssUrl } from "./presign.js";
|
|
14
|
+
/** Full-jitter capped exponential backoff (1s base → 60s cap by default). */
|
|
15
|
+
export function backoffDelayMs(attempt, baseMs, maxMs, random) {
|
|
16
|
+
const cap = Math.min(maxMs, baseMs * 2 ** attempt);
|
|
17
|
+
return Math.max(0, random() * cap);
|
|
18
|
+
}
|
|
19
|
+
export function buildPresencePayload(input) {
|
|
20
|
+
return {
|
|
21
|
+
v: 1,
|
|
22
|
+
status: input.status,
|
|
23
|
+
actorUid: input.actorUid,
|
|
24
|
+
actorType: input.actorType,
|
|
25
|
+
at: input.at ?? new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Assert a topic is an own presence topic for the given actor.
|
|
30
|
+
* PresenceClient refuses to publish anything else.
|
|
31
|
+
*/
|
|
32
|
+
export function isOwnPresenceTopic(topic, actorUid, companyUids) {
|
|
33
|
+
const m = /^hq\/([^/]+)\/presence\/([^/]+)$/.exec(topic);
|
|
34
|
+
if (!m)
|
|
35
|
+
return false;
|
|
36
|
+
const companyUid = m[1];
|
|
37
|
+
const topicActor = m[2];
|
|
38
|
+
return topicActor === actorUid && companyUids.has(companyUid);
|
|
39
|
+
}
|
|
40
|
+
export class PresenceClient {
|
|
41
|
+
options;
|
|
42
|
+
client = null;
|
|
43
|
+
bundle = null;
|
|
44
|
+
state = "idle";
|
|
45
|
+
attempt = 0;
|
|
46
|
+
reconnectHandle = null;
|
|
47
|
+
stopped = false;
|
|
48
|
+
generation = 0;
|
|
49
|
+
publishedTopics = [];
|
|
50
|
+
mqttConnect;
|
|
51
|
+
timers;
|
|
52
|
+
random;
|
|
53
|
+
baseBackoffMs;
|
|
54
|
+
maxBackoffMs;
|
|
55
|
+
renewal;
|
|
56
|
+
now;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.options = options;
|
|
59
|
+
this.mqttConnect =
|
|
60
|
+
options.mqttConnect ??
|
|
61
|
+
((url, opts) => mqtt.connect(url, opts));
|
|
62
|
+
this.timers = options.timers ?? realTimerHost;
|
|
63
|
+
this.random = options.random ?? Math.random;
|
|
64
|
+
this.baseBackoffMs = options.baseBackoffMs ?? 1_000;
|
|
65
|
+
this.maxBackoffMs = options.maxBackoffMs ?? 60_000;
|
|
66
|
+
this.now = options.now ?? (() => new Date());
|
|
67
|
+
this.renewal = new CredentialRenewalManager(options.fetchCredentials, (bundle) => void this.onCredentialsRenewed(bundle), (err) => options.onError?.(err), this.timers);
|
|
68
|
+
}
|
|
69
|
+
getConnectionState() {
|
|
70
|
+
return this.state;
|
|
71
|
+
}
|
|
72
|
+
getBundle() {
|
|
73
|
+
return this.bundle;
|
|
74
|
+
}
|
|
75
|
+
/** Topics this client has published to (for doctor / tests). */
|
|
76
|
+
getPublishedTopics() {
|
|
77
|
+
return this.publishedTopics;
|
|
78
|
+
}
|
|
79
|
+
setState(state) {
|
|
80
|
+
if (this.state === state)
|
|
81
|
+
return;
|
|
82
|
+
this.state = state;
|
|
83
|
+
this.options.onState?.(state);
|
|
84
|
+
}
|
|
85
|
+
async start() {
|
|
86
|
+
if (this.stopped)
|
|
87
|
+
throw new Error("PresenceClient already stopped");
|
|
88
|
+
this.setState("connecting");
|
|
89
|
+
await this.connectOnce();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Graceful stop: disconnect cleanly (MQTT DISCONNECT so the broker emits
|
|
93
|
+
* the lifecycle event immediately). Offline is published server-side —
|
|
94
|
+
* the client must not overwrite retained presence with offline.
|
|
95
|
+
*/
|
|
96
|
+
async stop() {
|
|
97
|
+
this.stopped = true;
|
|
98
|
+
this.generation += 1;
|
|
99
|
+
this.renewal.stop();
|
|
100
|
+
this.clearReconnect();
|
|
101
|
+
this.teardownClient(false);
|
|
102
|
+
this.setState("closed");
|
|
103
|
+
}
|
|
104
|
+
/** Force a reconnect cycle (network change / sleep-wake). */
|
|
105
|
+
reconnectNow() {
|
|
106
|
+
if (this.stopped)
|
|
107
|
+
return;
|
|
108
|
+
this.attempt = 0;
|
|
109
|
+
this.clearReconnect();
|
|
110
|
+
void this.connectOnce();
|
|
111
|
+
}
|
|
112
|
+
teardownClient(force) {
|
|
113
|
+
const client = this.client;
|
|
114
|
+
if (!client)
|
|
115
|
+
return;
|
|
116
|
+
this.client = null;
|
|
117
|
+
try {
|
|
118
|
+
if (force) {
|
|
119
|
+
client.end(true);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
client.end(false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* best-effort */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async onCredentialsRenewed(bundle) {
|
|
130
|
+
this.bundle = bundle;
|
|
131
|
+
if (this.stopped)
|
|
132
|
+
return;
|
|
133
|
+
// Reconnect with the new presigned URL, then re-publish online.
|
|
134
|
+
this.attempt = 0;
|
|
135
|
+
await this.connectOnce();
|
|
136
|
+
}
|
|
137
|
+
async connectOnce() {
|
|
138
|
+
if (this.stopped)
|
|
139
|
+
return;
|
|
140
|
+
this.clearReconnect();
|
|
141
|
+
const generation = ++this.generation;
|
|
142
|
+
this.teardownClient(true);
|
|
143
|
+
try {
|
|
144
|
+
if (!this.bundle) {
|
|
145
|
+
this.bundle = await this.options.fetchCredentials();
|
|
146
|
+
this.renewal.schedule(this.bundle);
|
|
147
|
+
}
|
|
148
|
+
const bundle = this.bundle;
|
|
149
|
+
const url = presignIotWssUrl(bundle.credentials, bundle.iotEndpoint, bundle.region, this.now());
|
|
150
|
+
if (this.stopped || generation !== this.generation)
|
|
151
|
+
return;
|
|
152
|
+
let client;
|
|
153
|
+
try {
|
|
154
|
+
client = this.mqttConnect(url, {
|
|
155
|
+
clientId: bundle.clientId,
|
|
156
|
+
clean: true,
|
|
157
|
+
reconnectPeriod: 0,
|
|
158
|
+
keepalive: MQTT_KEEPALIVE_SECONDS,
|
|
159
|
+
protocolVersion: 4,
|
|
160
|
+
// No last-will: one connection serves several companies.
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
this.options.onError?.(err);
|
|
165
|
+
this.scheduleReconnect();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.client = client;
|
|
169
|
+
client.on("connect", () => {
|
|
170
|
+
if (this.stopped || generation !== this.generation)
|
|
171
|
+
return;
|
|
172
|
+
this.attempt = 0;
|
|
173
|
+
this.setState("connected");
|
|
174
|
+
void this.publishAll("online").then(() => {
|
|
175
|
+
this.options.onOnline?.(bundle.companies.map((c) => c.companyUid));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
client.on("error", (err) => {
|
|
179
|
+
if (this.stopped || generation !== this.generation)
|
|
180
|
+
return;
|
|
181
|
+
this.options.onError?.(err);
|
|
182
|
+
});
|
|
183
|
+
client.on("close", () => {
|
|
184
|
+
if (this.stopped || generation !== this.generation)
|
|
185
|
+
return;
|
|
186
|
+
this.scheduleReconnect();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
this.options.onError?.(err);
|
|
191
|
+
this.scheduleReconnect();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Publish retained online to every own presence topic (connect / renew). */
|
|
195
|
+
async publishAll(status) {
|
|
196
|
+
const bundle = this.bundle;
|
|
197
|
+
const client = this.client;
|
|
198
|
+
if (!bundle || !client)
|
|
199
|
+
return;
|
|
200
|
+
const companyUids = new Set(bundle.companies.map((c) => c.companyUid));
|
|
201
|
+
const payload = JSON.stringify(buildPresencePayload({
|
|
202
|
+
status,
|
|
203
|
+
actorUid: bundle.actorUid,
|
|
204
|
+
actorType: bundle.actorType,
|
|
205
|
+
at: this.now().toISOString(),
|
|
206
|
+
}));
|
|
207
|
+
for (const company of bundle.companies) {
|
|
208
|
+
const topic = company.presenceTopic;
|
|
209
|
+
if (!isOwnPresenceTopic(topic, bundle.actorUid, companyUids)) {
|
|
210
|
+
this.options.onError?.(new Error(`refusing to publish to non-presence topic`));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
await new Promise((resolve) => {
|
|
214
|
+
try {
|
|
215
|
+
client.publish(topic, payload, { qos: 1, retain: true }, () => resolve());
|
|
216
|
+
if (!this.publishedTopics.includes(topic)) {
|
|
217
|
+
this.publishedTopics.push(topic);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
this.options.onError?.(err);
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
scheduleReconnect() {
|
|
228
|
+
if (this.stopped)
|
|
229
|
+
return;
|
|
230
|
+
this.setState("reconnecting");
|
|
231
|
+
this.clearReconnect();
|
|
232
|
+
const delay = backoffDelayMs(this.attempt, this.baseBackoffMs, this.maxBackoffMs, this.random);
|
|
233
|
+
this.attempt += 1;
|
|
234
|
+
this.reconnectHandle = this.timers.setTimeout(() => {
|
|
235
|
+
this.reconnectHandle = null;
|
|
236
|
+
void this.connectOnce();
|
|
237
|
+
}, delay);
|
|
238
|
+
}
|
|
239
|
+
clearReconnect() {
|
|
240
|
+
if (this.reconnectHandle !== null) {
|
|
241
|
+
this.timers.clearTimeout(this.reconnectHandle);
|
|
242
|
+
this.reconnectHandle = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Production mqtt.connect wrapper (typed). */
|
|
247
|
+
export function defaultMqttConnect(url, opts) {
|
|
248
|
+
return mqtt.connect(url, opts);
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=presence.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SigV4 query-presign of wss://{endpoint}/mqtt for AWS IoT Core.
|
|
3
|
+
*
|
|
4
|
+
* Approach mirrors hq-desktop-app packages/core/src/mesh/presign.ts:
|
|
5
|
+
* sign WITHOUT X-Amz-Security-Token, then append the session token after.
|
|
6
|
+
*/
|
|
7
|
+
export interface IotCredentials {
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
sessionToken: string;
|
|
11
|
+
expiration?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function hex(bytes: Buffer | Uint8Array): string;
|
|
14
|
+
export declare function rfc3986Encode(value: string): string;
|
|
15
|
+
export declare function amzDateOf(now: Date): string;
|
|
16
|
+
/** Presign a WSS MQTT URL for IoT Core using STS credentials. */
|
|
17
|
+
export declare function presignIotWssUrl(creds: IotCredentials, endpoint: string, region: string, now?: Date): string;
|
|
18
|
+
//# sourceMappingURL=presign.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SigV4 query-presign of wss://{endpoint}/mqtt for AWS IoT Core.
|
|
3
|
+
*
|
|
4
|
+
* Approach mirrors hq-desktop-app packages/core/src/mesh/presign.ts:
|
|
5
|
+
* sign WITHOUT X-Amz-Security-Token, then append the session token after.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, createHmac } from "node:crypto";
|
|
8
|
+
const IOT_SERVICE = "iotdevicegateway";
|
|
9
|
+
const CANONICAL_URI = "/mqtt";
|
|
10
|
+
const ALGORITHM = "AWS4-HMAC-SHA256";
|
|
11
|
+
export function hex(bytes) {
|
|
12
|
+
return Buffer.from(bytes).toString("hex");
|
|
13
|
+
}
|
|
14
|
+
export function rfc3986Encode(value) {
|
|
15
|
+
return encodeURIComponent(value).replace(/[!*'()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
16
|
+
}
|
|
17
|
+
export function amzDateOf(now) {
|
|
18
|
+
return now
|
|
19
|
+
.toISOString()
|
|
20
|
+
.replace(/[:-]|\.\d{3}/g, "")
|
|
21
|
+
.replace(/Z?$/, "Z");
|
|
22
|
+
}
|
|
23
|
+
function hmac(key, data) {
|
|
24
|
+
return createHmac("sha256", key).update(data, "utf8").digest();
|
|
25
|
+
}
|
|
26
|
+
function sha256Hex(data) {
|
|
27
|
+
return createHash("sha256").update(data, "utf8").digest("hex");
|
|
28
|
+
}
|
|
29
|
+
/** Presign a WSS MQTT URL for IoT Core using STS credentials. */
|
|
30
|
+
export function presignIotWssUrl(creds, endpoint, region, now = new Date()) {
|
|
31
|
+
const amzDate = amzDateOf(now);
|
|
32
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
33
|
+
const scope = `${dateStamp}/${region}/${IOT_SERVICE}/aws4_request`;
|
|
34
|
+
const canonicalQuery = `X-Amz-Algorithm=${rfc3986Encode(ALGORITHM)}` +
|
|
35
|
+
`&X-Amz-Credential=${rfc3986Encode(`${creds.accessKeyId}/${scope}`)}` +
|
|
36
|
+
`&X-Amz-Date=${rfc3986Encode(amzDate)}` +
|
|
37
|
+
`&X-Amz-SignedHeaders=host`;
|
|
38
|
+
const canonicalHeaders = `host:${endpoint}\n`;
|
|
39
|
+
const canonicalRequest = [
|
|
40
|
+
"GET",
|
|
41
|
+
CANONICAL_URI,
|
|
42
|
+
canonicalQuery,
|
|
43
|
+
canonicalHeaders,
|
|
44
|
+
"host",
|
|
45
|
+
sha256Hex(""),
|
|
46
|
+
].join("\n");
|
|
47
|
+
const stringToSign = [
|
|
48
|
+
ALGORITHM,
|
|
49
|
+
amzDate,
|
|
50
|
+
scope,
|
|
51
|
+
sha256Hex(canonicalRequest),
|
|
52
|
+
].join("\n");
|
|
53
|
+
const kDate = hmac(`AWS4${creds.secretAccessKey}`, dateStamp);
|
|
54
|
+
const kRegion = hmac(kDate, region);
|
|
55
|
+
const kService = hmac(kRegion, IOT_SERVICE);
|
|
56
|
+
const kSigning = hmac(kService, "aws4_request");
|
|
57
|
+
const signature = hex(hmac(kSigning, stringToSign));
|
|
58
|
+
let url = `wss://${endpoint}${CANONICAL_URI}?${canonicalQuery}` +
|
|
59
|
+
`&X-Amz-Signature=${signature}`;
|
|
60
|
+
if (creds.sessionToken) {
|
|
61
|
+
url += `&X-Amz-Security-Token=${rfc3986Encode(creds.sessionToken)}`;
|
|
62
|
+
}
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=presign.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq mesh daemon run — single-instance resident process.
|
|
3
|
+
*
|
|
4
|
+
* - pid lock at ~/.hq/work-mesh/daemon/pid
|
|
5
|
+
* - fs.watch spool + 2s debounce; flush at least every 10s (US-008 path)
|
|
6
|
+
* - replay context outbox
|
|
7
|
+
* - board.md refresh for bound sessions ≤ every 60s
|
|
8
|
+
* - MQTT presence (contract-3)
|
|
9
|
+
* - SIGTERM/SIGINT: flush once more, MQTT DISCONNECT (server derives offline), exit
|
|
10
|
+
*/
|
|
11
|
+
import { type FlushSummary } from "../flush.js";
|
|
12
|
+
import { type CredentialsFetcher, type TimerHost } from "./credentials.js";
|
|
13
|
+
import { type PidLockDeps } from "./pid-lock.js";
|
|
14
|
+
import { PresenceClient, type MqttConnectFn } from "./presence.js";
|
|
15
|
+
import { TranscriptWatcher, type TranscriptFs } from "./transcript-watch.js";
|
|
16
|
+
export declare const SPOOL_DEBOUNCE_MS = 2000;
|
|
17
|
+
export declare const FLUSH_INTERVAL_MS = 10000;
|
|
18
|
+
export interface DaemonRunDeps {
|
|
19
|
+
home?: string;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
meshRoot?: string;
|
|
22
|
+
workContextRoot?: string;
|
|
23
|
+
pidLock?: PidLockDeps;
|
|
24
|
+
timers?: TimerHost;
|
|
25
|
+
fetchCredentials?: CredentialsFetcher;
|
|
26
|
+
mqttConnect?: MqttConnectFn;
|
|
27
|
+
/** Injected flush (tests). */
|
|
28
|
+
flush?: () => Promise<FlushSummary>;
|
|
29
|
+
/** Injected outbox replay (tests). */
|
|
30
|
+
replayOutbox?: () => Promise<{
|
|
31
|
+
delivered: number;
|
|
32
|
+
queued: number;
|
|
33
|
+
quarantined: number;
|
|
34
|
+
}>;
|
|
35
|
+
/** Injected board refresh (tests). */
|
|
36
|
+
refreshBoards?: () => Promise<{
|
|
37
|
+
refreshed: number;
|
|
38
|
+
skipped: number;
|
|
39
|
+
errors: number;
|
|
40
|
+
}>;
|
|
41
|
+
/** When false, skip MQTT (flush-only mode for tests). */
|
|
42
|
+
enablePresence?: boolean;
|
|
43
|
+
/** When false, skip transcript-watch lane (tests). Default true. */
|
|
44
|
+
enableTranscriptWatch?: boolean;
|
|
45
|
+
/** Injectable transcript fs (tests). */
|
|
46
|
+
transcriptFs?: TranscriptFs;
|
|
47
|
+
/** HQ tree root for deterministic cwd→company/project mapping. */
|
|
48
|
+
hqRoot?: string;
|
|
49
|
+
/** Process signal registration seam. */
|
|
50
|
+
processEvents?: Pick<NodeJS.Process, "once" | "on" | "off">;
|
|
51
|
+
exit?: (code: number) => void;
|
|
52
|
+
/** Token provider (tests). */
|
|
53
|
+
getToken?: () => Promise<string>;
|
|
54
|
+
log?: (dir: string, line: string) => void;
|
|
55
|
+
/** Stop after this many ms (tests). */
|
|
56
|
+
runForMs?: number;
|
|
57
|
+
random?: () => number;
|
|
58
|
+
now?: () => Date;
|
|
59
|
+
}
|
|
60
|
+
export interface DaemonHandle {
|
|
61
|
+
stop: () => Promise<void>;
|
|
62
|
+
dir: string;
|
|
63
|
+
presence: PresenceClient | null;
|
|
64
|
+
/** Hook-activity map shared with the transcript watcher (US-018). */
|
|
65
|
+
lastHookEventAt: Map<string, number>;
|
|
66
|
+
/** Transcript watcher instance when enabled. */
|
|
67
|
+
transcriptWatcher: TranscriptWatcher | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Start the daemon. Resolves once the process is running (lock held, loops armed).
|
|
71
|
+
* Call handle.stop() or send SIGTERM for graceful shutdown.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runMeshDaemon(deps?: DaemonRunDeps): Promise<DaemonHandle>;
|
|
74
|
+
//# sourceMappingURL=run.d.ts.map
|