@indigoai-us/hq-cli 5.127.0 → 5.129.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.
@@ -0,0 +1,310 @@
1
+ import { ensureCognitoToken, } from "../utils/cognito-session.js";
2
+ import { vaultApiFetch } from "../utils/vault-api.js";
3
+ import { listReportTemplates } from "../lib/report-templates.js";
4
+ import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext, ReportPublishError, plainFailureReason, publishReport, } from "../lib/report-publish.js";
5
+ const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL ?? "https://api.indigo-hq.com";
6
+ function reportPath(channelId, reportId) {
7
+ return `/v1/notify/channels/${encodeURIComponent(channelId)}/reports/${encodeURIComponent(reportId)}`;
8
+ }
9
+ function errorMessage(error) {
10
+ if (error instanceof ReportPublishError)
11
+ return plainFailureReason(error);
12
+ return "Report command failed. Check the report context and try again.";
13
+ }
14
+ function print(value, json) {
15
+ if (json)
16
+ console.log(JSON.stringify(value, null, 2));
17
+ else
18
+ console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
19
+ }
20
+ /** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
21
+ export async function resolveReport(token, reportId, requestedChannelId) {
22
+ const explicit = requestedChannelId ?? process.env.HQ_REPORT_CHANNEL_ID;
23
+ if (explicit) {
24
+ const res = await vaultApiFetch({ token, path: reportPath(explicit, reportId) });
25
+ if (!res.ok)
26
+ throw new ReportPublishError("Report was not found in the supplied channel context.");
27
+ const body = await res.json();
28
+ if (!body.report)
29
+ throw new ReportPublishError("Report response was incomplete.");
30
+ return body.report;
31
+ }
32
+ // The report API is intentionally channel-scoped. Discover only among the
33
+ // caller's existing channels rather than inventing an unscoped report route.
34
+ const channels = await vaultApiFetch({ token, path: "/v1/notify/channels" });
35
+ if (!channels.ok)
36
+ throw new ReportPublishError("Could not resolve the report channel.");
37
+ const listed = await channels.json();
38
+ const matches = [];
39
+ for (const row of listed.channels ?? []) {
40
+ if (!row.channelId)
41
+ continue;
42
+ const res = await vaultApiFetch({ token, path: reportPath(row.channelId, reportId) });
43
+ if (res.status === 404)
44
+ continue;
45
+ if (!res.ok)
46
+ continue;
47
+ const body = await res.json();
48
+ if (body.report)
49
+ matches.push(body.report);
50
+ }
51
+ if (matches.length === 1)
52
+ return matches[0];
53
+ if (matches.length > 1)
54
+ throw new ReportPublishError("Report ID is ambiguous; pass --channel or set HQ_REPORT_CHANNEL_ID.");
55
+ throw new ReportPublishError("Report not found. Pass --channel or set HQ_REPORT_CHANNEL_ID.");
56
+ }
57
+ export async function patchReport(token, report, patch) {
58
+ const res = await vaultApiFetch({ token, path: reportPath(report.channelId, report.reportId), method: "PATCH", body: patch });
59
+ if (res.status === 409) {
60
+ const body = await res.json().catch(() => ({}));
61
+ if (body.error === "already-running")
62
+ throw new ReportPublishError("already running", "already-running");
63
+ throw new ReportPublishError("Report changed while it was being updated.");
64
+ }
65
+ if (!res.ok)
66
+ throw new ReportPublishError("Could not update the report record.");
67
+ const body = await res.json();
68
+ if (!body.report)
69
+ throw new ReportPublishError("Report update response was incomplete.");
70
+ return body.report;
71
+ }
72
+ function deployHeaders(token, json = true, extra = {}) {
73
+ return {
74
+ Authorization: `Bearer ${token}`,
75
+ Accept: "application/json",
76
+ ...(json ? { "Content-Type": "application/json" } : {}),
77
+ ...extra,
78
+ };
79
+ }
80
+ async function deployJson(token, route, method, body, extraHeaders = {}) {
81
+ let res;
82
+ try {
83
+ res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}${route}`, {
84
+ method,
85
+ headers: deployHeaders(token, body !== undefined, extraHeaders),
86
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
87
+ });
88
+ }
89
+ catch {
90
+ throw new ReportPublishError("Could not reach hq-deploy.");
91
+ }
92
+ if (!res.ok)
93
+ throw new ReportPublishError("hq-deploy rejected the report deployment.");
94
+ return await res.json();
95
+ }
96
+ export async function resolveCompanySlug(token, companyUid) {
97
+ const res = await vaultApiFetch({ token, path: `/entity/${encodeURIComponent(companyUid)}` });
98
+ if (!res.ok)
99
+ throw new ReportPublishError("Could not resolve the report's company for deploy.");
100
+ const body = await res.json();
101
+ const slug = body.entity?.slug;
102
+ if (!slug)
103
+ throw new ReportPublishError("Could not resolve the report's company for deploy.");
104
+ return slug;
105
+ }
106
+ async function orgFor(token, report) {
107
+ const slug = report.companyUid ? await resolveCompanySlug(token, report.companyUid) : undefined;
108
+ return deployOrgContext(report, slug);
109
+ }
110
+ async function findOrCreateApp(token, slug, org) {
111
+ const listed = await deployJson(token, "/api/apps", "GET", undefined, org.headers);
112
+ const existing = Array.isArray(listed.apps)
113
+ ? listed.apps.find((app) => !!app && typeof app === "object" && (app.subdomain === slug || app.name === slug))
114
+ : undefined;
115
+ const app = existing ?? await deployJson(token, "/api/apps", "POST", { name: slug, type: "static" }, org.headers);
116
+ const id = typeof app.id === "string" ? app.id : undefined;
117
+ const subdomain = typeof app.subdomain === "string" ? app.subdomain : undefined;
118
+ if (!id || !subdomain)
119
+ throw new ReportPublishError("hq-deploy returned an incomplete app record.");
120
+ return { id, subdomain };
121
+ }
122
+ export async function deployReportStatic(token, input) {
123
+ const app = await findOrCreateApp(token, input.slug, input.org);
124
+ await deployJson(token, `/api/apps/${encodeURIComponent(app.id)}/access-policy`, "PUT", input.access, input.org.headers);
125
+ const tarball = buildReportTarball(input.files);
126
+ const deployment = await deployJson(token, "/api/deploys", "POST", {
127
+ appSlug: app.subdomain,
128
+ ...(input.org.org ? { org: input.org.org } : {}),
129
+ manifest: { files: [], size: tarball.sizeBytes, sha256: tarball.sha256 },
130
+ }, input.org.headers);
131
+ const deployId = typeof deployment.deployId === "string" ? deployment.deployId : undefined;
132
+ const presignedUrl = typeof deployment.presignedUrl === "string" ? deployment.presignedUrl : undefined;
133
+ if (!deployId || !presignedUrl)
134
+ throw new ReportPublishError("hq-deploy did not create an upload.");
135
+ let upload;
136
+ try {
137
+ upload = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": "application/gzip" }, body: tarball.bytes });
138
+ }
139
+ catch {
140
+ throw new ReportPublishError("Report upload failed.");
141
+ }
142
+ if (!upload.ok)
143
+ throw new ReportPublishError("Report upload failed.");
144
+ const completed = await deployJson(token, `/api/deploys/${encodeURIComponent(deployId)}/complete`, "POST", { appSlug: app.subdomain }, input.org.headers);
145
+ const url = typeof completed.url === "string" ? completed.url : `https://${app.subdomain}.indigo-hq.com`;
146
+ return { appId: app.id, url, deployId };
147
+ }
148
+ async function publishWithOrg(token, report, directory, draft) {
149
+ return publishReport(report, directory, draft, {
150
+ patch: (current, patch) => patchReport(token, current, patch),
151
+ deploy: async (input) => {
152
+ const org = await orgFor(token, report);
153
+ return deployReportStatic(token, { ...input, org });
154
+ },
155
+ now: () => new Date().toISOString(),
156
+ });
157
+ }
158
+ async function publishAction(reportId, options) {
159
+ const token = await ensureCognitoToken();
160
+ const report = await resolveReport(token, reportId, options.channel);
161
+ print(await publishWithOrg(token, report, options.dir, options.draft === true), options.json === true);
162
+ }
163
+ async function promoteAction(reportId, options) {
164
+ const token = await ensureCognitoToken();
165
+ const report = await resolveReport(token, reportId, options.channel);
166
+ const directory = options.dir?.trim() ? options.dir : conventionReportDir(reportId);
167
+ const published = await publishWithOrg(token, report, directory, false);
168
+ // A promotion makes the freshly deployed live page authoritative. Null clears
169
+ // the draft pointer; omitting the field would leave the old draft in place.
170
+ const updated = published.draft
171
+ ? await patchReport(token, published, { draft: null, state: "live" })
172
+ : published;
173
+ print(updated, options.json === true);
174
+ }
175
+ async function getAction(reportId, options) {
176
+ const report = await resolveReport(await ensureCognitoToken(), reportId, options.channel);
177
+ print(report, options.json === true);
178
+ }
179
+ async function discardDraftAction(reportId, options) {
180
+ const token = await ensureCognitoToken();
181
+ const report = await resolveReport(token, reportId, options.channel);
182
+ const org = await orgFor(token, report);
183
+ if (report.draft?.appId) {
184
+ const res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}/api/apps/${encodeURIComponent(report.draft.appId)}`, {
185
+ method: "DELETE",
186
+ headers: deployHeaders(token, false, org.headers),
187
+ });
188
+ if (!res.ok && res.status !== 404)
189
+ throw new ReportPublishError("Could not remove the draft deployment.");
190
+ }
191
+ const updated = await patchReport(token, report, { draft: null, state: report.url ? "live" : "draft-pending" });
192
+ print(updated, options.json === true);
193
+ }
194
+ async function accessAction(reportId, options) {
195
+ if (!["company", "selected", "password", "public"].includes(options.mode)) {
196
+ throw new ReportPublishError("Access mode must be company, selected, password, or public.");
197
+ }
198
+ if (options.mode === "password" && !options.password) {
199
+ throw new ReportPublishError("A password is required for password access mode.");
200
+ }
201
+ const token = await ensureCognitoToken();
202
+ const report = await resolveReport(token, reportId, options.channel);
203
+ const appId = report.deployAppId ?? report.draft?.appId;
204
+ if (!appId)
205
+ throw new ReportPublishError("Publish the report before changing its access policy.");
206
+ const companyUid = report.companyUid ?? "personal";
207
+ const policy = {
208
+ mode: options.mode,
209
+ companyUid,
210
+ users: options.mode === "selected" ? [{ id: report.createdBy }] : [],
211
+ groups: [],
212
+ ...(options.mode === "password" && options.password ? { password: options.password } : {}),
213
+ };
214
+ const org = await orgFor(token, report);
215
+ const result = await deployJson(token, `/api/apps/${encodeURIComponent(appId)}/access-policy`, "PUT", policy, org.headers);
216
+ print(result, options.json === true);
217
+ }
218
+ async function cancelOwnedJob(token, agentUid, jobId) {
219
+ const res = await vaultApiFetch({
220
+ token,
221
+ path: `/v1/agents/${encodeURIComponent(agentUid)}/jobs/${encodeURIComponent(jobId)}/cancel`,
222
+ method: "POST",
223
+ });
224
+ if (!res.ok && res.status !== 404)
225
+ throw new ReportPublishError("Could not remove the report job.");
226
+ }
227
+ async function adoptAction(reportId, options) {
228
+ const token = await ensureCognitoToken();
229
+ const report = await resolveReport(token, reportId, options.channel);
230
+ const updated = await adoptReport(report, {
231
+ cancelJob: (jobId) => cancelOwnedJob(token, report.ownerAgentUid, jobId),
232
+ createJob: async (input) => {
233
+ const res = await vaultApiFetch({
234
+ token,
235
+ path: `/v1/agents/${encodeURIComponent(report.ownerAgentUid)}/jobs`,
236
+ method: "POST",
237
+ body: input,
238
+ });
239
+ if (!res.ok)
240
+ throw new ReportPublishError("Could not install the report job.");
241
+ const body = await res.json();
242
+ const jobId = body.job?.jobId ?? body.jobId;
243
+ if (!jobId)
244
+ throw new ReportPublishError("Jobs API returned an incomplete job.");
245
+ return { jobId };
246
+ },
247
+ patch: (current, patch) => patchReport(token, current, patch),
248
+ });
249
+ print(updated, options.json === true);
250
+ }
251
+ async function decommissionAction(reportId, options) {
252
+ const token = await ensureCognitoToken();
253
+ const report = await resolveReport(token, reportId, options.channel);
254
+ const org = await orgFor(token, report);
255
+ const leftover = [];
256
+ for (const id of [report.deployAppId, report.draft?.appId]) {
257
+ if (!id)
258
+ continue;
259
+ try {
260
+ const res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}/api/apps/${encodeURIComponent(id)}`, {
261
+ method: "DELETE",
262
+ headers: deployHeaders(token, false, org.headers),
263
+ });
264
+ if (res.status === 404)
265
+ leftover.push("The published page was already gone.");
266
+ else if (!res.ok)
267
+ leftover.push("A deployment could not be deactivated.");
268
+ }
269
+ catch {
270
+ leftover.push("A deployment could not be reached.");
271
+ }
272
+ }
273
+ // The durable report is marked decommissioned by hq-pro before this prompt.
274
+ // Cleanup gaps stay on the result so a missing app or offline bot does not roll the record back.
275
+ if (report.jobId) {
276
+ try {
277
+ await cancelOwnedJob(token, report.ownerAgentUid, report.jobId);
278
+ }
279
+ catch {
280
+ leftover.push("The report job is still installed.");
281
+ }
282
+ }
283
+ const note = leftover.length ? [...new Set(leftover)].join(" ") : null;
284
+ print({ reportId: report.reportId, decommissioned: true, note }, options.json === true);
285
+ }
286
+ export function registerReportCommand(program) {
287
+ const report = program.command("report").description("Publish and manage a channel report owned by this bot.");
288
+ const context = (command) => command.option("--channel <channelId>", "report channel context (or set HQ_REPORT_CHANNEL_ID)");
289
+ context(report.command("get <reportId>").option("--json").action((reportId, options) => runReportCommand(() => getAction(reportId, options))));
290
+ context(report.command("publish <reportId>").requiredOption("--dir <path>").option("--draft").option("--json").action((reportId, options) => runReportCommand(() => publishAction(reportId, options))));
291
+ context(report.command("promote <reportId>").option("--dir <path>", "report folder; defaults to <workerDir>/reports/<reportId>").option("--json").action((reportId, options) => runReportCommand(() => promoteAction(reportId, options))));
292
+ context(report.command("discard-draft <reportId>").option("--json").action((reportId, options) => runReportCommand(() => discardDraftAction(reportId, options))));
293
+ context(report.command("access <reportId>").requiredOption("--mode <mode>", "company|selected|password|public").option("--password <password>").option("--json").action((reportId, options) => runReportCommand(() => accessAction(reportId, options))));
294
+ context(report.command("decommission <reportId>").option("--json").action((reportId, options) => runReportCommand(() => decommissionAction(reportId, options))));
295
+ context(report.command("adopt <reportId>").option("--json").action((reportId, options) => runReportCommand(() => adoptAction(reportId, options))));
296
+ report.command("templates").option("--json").action((options) => {
297
+ const templates = listReportTemplates().map(({ name, path: templatePath }) => ({ name, path: templatePath }));
298
+ print(templates, options.json === true);
299
+ });
300
+ }
301
+ export async function runReportCommand(action) {
302
+ try {
303
+ await action();
304
+ }
305
+ catch (error) {
306
+ console.error(errorMessage(error));
307
+ process.exitCode = 1;
308
+ }
309
+ }
310
+ //# sourceMappingURL=report.js.map
@@ -12,6 +12,7 @@
12
12
  * than truncated. Unknown scalar metadata is dropped instead: the allowlist,
13
13
  * not a caller version match, determines what may leave the machine.
14
14
  */
15
+ import * as Sentry from "@sentry/node";
15
16
  import type { CaptureContext, SeverityLevel } from "@sentry/node";
16
17
  export declare const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
17
18
  export declare const MAX_SENTRY_REPORT_TIMEOUT_MS = 5000;
@@ -41,6 +42,7 @@ export type SentryReportOptions = {
41
42
  export type SentryReportDependencies = {
42
43
  sentry: {
43
44
  captureMessage: (message: string, context?: CaptureContext | SeverityLevel) => string;
45
+ metrics: Pick<typeof Sentry.metrics, "count">;
44
46
  flush: (timeout?: number) => PromiseLike<boolean>;
45
47
  };
46
48
  stdout: Pick<NodeJS.WriteStream, "write">;
@@ -29,6 +29,9 @@ const SENTRY_REPORT_LEVELS = new Set([
29
29
  "info",
30
30
  "debug",
31
31
  ]);
32
+ const HOOK_TIMEOUT_WARNING_TYPE = "hook_timeout_warning";
33
+ const HOOK_TIMEOUT_WARNING_METRIC = "hq.hook_timeout_warning";
34
+ const HOOK_TIMEOUT_EXCEEDED_FINGERPRINT = "hook-timeout-exceeded";
32
35
  /**
33
36
  * The only hook diagnostics that can be attached as metadata. The list omits
34
37
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -182,21 +185,43 @@ function resolveTimeout(timeoutMs) {
182
185
  return null;
183
186
  return timeout;
184
187
  }
185
- function sentryEnvelope(event) {
188
+ function sentryEnvelope(event, overrides) {
186
189
  return {
187
- level: event.level,
188
- fingerprint: [event.fingerprint],
190
+ level: overrides?.level ?? event.level,
191
+ fingerprint: overrides?.fingerprint ?? [event.fingerprint],
189
192
  tags: { sentry_event_type: event.type },
190
193
  extra: event.metadata,
191
194
  };
192
195
  }
193
196
  /** Build the same scrubbed event projection that the live SDK path uses. */
194
- function scrubbedSentryEnvelope(event) {
197
+ function scrubbedSentryEnvelope(event, overrides) {
195
198
  return beforeSend({
196
199
  message: event.message,
197
- ...sentryEnvelope(event),
200
+ ...sentryEnvelope(event, overrides),
198
201
  }, {});
199
202
  }
203
+ /**
204
+ * The watchdog's relative threshold fires before the declared timeout and
205
+ * clamps remaining_ms at zero, so its real warnings stay on the metric path.
206
+ * Keep a defensive branch for a genuine exceeded payload from another
207
+ * producer without restoring per-hook grouping.
208
+ */
209
+ function hookTimeoutExceededOverrides(event) {
210
+ if (event.type !== HOOK_TIMEOUT_WARNING_TYPE)
211
+ return undefined;
212
+ const elapsedMs = event.metadata.elapsed_ms;
213
+ const declaredTimeoutMs = event.metadata.declared_timeout_ms;
214
+ if (typeof elapsedMs !== "number" ||
215
+ typeof declaredTimeoutMs !== "number" ||
216
+ elapsedMs < declaredTimeoutMs) {
217
+ return undefined;
218
+ }
219
+ const hookEvent = typeof event.metadata.hook_event === "string" ? event.metadata.hook_event : "unknown";
220
+ return {
221
+ level: "warning",
222
+ fingerprint: [HOOK_TIMEOUT_EXCEEDED_FINGERPRINT, hookEvent],
223
+ };
224
+ }
200
225
  /**
201
226
  * Resolve true only when the local transport queue drains within the budget.
202
227
  * This does not confirm that the ingest service accepted the event.
@@ -234,13 +259,28 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
234
259
  const noun = result.droppedMetadataKeys === 1 ? "key" : "keys";
235
260
  dependencies.stderr.write(`Dropped ${result.droppedMetadataKeys} unsupported Sentry report metadata ${noun}.\n`);
236
261
  }
237
- const envelope = sentryEnvelope(result.event);
262
+ const overrides = hookTimeoutExceededOverrides(result.event);
238
263
  if (options.dryRun) {
239
- const scrubbed = scrubbedSentryEnvelope(result.event);
264
+ const scrubbed = scrubbedSentryEnvelope(result.event, overrides);
240
265
  if (scrubbed)
241
266
  dependencies.stdout.write(`${JSON.stringify(scrubbed)}\n`);
242
267
  return result;
243
268
  }
269
+ if (result.event.type === HOOK_TIMEOUT_WARNING_TYPE && !overrides) {
270
+ try {
271
+ dependencies.sentry.metrics.count(HOOK_TIMEOUT_WARNING_METRIC, 1, {
272
+ attributes: result.event.metadata,
273
+ });
274
+ if (await flushWithin(dependencies.sentry, timeoutMs))
275
+ return result;
276
+ }
277
+ catch {
278
+ // Report failure below without exposing event fields or transport details.
279
+ }
280
+ dependencies.stderr.write("Sentry report failed.\n");
281
+ return invalidInput();
282
+ }
283
+ const envelope = sentryEnvelope(result.event, overrides);
244
284
  try {
245
285
  dependencies.sentry.captureMessage(result.event.message, envelope);
246
286
  if (await flushWithin(dependencies.sentry, timeoutMs))
@@ -0,0 +1,173 @@
1
+ /** Mirrors the deploy skill's static-report guardrails. */
2
+ export declare const REPORT_SOURCE_MAX_BYTES: number;
3
+ export declare const REPORT_BUNDLE_MAX_BYTES: number;
4
+ export declare const REPORT_SOURCE_MAX_FILES = 100;
5
+ export declare const REPORT_CSP = "default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'none'; frame-src 'none'; form-action 'none'";
6
+ export type ReportLastRun = {
7
+ at: string;
8
+ status: "running" | "succeeded" | "failed";
9
+ reason?: string;
10
+ version?: string;
11
+ };
12
+ export type ReportSchedule = {
13
+ kind: "daily" | "weekly" | "manual";
14
+ time: string;
15
+ timezone: string;
16
+ weekday?: number;
17
+ };
18
+ export type ChannelReport = {
19
+ reportId: string;
20
+ channelId: string;
21
+ companyUid?: string;
22
+ createdBy: string;
23
+ ownerAgentUid: string;
24
+ deploySlug: string;
25
+ state: "draft-pending" | "draft-ready" | "live" | "decommissioned";
26
+ instructions: string;
27
+ revision: number;
28
+ deployAppId?: string;
29
+ url?: string;
30
+ draft?: {
31
+ url: string;
32
+ appId: string;
33
+ generatedAt: string;
34
+ note?: string;
35
+ };
36
+ lastRun?: ReportLastRun;
37
+ jobId?: string;
38
+ jobState?: "pending" | "adopted" | "failed";
39
+ jobStateReason?: string;
40
+ schedule?: ReportSchedule;
41
+ };
42
+ export type ReportPatch = Partial<Pick<ChannelReport, "deployAppId" | "url" | "state" | "lastRun">> & {
43
+ previewImage?: string;
44
+ previewTitle?: string;
45
+ jobState?: "pending" | "adopted" | "failed";
46
+ jobStateReason?: string | null;
47
+ /** Null clears the live draft pointer or the adopted job id. */
48
+ draft?: ChannelReport["draft"] | null;
49
+ jobId?: string | null;
50
+ };
51
+ export type DeployOrgContext = {
52
+ headers: Record<string, string>;
53
+ org?: string;
54
+ };
55
+ export type StaticFile = {
56
+ path: string;
57
+ content: string;
58
+ encoding?: "utf8" | "base64";
59
+ };
60
+ export type ReportSource = {
61
+ files: StaticFile[];
62
+ previewTitle?: string;
63
+ previewImage?: string;
64
+ };
65
+ export type DeployAccessPolicy = {
66
+ mode: "company" | "selected" | "password" | "public";
67
+ companyUid: string;
68
+ users: Array<{
69
+ id: string;
70
+ }>;
71
+ groups: Array<{
72
+ id: string;
73
+ }>;
74
+ password?: string;
75
+ };
76
+ export type ReportDeployment = {
77
+ appId: string;
78
+ url: string;
79
+ deployId: string;
80
+ };
81
+ export interface ReportPublishDependencies {
82
+ patch(report: ChannelReport, patch: ReportPatch): Promise<ChannelReport>;
83
+ deploy(input: {
84
+ slug: string;
85
+ files: StaticFile[];
86
+ access: DeployAccessPolicy;
87
+ }): Promise<ReportDeployment>;
88
+ now(): string;
89
+ /** Owner DM for a failed run. Omitted by callers that only record lastRun. */
90
+ notifyOwner?(notice: {
91
+ to: string;
92
+ body: string;
93
+ }): Promise<void>;
94
+ }
95
+ /**
96
+ * Plain owner DM after a failed run. Suppressed for the publish lock
97
+ * ("already running") so a second run does not page the owner.
98
+ */
99
+ export declare function ownerFailureNotice(input: {
100
+ code?: string | null;
101
+ reason: string;
102
+ reportUrl?: string | null;
103
+ ownerPersonUid: string;
104
+ }): {
105
+ to: string;
106
+ body: string;
107
+ } | null;
108
+ export interface ReportAdoptDependencies {
109
+ createJob(input: {
110
+ prompt: string;
111
+ schedule: {
112
+ kind: "recurring";
113
+ cron: string;
114
+ timezone: string;
115
+ };
116
+ context: {
117
+ reportId: string;
118
+ channelId: string;
119
+ };
120
+ }): Promise<{
121
+ jobId: string;
122
+ }>;
123
+ cancelJob(jobId: string): Promise<void>;
124
+ patch(report: ChannelReport, patch: ReportPatch): Promise<ChannelReport>;
125
+ }
126
+ export declare class ReportPublishError extends Error {
127
+ readonly code: "already-running" | "source-invalid" | "deploy-failed" | "job-failed";
128
+ constructor(message: string, code?: "already-running" | "source-invalid" | "deploy-failed" | "job-failed");
129
+ }
130
+ /** A deliberately boring, token-safe failure reason stored on the report. */
131
+ export declare function plainFailureReason(error: unknown): string;
132
+ export declare function injectReportCsp(html: string): string;
133
+ /** Read a deployable static report and inject its non-negotiable CSP. */
134
+ export declare function loadReportSource(directory: string): ReportSource;
135
+ /** Access policy defaults deliberately keep personal reports owner-only. */
136
+ export declare function defaultAccessPolicy(report: ChannelReport): DeployAccessPolicy;
137
+ export declare function reportDeploySlug(report: ChannelReport, draft: boolean): string;
138
+ /** Source folder convention: `<bot workerDir>/reports/<reportId>/`. */
139
+ export declare function conventionReportDir(reportId: string, workerDir?: string): string;
140
+ /** Company deploys send X-Org-Slug; personal-channel reports have no company to gate on. */
141
+ export declare function deployOrgContext(report: ChannelReport, companySlug?: string): DeployOrgContext;
142
+ /**
143
+ * EventBridge Scheduler cron for the agent jobs API.
144
+ * Daily uses `?` on the weekday field; weekly pins the weekday and `?` on the day-of-month.
145
+ */
146
+ export declare function reportJobCron(schedule: ReportSchedule): string | null;
147
+ /** Job prompt is the stored update-report runbook with this report id filled in. */
148
+ export declare function reportJobPrompt(reportId: string): string;
149
+ /**
150
+ * Install, replace, or delete this bot's own scheduled job for a report.
151
+ * Jobs-API failures are recorded as jobState=failed with a plain reason.
152
+ */
153
+ export declare function adoptReport(report: ChannelReport, deps: ReportAdoptDependencies): Promise<ChannelReport>;
154
+ /**
155
+ * Lock, deploy, then persist a report run. Every known failure after lock
156
+ * acquisition writes a terminal lastRun so the 15-minute server TTL is a
157
+ * crash-only fallback rather than normal control flow.
158
+ */
159
+ export declare function publishReport(report: ChannelReport, directory: string, draft: boolean, deps: ReportPublishDependencies): Promise<ChannelReport>;
160
+ type BuiltTarball = {
161
+ bytes: Buffer;
162
+ sha256: string;
163
+ sizeBytes: number;
164
+ };
165
+ /** hq-deploy static payload format: index.html stays at the archive root. */
166
+ export declare function buildReportTarball(files: StaticFile[]): BuiltTarball;
167
+ /** Isolated test helper for creating a compliant report source. */
168
+ export declare function makeReportFixture(html: string): {
169
+ directory: string;
170
+ cleanup: () => void;
171
+ };
172
+ export {};
173
+ //# sourceMappingURL=report-publish.d.ts.map