@indigoai-us/hq-cli 5.345.178 → 5.345.180
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 +39 -0
- package/dist/command-catalog.generated.d.ts +197 -1
- package/dist/command-catalog.generated.js +241 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +37 -1
- package/dist/commands/bot.js +289 -13
- package/dist/commands/channels.d.ts +18 -1
- package/dist/commands/channels.js +185 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +6 -2
- package/dist/commands/mesh.js +19 -0
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/config.d.ts +8 -0
- package/dist/lib/bot/fleet-local.d.ts +1 -1
- package/dist/lib/bot/fleet-local.js +82 -30
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +99 -1
- package/dist/lib/bot/runtime-auth.d.ts +119 -0
- package/dist/lib/bot/runtime-auth.js +251 -0
- package/dist/lib/daemon/catalog.d.ts +1 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +5 -2
- package/dist/lib/daemon/run.js +74 -22
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +525 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +146 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +203 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +221 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/accounts.d.ts +1 -17
- package/dist/lib/lanes/accounts.js +5 -45
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- package/dist/lib/lanes/provider-home.d.ts +24 -0
- package/dist/lib/lanes/provider-home.js +52 -0
- package/dist/lib/lanes/spawn.d.ts +12 -0
- package/dist/lib/lanes/spawn.js +27 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +13 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
- package/dist/lib/mesh/live/daemon/run.js +31 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/sync-core/flag.d.ts +1 -1
- package/dist/lib/sync-core/flag.js +1 -1
- package/dist/lib/sync-core/reconcile.d.ts +1 -1
- package/dist/lib/sync-core/reconcile.js +9 -7
- package/dist/lib/sync-core/release.d.ts +3 -3
- package/dist/lib/sync-core/release.js +4 -4
- package/dist/lib/sync-core/rollout-state.d.ts +1 -0
- package/dist/lib/sync-core/rollout-state.js +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner (prs_) DM doorbell state for the mesh daemon.
|
|
3
|
+
*
|
|
4
|
+
* The server publishes a wake to `hq/{prs}/dm` for each DM, thread reply and
|
|
5
|
+
* channel post the owner should see. The daemon reads only `type`,
|
|
6
|
+
* `channelId` and `eventId` from that payload, keeps a channel event only when
|
|
7
|
+
* the channel belongs to an allowed company, and bumps a content-free counter
|
|
8
|
+
* in `owner-inbox-push.json`. Message text, poster name and email are never
|
|
9
|
+
* stored, logged or forwarded.
|
|
10
|
+
*/
|
|
11
|
+
export { OWNER_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
|
|
12
|
+
/** Heartbeat cadence while the owner DM topic is subscribed. */
|
|
13
|
+
export declare const OWNER_PUSH_HEARTBEAT_MS = 5000;
|
|
14
|
+
/** A heartbeat older than this means the push signal is stale. */
|
|
15
|
+
export declare const OWNER_PUSH_STALE_MS = 15000;
|
|
16
|
+
/** Channel list refreshes at most this often when an unknown channel arrives. */
|
|
17
|
+
export declare const OWNER_CHANNEL_REFRESH_MIN_MS = 60000;
|
|
18
|
+
/** A channel-list fetch that runs longer than this is abandoned. */
|
|
19
|
+
export declare const OWNER_CHANNEL_REFRESH_TIMEOUT_MS = 10000;
|
|
20
|
+
export type OwnerDoorbellType = "dm" | "thread" | "channel";
|
|
21
|
+
export interface OwnerDoorbell {
|
|
22
|
+
type: OwnerDoorbellType;
|
|
23
|
+
channelId?: string;
|
|
24
|
+
eventId?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Every key the state file may contain. Nothing else is ever written. */
|
|
27
|
+
export declare const OWNER_INBOX_PUSH_STATE_KEYS: readonly ["connected", "heartbeatAtMs", "count", "lastAt", "lastType", "principalUid"];
|
|
28
|
+
export interface OwnerInboxPushState {
|
|
29
|
+
connected: boolean;
|
|
30
|
+
heartbeatAtMs: number;
|
|
31
|
+
count: number;
|
|
32
|
+
lastAt?: string;
|
|
33
|
+
lastType?: OwnerDoorbellType;
|
|
34
|
+
principalUid?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read only `type`, `channelId` and `eventId`. Returns null for anything that
|
|
38
|
+
* is not a dm, thread or channel wake. Other payload fields are discarded.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseOwnerDoorbell(payload: Buffer | string): OwnerDoorbell | null;
|
|
41
|
+
/** Read the state file. Missing, non-regular or malformed files return null. */
|
|
42
|
+
export declare function readOwnerInboxPushState(meshRoot: string, onInfo?: (message: string) => void): OwnerInboxPushState | null;
|
|
43
|
+
/** Atomic write (temp file plus rename), mode 0600, allowed keys only. */
|
|
44
|
+
export declare function writeOwnerInboxPushState(meshRoot: string, state: OwnerInboxPushState): void;
|
|
45
|
+
/**
|
|
46
|
+
* Remove the state file. Used when the account is not a person account, so a
|
|
47
|
+
* file left by an earlier person login cannot report the push as stale.
|
|
48
|
+
*/
|
|
49
|
+
export declare function clearOwnerInboxPushState(meshRoot: string): void;
|
|
50
|
+
export type OwnerPushStatusKind = "live" | "stale" | "off";
|
|
51
|
+
export interface OwnerPushStatus {
|
|
52
|
+
status: OwnerPushStatusKind;
|
|
53
|
+
connected: boolean;
|
|
54
|
+
count: number;
|
|
55
|
+
lastAt: string | null;
|
|
56
|
+
lastType: OwnerDoorbellType | null;
|
|
57
|
+
heartbeatAt: string | null;
|
|
58
|
+
heartbeatAgeMs: number | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* live: connected with a fresh heartbeat. stale: the file exists but the
|
|
62
|
+
* subscriber is disconnected or its heartbeat is older than the stale window.
|
|
63
|
+
* off: no state file (the daemon never subscribed on this machine).
|
|
64
|
+
*/
|
|
65
|
+
export declare function ownerPushStatus(state: OwnerInboxPushState | null, nowMs: number, staleMs?: number): OwnerPushStatus;
|
|
66
|
+
/**
|
|
67
|
+
* Status for the signed-in account. Only a person account has an owner DM
|
|
68
|
+
* topic, so any other account kind (agent, outpost, or no login) reports off.
|
|
69
|
+
*/
|
|
70
|
+
export declare function ownerPushStatusForAccount(state: OwnerInboxPushState | null, nowMs: number, actorKind: string): OwnerPushStatus;
|
|
71
|
+
export declare function formatOwnerPushStatus(status: OwnerPushStatus): string;
|
|
72
|
+
export interface ChannelCompanyRow {
|
|
73
|
+
channelId: string;
|
|
74
|
+
companyUid?: string;
|
|
75
|
+
}
|
|
76
|
+
export interface OwnerChannelScope {
|
|
77
|
+
/** True only when the channel is known and its company is allowed. */
|
|
78
|
+
isInScope(channelId: string): Promise<boolean>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Channel filter backed by the owner's channel list. A channel with no
|
|
82
|
+
* company, an unknown channel, or a company outside the allowed set is
|
|
83
|
+
* dropped. The allowed set is read on every check so a session lock change
|
|
84
|
+
* applies at once. The list refreshes at most once per refresh window.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createOwnerChannelScope(opts: {
|
|
87
|
+
listChannels: () => Promise<ChannelCompanyRow[]>;
|
|
88
|
+
allowedCompanyUids: () => readonly string[] | undefined;
|
|
89
|
+
now?: () => number;
|
|
90
|
+
refreshMinMs?: number;
|
|
91
|
+
refreshTimeoutMs?: number;
|
|
92
|
+
onInfo?: (message: string) => void;
|
|
93
|
+
}): OwnerChannelScope;
|
|
94
|
+
//# sourceMappingURL=owner-inbox-push.d.ts.map
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner (prs_) DM doorbell state for the mesh daemon.
|
|
3
|
+
*
|
|
4
|
+
* The server publishes a wake to `hq/{prs}/dm` for each DM, thread reply and
|
|
5
|
+
* channel post the owner should see. The daemon reads only `type`,
|
|
6
|
+
* `channelId` and `eventId` from that payload, keeps a channel event only when
|
|
7
|
+
* the channel belongs to an allowed company, and bumps a content-free counter
|
|
8
|
+
* in `owner-inbox-push.json`. Message text, poster name and email are never
|
|
9
|
+
* stored, logged or forwarded.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { OWNER_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
|
|
14
|
+
export { OWNER_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
|
|
15
|
+
/** Heartbeat cadence while the owner DM topic is subscribed. */
|
|
16
|
+
export const OWNER_PUSH_HEARTBEAT_MS = 5_000;
|
|
17
|
+
/** A heartbeat older than this means the push signal is stale. */
|
|
18
|
+
export const OWNER_PUSH_STALE_MS = 15_000;
|
|
19
|
+
/** Channel list refreshes at most this often when an unknown channel arrives. */
|
|
20
|
+
export const OWNER_CHANNEL_REFRESH_MIN_MS = 60_000;
|
|
21
|
+
/** A channel-list fetch that runs longer than this is abandoned. */
|
|
22
|
+
export const OWNER_CHANNEL_REFRESH_TIMEOUT_MS = 10_000;
|
|
23
|
+
/** Every key the state file may contain. Nothing else is ever written. */
|
|
24
|
+
export const OWNER_INBOX_PUSH_STATE_KEYS = [
|
|
25
|
+
"connected",
|
|
26
|
+
"heartbeatAtMs",
|
|
27
|
+
"count",
|
|
28
|
+
"lastAt",
|
|
29
|
+
"lastType",
|
|
30
|
+
"principalUid",
|
|
31
|
+
];
|
|
32
|
+
const CHANNEL_ID_RE = /^chn_[A-Za-z0-9]+$/;
|
|
33
|
+
const EVENT_ID_RE = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
34
|
+
/**
|
|
35
|
+
* Read only `type`, `channelId` and `eventId`. Returns null for anything that
|
|
36
|
+
* is not a dm, thread or channel wake. Other payload fields are discarded.
|
|
37
|
+
*/
|
|
38
|
+
export function parseOwnerDoorbell(payload) {
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(typeof payload === "string" ? payload : payload.toString("utf8"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
47
|
+
return null;
|
|
48
|
+
const record = raw;
|
|
49
|
+
const type = record.type;
|
|
50
|
+
if (type !== "dm" && type !== "thread" && type !== "channel")
|
|
51
|
+
return null;
|
|
52
|
+
const out = { type };
|
|
53
|
+
if (typeof record.channelId === "string" && CHANNEL_ID_RE.test(record.channelId)) {
|
|
54
|
+
out.channelId = record.channelId;
|
|
55
|
+
}
|
|
56
|
+
if (typeof record.eventId === "string" && EVENT_ID_RE.test(record.eventId)) {
|
|
57
|
+
out.eventId = record.eventId;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function ownerStatePath(meshRoot) {
|
|
62
|
+
return path.join(meshRoot, OWNER_INBOX_PUSH_STATE_BASENAME);
|
|
63
|
+
}
|
|
64
|
+
/** Read the state file. Missing, non-regular or malformed files return null. */
|
|
65
|
+
export function readOwnerInboxPushState(meshRoot, onInfo) {
|
|
66
|
+
const target = ownerStatePath(meshRoot);
|
|
67
|
+
try {
|
|
68
|
+
const info = fs.lstatSync(target);
|
|
69
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
70
|
+
onInfo?.("owner inbox push state read ignored (not a regular file)");
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
74
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
75
|
+
return null;
|
|
76
|
+
const count = Number.isSafeInteger(parsed.count) && Number(parsed.count) >= 0
|
|
77
|
+
? Number(parsed.count)
|
|
78
|
+
: 0;
|
|
79
|
+
const heartbeatAtMs = Number.isFinite(parsed.heartbeatAtMs)
|
|
80
|
+
? Number(parsed.heartbeatAtMs)
|
|
81
|
+
: 0;
|
|
82
|
+
const state = {
|
|
83
|
+
connected: parsed.connected === true,
|
|
84
|
+
heartbeatAtMs,
|
|
85
|
+
count,
|
|
86
|
+
};
|
|
87
|
+
if (typeof parsed.lastAt === "string" && Number.isFinite(Date.parse(parsed.lastAt))) {
|
|
88
|
+
state.lastAt = parsed.lastAt;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.lastType === "dm" || parsed.lastType === "thread" || parsed.lastType === "channel") {
|
|
91
|
+
state.lastType = parsed.lastType;
|
|
92
|
+
}
|
|
93
|
+
if (typeof parsed.principalUid === "string" && parsed.principalUid.startsWith("prs_")) {
|
|
94
|
+
state.principalUid = parsed.principalUid;
|
|
95
|
+
}
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error.code !== "ENOENT") {
|
|
100
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
101
|
+
onInfo?.(`owner inbox push state read failed (${name})`);
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Atomic write (temp file plus rename), mode 0600, allowed keys only. */
|
|
107
|
+
export function writeOwnerInboxPushState(meshRoot, state) {
|
|
108
|
+
fs.mkdirSync(meshRoot, { recursive: true, mode: 0o700 });
|
|
109
|
+
const target = ownerStatePath(meshRoot);
|
|
110
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
111
|
+
const body = {
|
|
112
|
+
connected: state.connected,
|
|
113
|
+
heartbeatAtMs: state.heartbeatAtMs,
|
|
114
|
+
count: state.count,
|
|
115
|
+
};
|
|
116
|
+
if (state.lastAt)
|
|
117
|
+
body.lastAt = state.lastAt;
|
|
118
|
+
if (state.lastType)
|
|
119
|
+
body.lastType = state.lastType;
|
|
120
|
+
if (state.principalUid)
|
|
121
|
+
body.principalUid = state.principalUid;
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(temporary, `${JSON.stringify(body)}\n`, { mode: 0o600 });
|
|
124
|
+
fs.chmodSync(temporary, 0o600);
|
|
125
|
+
fs.renameSync(temporary, target);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
try {
|
|
129
|
+
fs.unlinkSync(temporary);
|
|
130
|
+
}
|
|
131
|
+
catch (cleanupError) {
|
|
132
|
+
if (cleanupError.code !== "ENOENT")
|
|
133
|
+
throw cleanupError;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Remove the state file. Used when the account is not a person account, so a
|
|
140
|
+
* file left by an earlier person login cannot report the push as stale.
|
|
141
|
+
*/
|
|
142
|
+
export function clearOwnerInboxPushState(meshRoot) {
|
|
143
|
+
try {
|
|
144
|
+
fs.unlinkSync(ownerStatePath(meshRoot));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code !== "ENOENT")
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* live: connected with a fresh heartbeat. stale: the file exists but the
|
|
153
|
+
* subscriber is disconnected or its heartbeat is older than the stale window.
|
|
154
|
+
* off: no state file (the daemon never subscribed on this machine).
|
|
155
|
+
*/
|
|
156
|
+
export function ownerPushStatus(state, nowMs, staleMs = OWNER_PUSH_STALE_MS) {
|
|
157
|
+
if (!state) {
|
|
158
|
+
return {
|
|
159
|
+
status: "off",
|
|
160
|
+
connected: false,
|
|
161
|
+
count: 0,
|
|
162
|
+
lastAt: null,
|
|
163
|
+
lastType: null,
|
|
164
|
+
heartbeatAt: null,
|
|
165
|
+
heartbeatAgeMs: null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const heartbeatAgeMs = state.heartbeatAtMs > 0 ? Math.max(0, nowMs - state.heartbeatAtMs) : null;
|
|
169
|
+
const fresh = heartbeatAgeMs !== null && heartbeatAgeMs <= staleMs;
|
|
170
|
+
return {
|
|
171
|
+
status: state.connected && fresh ? "live" : "stale",
|
|
172
|
+
connected: state.connected,
|
|
173
|
+
count: state.count,
|
|
174
|
+
lastAt: state.lastAt ?? null,
|
|
175
|
+
lastType: state.lastType ?? null,
|
|
176
|
+
heartbeatAt: state.heartbeatAtMs > 0 ? new Date(state.heartbeatAtMs).toISOString() : null,
|
|
177
|
+
heartbeatAgeMs,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Status for the signed-in account. Only a person account has an owner DM
|
|
182
|
+
* topic, so any other account kind (agent, outpost, or no login) reports off.
|
|
183
|
+
*/
|
|
184
|
+
export function ownerPushStatusForAccount(state, nowMs, actorKind) {
|
|
185
|
+
return ownerPushStatus(actorKind === "human" ? state : null, nowMs);
|
|
186
|
+
}
|
|
187
|
+
export function formatOwnerPushStatus(status) {
|
|
188
|
+
const last = status.lastAt ? `last event ${status.lastAt} (${status.lastType ?? "unknown"})` : "no events yet";
|
|
189
|
+
if (status.status === "off")
|
|
190
|
+
return "owner push: off (daemon has not subscribed on this machine)";
|
|
191
|
+
const beat = status.heartbeatAgeMs === null
|
|
192
|
+
? "no heartbeat"
|
|
193
|
+
: `heartbeat ${Math.round(status.heartbeatAgeMs / 1000)}s ago`;
|
|
194
|
+
return `owner push: ${status.status}; ${last}; count=${status.count}; ${beat}`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Channel filter backed by the owner's channel list. A channel with no
|
|
198
|
+
* company, an unknown channel, or a company outside the allowed set is
|
|
199
|
+
* dropped. The allowed set is read on every check so a session lock change
|
|
200
|
+
* applies at once. The list refreshes at most once per refresh window.
|
|
201
|
+
*/
|
|
202
|
+
export function createOwnerChannelScope(opts) {
|
|
203
|
+
const now = opts.now ?? Date.now;
|
|
204
|
+
const refreshMinMs = opts.refreshMinMs ?? OWNER_CHANNEL_REFRESH_MIN_MS;
|
|
205
|
+
const refreshTimeoutMs = opts.refreshTimeoutMs ?? OWNER_CHANNEL_REFRESH_TIMEOUT_MS;
|
|
206
|
+
let companyByChannel = new Map();
|
|
207
|
+
let lastRefreshMs = null;
|
|
208
|
+
let inflight = null;
|
|
209
|
+
const refresh = async () => {
|
|
210
|
+
if (inflight)
|
|
211
|
+
return inflight;
|
|
212
|
+
lastRefreshMs = now();
|
|
213
|
+
inflight = (async () => {
|
|
214
|
+
try {
|
|
215
|
+
let timer;
|
|
216
|
+
const timeout = new Promise((_, reject) => {
|
|
217
|
+
timer = setTimeout(() => {
|
|
218
|
+
const err = new Error("owner channel list timed out");
|
|
219
|
+
err.name = "TimeoutError";
|
|
220
|
+
reject(err);
|
|
221
|
+
}, refreshTimeoutMs);
|
|
222
|
+
timer.unref?.();
|
|
223
|
+
});
|
|
224
|
+
let rows;
|
|
225
|
+
try {
|
|
226
|
+
rows = await Promise.race([opts.listChannels(), timeout]);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
if (timer)
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
}
|
|
232
|
+
const next = new Map();
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
if (typeof row.channelId !== "string")
|
|
235
|
+
continue;
|
|
236
|
+
next.set(row.channelId, typeof row.companyUid === "string" && row.companyUid ? row.companyUid : undefined);
|
|
237
|
+
}
|
|
238
|
+
companyByChannel = next;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// Keep the previous map; an unknown channel stays out of scope.
|
|
242
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
243
|
+
opts.onInfo?.(`owner channel list refresh failed (${name})`);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
inflight = null;
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
return inflight;
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
async isInScope(channelId) {
|
|
253
|
+
let allowed;
|
|
254
|
+
try {
|
|
255
|
+
allowed = opts.allowedCompanyUids();
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
259
|
+
opts.onInfo?.(`owner allowed company read failed (${name})`);
|
|
260
|
+
allowed = undefined;
|
|
261
|
+
}
|
|
262
|
+
if (!allowed || allowed.length === 0)
|
|
263
|
+
return false;
|
|
264
|
+
if (!companyByChannel.has(channelId) &&
|
|
265
|
+
(lastRefreshMs === null || now() - lastRefreshMs >= refreshMinMs || inflight)) {
|
|
266
|
+
await refresh();
|
|
267
|
+
}
|
|
268
|
+
const companyUid = companyByChannel.get(channelId);
|
|
269
|
+
return companyUid !== undefined && allowed.includes(companyUid);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=owner-inbox-push.js.map
|
|
@@ -40,6 +40,8 @@ import { resolveRunningCliEntry } from "./install.js";
|
|
|
40
40
|
import { readDaemonConfig } from "./config.js";
|
|
41
41
|
import { retireLegacyListen } from "./legacy-listen.js";
|
|
42
42
|
import { startWorkCacheSubscriber } from "./work-subscribe.js";
|
|
43
|
+
import { createOwnerChannelScope } from "./owner-inbox-push.js";
|
|
44
|
+
import { vaultApiFetch } from "../../../../utils/vault-api.js";
|
|
43
45
|
import { capacityRoot } from "../../../capacity/ledger.js";
|
|
44
46
|
import { startCapacityScheduler, } from "../../../capacity/scheduler.js";
|
|
45
47
|
export const SPOOL_DEBOUNCE_MS = 2_000;
|
|
@@ -616,6 +618,35 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
616
618
|
now,
|
|
617
619
|
onInfo: (message) => log(dir, message.slice(0, 200)),
|
|
618
620
|
random: deps.random,
|
|
621
|
+
// Channel events count only when the channel's company is in the
|
|
622
|
+
// device default or recent session company set. Unknown means drop.
|
|
623
|
+
ownerChannelScope: createOwnerChannelScope({
|
|
624
|
+
listChannels: async () => {
|
|
625
|
+
const res = await vaultApiFetch({
|
|
626
|
+
token: await getToken(),
|
|
627
|
+
path: "/v1/notify/channels",
|
|
628
|
+
baseUrl: resolveVaultApiBase(env),
|
|
629
|
+
});
|
|
630
|
+
if (!res.ok)
|
|
631
|
+
throw new Error(`channel list failed (${res.status})`);
|
|
632
|
+
const body = (await res.json());
|
|
633
|
+
if (!Array.isArray(body.channels))
|
|
634
|
+
return [];
|
|
635
|
+
return body.channels.flatMap((row) => {
|
|
636
|
+
if (!row || typeof row !== "object")
|
|
637
|
+
return [];
|
|
638
|
+
const record = row;
|
|
639
|
+
if (typeof record.channelId !== "string")
|
|
640
|
+
return [];
|
|
641
|
+
return [{
|
|
642
|
+
channelId: record.channelId,
|
|
643
|
+
...(typeof record.companyUid === "string" ? { companyUid: record.companyUid } : {}),
|
|
644
|
+
}];
|
|
645
|
+
});
|
|
646
|
+
},
|
|
647
|
+
allowedCompanyUids: () => readPreferredCompanyUids({ workContextRoot: ctxRoot }),
|
|
648
|
+
onInfo: (message) => log(dir, message.slice(0, 200)),
|
|
649
|
+
}),
|
|
619
650
|
});
|
|
620
651
|
}
|
|
621
652
|
catch (err) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contract-2 subscriber for the personal work topic.
|
|
3
3
|
* Owns live-cache.json writes. Presence publish stays on PresenceClient.
|
|
4
|
+
* On a person (prs_) account it also subscribes to the owner DM topic and
|
|
5
|
+
* keeps the content-free owner-inbox-push.json counter.
|
|
4
6
|
*/
|
|
5
7
|
import type { IClientOptions } from "mqtt";
|
|
6
8
|
import type { MeshMqttEventClient } from "./mqtt-events.js";
|
|
7
9
|
import { type PersonalCredentialsFetcher } from "./credentials.js";
|
|
10
|
+
import { type OwnerChannelScope } from "./owner-inbox-push.js";
|
|
8
11
|
export interface WorkCacheMqtt extends MeshMqttEventClient {
|
|
9
12
|
subscribe(topic: string, opts: {
|
|
10
13
|
qos: 0 | 1;
|
|
@@ -19,6 +22,12 @@ export type SyncWakeVaults = Record<string, {
|
|
|
19
22
|
wakeSequence: number;
|
|
20
23
|
head?: number;
|
|
21
24
|
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Owner DM subscribe retries per connection. The owner signal is best-effort:
|
|
27
|
+
* after this many failures it stays off until the next broker reconnect, and
|
|
28
|
+
* the work topic keeps running.
|
|
29
|
+
*/
|
|
30
|
+
export declare const OWNER_SUBSCRIBE_MAX_RETRIES = 6;
|
|
22
31
|
export declare function startWorkCacheSubscriber(opts: {
|
|
23
32
|
meshRoot: string;
|
|
24
33
|
fetchCredentials?: PersonalCredentialsFetcher;
|
|
@@ -32,5 +41,10 @@ export declare function startWorkCacheSubscriber(opts: {
|
|
|
32
41
|
};
|
|
33
42
|
onInfo?: (message: string) => void;
|
|
34
43
|
random?: () => number;
|
|
44
|
+
/**
|
|
45
|
+
* Company filter for channel events on the owner DM topic. Without it every
|
|
46
|
+
* event that names a channel is dropped.
|
|
47
|
+
*/
|
|
48
|
+
ownerChannelScope?: OwnerChannelScope;
|
|
35
49
|
}): Promise<WorkCacheSubscriber>;
|
|
36
50
|
//# sourceMappingURL=work-subscribe.d.ts.map
|