@hachej/boring-automation 0.1.80 → 0.1.81
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 +1 -1
- package/dist/front/index.js +3 -1
- package/dist/server/index.d.ts +57 -27
- package/dist/server/index.js +315 -164
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +3 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -15,4 +15,4 @@ Executable model values use explicit `provider:model-id` syntax. Legacy unqualif
|
|
|
15
15
|
|
|
16
16
|
Execution is currently composed for trusted local CLI folder mode. Scheduling has no background timer: user-owned cron/systemd may invoke `POST /api/v1/boring-automation/due` once per minute while the CLI server is running. Missed minutes are not backfilled.
|
|
17
17
|
|
|
18
|
-
Hosted persistence
|
|
18
|
+
Hosted persistence and creator-scoped execution are available in full-app. The deployment migration callback is `runBoringAutomationMigrations`; configure `BORING_AUTOMATION_TRIGGER_TOKEN` and have the platform scheduler invoke `POST /api/v1/boring-automation/due/hosted` with `Authorization: Bearer <token>`. The endpoint re-checks each creator and fails closed when authorization is lost. It has no hidden timer.
|
package/dist/front/index.js
CHANGED
|
@@ -21,7 +21,9 @@ var BORING_AUTOMATION_ERROR_CODES = {
|
|
|
21
21
|
RUN_ALREADY_RECORDED: "BORING_AUTOMATION_RUN_ALREADY_RECORDED",
|
|
22
22
|
RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE",
|
|
23
23
|
TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN",
|
|
24
|
-
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED"
|
|
24
|
+
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED",
|
|
25
|
+
OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED",
|
|
26
|
+
TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED"
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
// src/shared/schema.ts
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { WorkspaceAgentDispatcherResolver } from '@hachej/boring-agent/server';
|
|
|
2
2
|
import { FastifyRequest, FastifyInstance } from 'fastify';
|
|
3
3
|
import { WorkspaceAgentServerPluginContext } from '@hachej/boring-workspace/app/server';
|
|
4
4
|
import { WorkspaceServerPlugin } from '@hachej/boring-workspace/server';
|
|
5
|
+
import postgres from 'postgres';
|
|
5
6
|
import { Automation, AutomationCreate, AutomationPatch, AutomationRunBegin, AutomationRun, AutomationRunLifecyclePatch, BoringAutomationErrorCode, AutomationScheduleDecision } from '../shared/index.js';
|
|
6
7
|
export { AUTOMATION_SCHEDULE_ERRORS, AutomationCreateInput, AutomationCreateSchema, AutomationPatchInput, AutomationPatchSchema, AutomationRunBeginInput, AutomationRunBeginSchema, AutomationRunLifecyclePatchInput, AutomationRunLifecyclePatchSchema, AutomationRunStatus, AutomationRunStatusSchema, AutomationRunTrigger, AutomationRunTriggerSchema, AutomationScheduleDueDecision, AutomationScheduleDueReason, AutomationScheduleSkipDecision, AutomationScheduleSkipReason, AutomationScheduleValidationResult, BORING_AUTOMATION_ERROR_CODES, BORING_AUTOMATION_PLUGIN_ID, BORING_AUTOMATION_PLUGIN_LABEL, BORING_AUTOMATION_ROUTE_PREFIX, EvaluateAutomationScheduleInput, EvaluateAutomationScheduleResult, IdParamsSchema, PromptUpdateSchema, evaluateAutomationSchedule, isValidFiveFieldCron, isValidIanaTimeZone, validateAutomationSchedule } from '../shared/index.js';
|
|
7
|
-
import postgres from 'postgres';
|
|
8
8
|
import 'zod';
|
|
9
9
|
|
|
10
10
|
/** Plugin-local dependency injection seam for the single-workspace automation store. */
|
|
@@ -46,6 +46,7 @@ interface ManualRunInput {
|
|
|
46
46
|
request: FastifyRequest;
|
|
47
47
|
trigger?: "manual" | "scheduled";
|
|
48
48
|
scheduledFor?: string | null;
|
|
49
|
+
actor?: VerifiedAutomationActor;
|
|
49
50
|
}
|
|
50
51
|
declare class ManualRunExecutor {
|
|
51
52
|
private readonly options;
|
|
@@ -97,6 +98,55 @@ declare class DueRunService {
|
|
|
97
98
|
runDue(request: FastifyRequest): Promise<DueRunResult>;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
interface HostedAutomationActor {
|
|
102
|
+
workspaceId: string;
|
|
103
|
+
userId: string;
|
|
104
|
+
}
|
|
105
|
+
interface HostedAutomationCandidate {
|
|
106
|
+
automation: Automation;
|
|
107
|
+
actor: HostedAutomationActor;
|
|
108
|
+
runs: AutomationRun[];
|
|
109
|
+
}
|
|
110
|
+
type Sql = postgres.Sql;
|
|
111
|
+
/** Hosted store bound to a verified workspace/user pair; every query is scoped by both. */
|
|
112
|
+
declare class PostgresAutomationStore implements AutomationStore {
|
|
113
|
+
private readonly sql;
|
|
114
|
+
private readonly actor;
|
|
115
|
+
private readonly clock;
|
|
116
|
+
constructor(sql: Sql, actor: HostedAutomationActor, clock?: () => Date);
|
|
117
|
+
listAutomations(): Promise<Automation[]>;
|
|
118
|
+
getAutomation(id: string): Promise<Automation | null>;
|
|
119
|
+
createAutomation(input: AutomationCreate): Promise<Automation>;
|
|
120
|
+
updateAutomation(id: string, patch: AutomationPatch): Promise<Automation>;
|
|
121
|
+
deleteAutomation(id: string): Promise<void>;
|
|
122
|
+
getPrompt(automationId: string): Promise<string>;
|
|
123
|
+
updatePrompt(automationId: string, body: string): Promise<void>;
|
|
124
|
+
reconcileOrphanedRuns(automationId: string): Promise<void>;
|
|
125
|
+
beginRun(input: AutomationRunBegin): Promise<AutomationRun>;
|
|
126
|
+
updateRunLifecycle(runId: string, patch: AutomationRunLifecyclePatch): Promise<AutomationRun>;
|
|
127
|
+
listRuns(automationId: string): Promise<AutomationRun[]>;
|
|
128
|
+
private findRun;
|
|
129
|
+
}
|
|
130
|
+
declare function listHostedAutomationCandidates(sql: Sql): Promise<HostedAutomationCandidate[]>;
|
|
131
|
+
|
|
132
|
+
interface HostedDueRunServiceOptions {
|
|
133
|
+
sql: postgres.Sql;
|
|
134
|
+
dispatcherResolver: WorkspaceAgentDispatcherResolver;
|
|
135
|
+
verifyActor: (actor: HostedAutomationActor) => Promise<boolean> | boolean;
|
|
136
|
+
clock?: () => Date;
|
|
137
|
+
}
|
|
138
|
+
interface HostedDueRunResult {
|
|
139
|
+
now: string;
|
|
140
|
+
outcomes: DueRunOutcome[];
|
|
141
|
+
}
|
|
142
|
+
/** Runs due work for every creator while preserving creator-scoped execution. */
|
|
143
|
+
declare class HostedDueRunService {
|
|
144
|
+
private readonly options;
|
|
145
|
+
private readonly clock;
|
|
146
|
+
constructor(options: HostedDueRunServiceOptions);
|
|
147
|
+
runDue(request: FastifyRequest): Promise<HostedDueRunResult>;
|
|
148
|
+
}
|
|
149
|
+
|
|
100
150
|
type AtomicWriter = (path: string, content: string) => Promise<void>;
|
|
101
151
|
interface FileAutomationStoreOptions {
|
|
102
152
|
writer?: AtomicWriter;
|
|
@@ -134,36 +184,13 @@ declare class FileAutomationStore implements AutomationStore {
|
|
|
134
184
|
/** Deployment-owned hosted schema registration for the automation plugin. */
|
|
135
185
|
declare function runBoringAutomationMigrations(sql: postgres.Sql): Promise<void>;
|
|
136
186
|
|
|
137
|
-
interface HostedAutomationActor {
|
|
138
|
-
workspaceId: string;
|
|
139
|
-
userId: string;
|
|
140
|
-
}
|
|
141
|
-
type Sql = postgres.Sql;
|
|
142
|
-
/** Hosted store bound to a verified workspace/user pair; every query is scoped by both. */
|
|
143
|
-
declare class PostgresAutomationStore implements AutomationStore {
|
|
144
|
-
private readonly sql;
|
|
145
|
-
private readonly actor;
|
|
146
|
-
private readonly clock;
|
|
147
|
-
constructor(sql: Sql, actor: HostedAutomationActor, clock?: () => Date);
|
|
148
|
-
listAutomations(): Promise<Automation[]>;
|
|
149
|
-
getAutomation(id: string): Promise<Automation | null>;
|
|
150
|
-
createAutomation(input: AutomationCreate): Promise<Automation>;
|
|
151
|
-
updateAutomation(id: string, patch: AutomationPatch): Promise<Automation>;
|
|
152
|
-
deleteAutomation(id: string): Promise<void>;
|
|
153
|
-
getPrompt(automationId: string): Promise<string>;
|
|
154
|
-
updatePrompt(automationId: string, body: string): Promise<void>;
|
|
155
|
-
reconcileOrphanedRuns(automationId: string): Promise<void>;
|
|
156
|
-
beginRun(input: AutomationRunBegin): Promise<AutomationRun>;
|
|
157
|
-
updateRunLifecycle(runId: string, patch: AutomationRunLifecyclePatch): Promise<AutomationRun>;
|
|
158
|
-
listRuns(automationId: string): Promise<AutomationRun[]>;
|
|
159
|
-
private findRun;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
187
|
interface AutomationRoutesOptions {
|
|
163
188
|
store: AutomationStore;
|
|
164
189
|
storeForRequest?: (request: FastifyRequest) => Promise<AutomationStore> | AutomationStore;
|
|
165
190
|
manualRunExecutor?: Pick<ManualRunExecutor, "run">;
|
|
166
191
|
dueRunService?: Pick<DueRunService, "runDue">;
|
|
192
|
+
hostedDueRunService?: Pick<HostedDueRunService, "runDue">;
|
|
193
|
+
hostedTriggerToken?: string;
|
|
167
194
|
}
|
|
168
195
|
declare function automationRoutes(app: FastifyInstance, opts: AutomationRoutesOptions): Promise<void>;
|
|
169
196
|
|
|
@@ -173,8 +200,11 @@ interface BoringAutomationServerPluginOptions {
|
|
|
173
200
|
dispatcherResolver?: WorkspaceAgentDispatcherResolver;
|
|
174
201
|
actorResolver?: (request: FastifyRequest) => Promise<VerifiedAutomationActor> | VerifiedAutomationActor;
|
|
175
202
|
storeForRequest?: (request: FastifyRequest, actor: VerifiedAutomationActor) => Promise<AutomationStore> | AutomationStore;
|
|
203
|
+
actorVerifier?: (actor: VerifiedAutomationActor) => Promise<boolean> | boolean;
|
|
204
|
+
hostedTriggerToken?: string;
|
|
205
|
+
hostedDueRunService?: Pick<HostedDueRunService, "runDue">;
|
|
176
206
|
}
|
|
177
207
|
declare function createBoringAutomationServerPlugin(options?: BoringAutomationServerPluginOptions): WorkspaceServerPlugin;
|
|
178
208
|
declare function defaultBoringAutomationServerPlugin(options?: BoringAutomationServerPluginOptions, ctx?: Pick<WorkspaceAgentServerPluginContext, "workspaceRoot"> & Partial<Pick<WorkspaceAgentServerPluginContext, "trusted">>): WorkspaceServerPlugin;
|
|
179
209
|
|
|
180
|
-
export { Automation, AutomationCreate, AutomationPatch, type AutomationRoutesOptions, AutomationRun, AutomationRunBegin, AutomationRunLifecyclePatch, AutomationScheduleDecision, type AutomationStore, AutomationStoreError, BoringAutomationErrorCode, type BoringAutomationServerPluginOptions, type DueRunOutcome, type DueRunResult, DueRunService, type DueRunServiceOptions, type DueRunSummary, FileAutomationStore, type FileAutomationStoreOptions, type HostedAutomationActor, ManualRunExecutor, type ManualRunExecutorOptions, type ManualRunInput, PostgresAutomationStore, type VerifiedAutomationActor, automationNotFound, automationRoutes, createBoringAutomationServerPlugin, defaultBoringAutomationServerPlugin as default, parseAutomationModel, runAlreadyActive, runAlreadyRecorded, runBoringAutomationMigrations, runNotFound };
|
|
210
|
+
export { Automation, AutomationCreate, AutomationPatch, type AutomationRoutesOptions, AutomationRun, AutomationRunBegin, AutomationRunLifecyclePatch, AutomationScheduleDecision, type AutomationStore, AutomationStoreError, BoringAutomationErrorCode, type BoringAutomationServerPluginOptions, type DueRunOutcome, type DueRunResult, DueRunService, type DueRunServiceOptions, type DueRunSummary, FileAutomationStore, type FileAutomationStoreOptions, type HostedAutomationActor, type HostedAutomationCandidate, type HostedDueRunResult, HostedDueRunService, type HostedDueRunServiceOptions, ManualRunExecutor, type ManualRunExecutorOptions, type ManualRunInput, PostgresAutomationStore, type VerifiedAutomationActor, automationNotFound, automationRoutes, createBoringAutomationServerPlugin, defaultBoringAutomationServerPlugin as default, listHostedAutomationCandidates, parseAutomationModel, runAlreadyActive, runAlreadyRecorded, runBoringAutomationMigrations, runNotFound };
|
package/dist/server/index.js
CHANGED
|
@@ -19,7 +19,9 @@ var BORING_AUTOMATION_ERROR_CODES = {
|
|
|
19
19
|
RUN_ALREADY_RECORDED: "BORING_AUTOMATION_RUN_ALREADY_RECORDED",
|
|
20
20
|
RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE",
|
|
21
21
|
TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN",
|
|
22
|
-
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED"
|
|
22
|
+
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED",
|
|
23
|
+
OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED",
|
|
24
|
+
TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED"
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
// src/shared/schema.ts
|
|
@@ -515,162 +517,6 @@ function clone(value) {
|
|
|
515
517
|
return JSON.parse(JSON.stringify(value));
|
|
516
518
|
}
|
|
517
519
|
|
|
518
|
-
// src/server/postgresStore.ts
|
|
519
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
520
|
-
var PostgresAutomationStore = class {
|
|
521
|
-
constructor(sql, actor, clock = () => /* @__PURE__ */ new Date()) {
|
|
522
|
-
this.sql = sql;
|
|
523
|
-
this.actor = actor;
|
|
524
|
-
this.clock = clock;
|
|
525
|
-
}
|
|
526
|
-
sql;
|
|
527
|
-
actor;
|
|
528
|
-
clock;
|
|
529
|
-
async listAutomations() {
|
|
530
|
-
const rows = await this.sql`
|
|
531
|
-
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
532
|
-
FROM boring_automation_automations
|
|
533
|
-
WHERE workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
534
|
-
ORDER BY created_at, id
|
|
535
|
-
`;
|
|
536
|
-
return rows.map(toAutomation);
|
|
537
|
-
}
|
|
538
|
-
async getAutomation(id) {
|
|
539
|
-
const rows = await this.sql`
|
|
540
|
-
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
541
|
-
FROM boring_automation_automations
|
|
542
|
-
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
543
|
-
`;
|
|
544
|
-
return rows[0] ? toAutomation(rows[0]) : null;
|
|
545
|
-
}
|
|
546
|
-
async createAutomation(input) {
|
|
547
|
-
const now = this.clock().toISOString();
|
|
548
|
-
const id = randomUUID2();
|
|
549
|
-
const prompt = input.prompt ?? "";
|
|
550
|
-
const rows = await this.sql`
|
|
551
|
-
INSERT INTO boring_automation_automations (id, workspace_id, owner_user_id, title, enabled, cron, timezone, model, prompt, created_at, updated_at)
|
|
552
|
-
VALUES (${id}, ${this.actor.workspaceId}, ${this.actor.userId}, ${input.title}, ${input.enabled ?? true}, ${input.cron}, ${input.timezone}, ${input.model}, ${prompt}, ${now}, ${now})
|
|
553
|
-
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
554
|
-
`;
|
|
555
|
-
return toAutomation(rows[0]);
|
|
556
|
-
}
|
|
557
|
-
async updateAutomation(id, patch) {
|
|
558
|
-
const current = await this.getAutomation(id);
|
|
559
|
-
if (!current) throw automationNotFound(id);
|
|
560
|
-
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
561
|
-
const rows = await this.sql`
|
|
562
|
-
UPDATE boring_automation_automations
|
|
563
|
-
SET title = ${next.title}, enabled = ${next.enabled}, cron = ${next.cron}, timezone = ${next.timezone}, model = ${next.model}, updated_at = ${next.updatedAt}
|
|
564
|
-
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
565
|
-
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
566
|
-
`;
|
|
567
|
-
if (!rows[0]) throw automationNotFound(id);
|
|
568
|
-
return toAutomation(rows[0]);
|
|
569
|
-
}
|
|
570
|
-
async deleteAutomation(id) {
|
|
571
|
-
const result = await this.sql`
|
|
572
|
-
DELETE FROM boring_automation_automations
|
|
573
|
-
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
574
|
-
`;
|
|
575
|
-
if (result.count === 0) throw automationNotFound(id);
|
|
576
|
-
}
|
|
577
|
-
async getPrompt(automationId) {
|
|
578
|
-
const automation = await this.getAutomation(automationId);
|
|
579
|
-
if (!automation) throw automationNotFound(automationId);
|
|
580
|
-
const rows = await this.sql`
|
|
581
|
-
SELECT prompt FROM boring_automation_automations
|
|
582
|
-
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
583
|
-
`;
|
|
584
|
-
return rows[0]?.prompt ?? "";
|
|
585
|
-
}
|
|
586
|
-
async updatePrompt(automationId, body) {
|
|
587
|
-
const result = await this.sql`
|
|
588
|
-
UPDATE boring_automation_automations SET prompt = ${body}, updated_at = ${this.clock().toISOString()}
|
|
589
|
-
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
590
|
-
`;
|
|
591
|
-
if (result.count === 0) throw automationNotFound(automationId);
|
|
592
|
-
}
|
|
593
|
-
async reconcileOrphanedRuns(automationId) {
|
|
594
|
-
await this.sql`
|
|
595
|
-
UPDATE boring_automation_runs
|
|
596
|
-
SET status = 'failed', completed_at = ${this.clock().toISOString()}, error = 'Automation host restarted before the run completed', updated_at = ${this.clock().toISOString()}
|
|
597
|
-
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
598
|
-
`;
|
|
599
|
-
}
|
|
600
|
-
async beginRun(input) {
|
|
601
|
-
const automation = await this.getAutomation(input.automationId);
|
|
602
|
-
if (!automation) throw automationNotFound(input.automationId);
|
|
603
|
-
const active = await this.sql`
|
|
604
|
-
SELECT id FROM boring_automation_runs
|
|
605
|
-
WHERE automation_id = ${input.automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
606
|
-
LIMIT 1
|
|
607
|
-
`;
|
|
608
|
-
if (active[0]) throw runAlreadyActive(input.automationId);
|
|
609
|
-
try {
|
|
610
|
-
const now = input.createdAt ?? this.clock().toISOString();
|
|
611
|
-
const id = randomUUID2();
|
|
612
|
-
const rows = await this.sql`
|
|
613
|
-
INSERT INTO boring_automation_runs (id, automation_id, workspace_id, owner_user_id, session_id, status, trigger, scheduled_for, started_at, completed_at, duration_ms, input_tokens, output_tokens, total_tokens, prompt_snapshot, model_snapshot, error, created_at, updated_at)
|
|
614
|
-
VALUES (${id}, ${input.automationId}, ${this.actor.workspaceId}, ${this.actor.userId}, NULL, 'queued', ${input.trigger}, ${input.scheduledFor ?? null}, NULL, NULL, NULL, NULL, NULL, NULL, ${input.promptSnapshot}, ${input.modelSnapshot}, NULL, ${now}, ${now})
|
|
615
|
-
RETURNING *
|
|
616
|
-
`;
|
|
617
|
-
return toRun(rows[0]);
|
|
618
|
-
} catch (error) {
|
|
619
|
-
if (isUniqueViolation(error)) {
|
|
620
|
-
if (constraintName(error) === "boring_automation_runs_active_once_idx") throw runAlreadyActive(input.automationId);
|
|
621
|
-
if (input.trigger === "scheduled" && input.scheduledFor) throw runAlreadyRecorded(input.automationId, input.scheduledFor);
|
|
622
|
-
}
|
|
623
|
-
throw error;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
async updateRunLifecycle(runId, patch) {
|
|
627
|
-
const current = await this.findRun(runId);
|
|
628
|
-
if (!current) throw runNotFound(runId);
|
|
629
|
-
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
630
|
-
const rows = await this.sql`
|
|
631
|
-
UPDATE boring_automation_runs
|
|
632
|
-
SET session_id = ${next.sessionId}, status = ${next.status}, started_at = ${next.startedAt}, completed_at = ${next.completedAt}, duration_ms = ${next.durationMs}, input_tokens = ${next.inputTokens}, output_tokens = ${next.outputTokens}, total_tokens = ${next.totalTokens}, error = ${next.error}, updated_at = ${next.updatedAt}
|
|
633
|
-
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
634
|
-
RETURNING *
|
|
635
|
-
`;
|
|
636
|
-
if (!rows[0]) throw runNotFound(runId);
|
|
637
|
-
return toRun(rows[0]);
|
|
638
|
-
}
|
|
639
|
-
async listRuns(automationId) {
|
|
640
|
-
const rows = await this.sql`
|
|
641
|
-
SELECT * FROM boring_automation_runs
|
|
642
|
-
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
643
|
-
ORDER BY created_at DESC, id DESC
|
|
644
|
-
`;
|
|
645
|
-
return rows.map(toRun);
|
|
646
|
-
}
|
|
647
|
-
async findRun(runId) {
|
|
648
|
-
const rows = await this.sql`
|
|
649
|
-
SELECT * FROM boring_automation_runs
|
|
650
|
-
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
651
|
-
`;
|
|
652
|
-
return rows[0] ? toRun(rows[0]) : null;
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
function toAutomation(row) {
|
|
656
|
-
return { id: row.id, title: row.title, enabled: row.enabled, cron: row.cron, timezone: row.timezone, model: row.model, promptRef: `hosted:${row.id}`, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
657
|
-
}
|
|
658
|
-
function toRun(row) {
|
|
659
|
-
return { id: row.id, automationId: row.automation_id, sessionId: row.session_id, status: row.status, trigger: row.trigger, scheduledFor: nullableIso(row.scheduled_for), startedAt: nullableIso(row.started_at), completedAt: nullableIso(row.completed_at), durationMs: row.duration_ms, inputTokens: row.input_tokens, outputTokens: row.output_tokens, totalTokens: row.total_tokens, promptSnapshot: row.prompt_snapshot, modelSnapshot: row.model_snapshot, error: row.error, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
660
|
-
}
|
|
661
|
-
function iso(value) {
|
|
662
|
-
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
663
|
-
}
|
|
664
|
-
function nullableIso(value) {
|
|
665
|
-
return value === null ? null : iso(value);
|
|
666
|
-
}
|
|
667
|
-
function isUniqueViolation(error) {
|
|
668
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "23505");
|
|
669
|
-
}
|
|
670
|
-
function constraintName(error) {
|
|
671
|
-
return error && typeof error === "object" && "constraint_name" in error ? String(error.constraint_name) : void 0;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
520
|
// src/server/manualRunExecutor.ts
|
|
675
521
|
var ManualRunExecutor = class {
|
|
676
522
|
constructor(options) {
|
|
@@ -680,7 +526,7 @@ var ManualRunExecutor = class {
|
|
|
680
526
|
options;
|
|
681
527
|
clock;
|
|
682
528
|
async run(input) {
|
|
683
|
-
const actor = await this.options.actorResolver(input.request);
|
|
529
|
+
const actor = input.actor ?? await this.options.actorResolver(input.request);
|
|
684
530
|
const store = await this.options.storeForRequest?.(input.request, actor) ?? this.options.store;
|
|
685
531
|
const automation = await store.getAutomation(input.automationId);
|
|
686
532
|
if (!automation) {
|
|
@@ -861,8 +707,277 @@ function durationMs(startedAt, completedAt) {
|
|
|
861
707
|
return Math.max(0, new Date(completedAt).getTime() - new Date(startedAt).getTime());
|
|
862
708
|
}
|
|
863
709
|
|
|
710
|
+
// src/server/postgresStore.ts
|
|
711
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
712
|
+
var PostgresAutomationStore = class {
|
|
713
|
+
constructor(sql, actor, clock = () => /* @__PURE__ */ new Date()) {
|
|
714
|
+
this.sql = sql;
|
|
715
|
+
this.actor = actor;
|
|
716
|
+
this.clock = clock;
|
|
717
|
+
}
|
|
718
|
+
sql;
|
|
719
|
+
actor;
|
|
720
|
+
clock;
|
|
721
|
+
async listAutomations() {
|
|
722
|
+
const rows = await this.sql`
|
|
723
|
+
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
724
|
+
FROM boring_automation_automations
|
|
725
|
+
WHERE workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
726
|
+
ORDER BY created_at, id
|
|
727
|
+
`;
|
|
728
|
+
return rows.map(toAutomation);
|
|
729
|
+
}
|
|
730
|
+
async getAutomation(id) {
|
|
731
|
+
const rows = await this.sql`
|
|
732
|
+
SELECT id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
733
|
+
FROM boring_automation_automations
|
|
734
|
+
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
735
|
+
`;
|
|
736
|
+
return rows[0] ? toAutomation(rows[0]) : null;
|
|
737
|
+
}
|
|
738
|
+
async createAutomation(input) {
|
|
739
|
+
const now = this.clock().toISOString();
|
|
740
|
+
const id = randomUUID2();
|
|
741
|
+
const prompt = input.prompt ?? "";
|
|
742
|
+
const rows = await this.sql`
|
|
743
|
+
INSERT INTO boring_automation_automations (id, workspace_id, owner_user_id, title, enabled, cron, timezone, model, prompt, created_at, updated_at)
|
|
744
|
+
VALUES (${id}, ${this.actor.workspaceId}, ${this.actor.userId}, ${input.title}, ${input.enabled ?? true}, ${input.cron}, ${input.timezone}, ${input.model}, ${prompt}, ${now}, ${now})
|
|
745
|
+
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
746
|
+
`;
|
|
747
|
+
return toAutomation(rows[0]);
|
|
748
|
+
}
|
|
749
|
+
async updateAutomation(id, patch) {
|
|
750
|
+
const current = await this.getAutomation(id);
|
|
751
|
+
if (!current) throw automationNotFound(id);
|
|
752
|
+
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
753
|
+
const rows = await this.sql`
|
|
754
|
+
UPDATE boring_automation_automations
|
|
755
|
+
SET title = ${next.title}, enabled = ${next.enabled}, cron = ${next.cron}, timezone = ${next.timezone}, model = ${next.model}, updated_at = ${next.updatedAt}
|
|
756
|
+
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
757
|
+
RETURNING id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
758
|
+
`;
|
|
759
|
+
if (!rows[0]) throw automationNotFound(id);
|
|
760
|
+
return toAutomation(rows[0]);
|
|
761
|
+
}
|
|
762
|
+
async deleteAutomation(id) {
|
|
763
|
+
const result = await this.sql`
|
|
764
|
+
DELETE FROM boring_automation_automations
|
|
765
|
+
WHERE id = ${id} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
766
|
+
`;
|
|
767
|
+
if (result.count === 0) throw automationNotFound(id);
|
|
768
|
+
}
|
|
769
|
+
async getPrompt(automationId) {
|
|
770
|
+
const automation = await this.getAutomation(automationId);
|
|
771
|
+
if (!automation) throw automationNotFound(automationId);
|
|
772
|
+
const rows = await this.sql`
|
|
773
|
+
SELECT prompt FROM boring_automation_automations
|
|
774
|
+
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
775
|
+
`;
|
|
776
|
+
return rows[0]?.prompt ?? "";
|
|
777
|
+
}
|
|
778
|
+
async updatePrompt(automationId, body) {
|
|
779
|
+
const result = await this.sql`
|
|
780
|
+
UPDATE boring_automation_automations SET prompt = ${body}, updated_at = ${this.clock().toISOString()}
|
|
781
|
+
WHERE id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
782
|
+
`;
|
|
783
|
+
if (result.count === 0) throw automationNotFound(automationId);
|
|
784
|
+
}
|
|
785
|
+
async reconcileOrphanedRuns(automationId) {
|
|
786
|
+
await this.sql`
|
|
787
|
+
UPDATE boring_automation_runs
|
|
788
|
+
SET status = 'failed', completed_at = ${this.clock().toISOString()}, error = 'Automation host restarted before the run completed', updated_at = ${this.clock().toISOString()}
|
|
789
|
+
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
790
|
+
`;
|
|
791
|
+
}
|
|
792
|
+
async beginRun(input) {
|
|
793
|
+
const automation = await this.getAutomation(input.automationId);
|
|
794
|
+
if (!automation) throw automationNotFound(input.automationId);
|
|
795
|
+
const active = await this.sql`
|
|
796
|
+
SELECT id FROM boring_automation_runs
|
|
797
|
+
WHERE automation_id = ${input.automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId} AND status IN ('queued', 'running')
|
|
798
|
+
LIMIT 1
|
|
799
|
+
`;
|
|
800
|
+
if (active[0]) throw runAlreadyActive(input.automationId);
|
|
801
|
+
try {
|
|
802
|
+
const now = input.createdAt ?? this.clock().toISOString();
|
|
803
|
+
const id = randomUUID2();
|
|
804
|
+
const rows = await this.sql`
|
|
805
|
+
INSERT INTO boring_automation_runs (id, automation_id, workspace_id, owner_user_id, session_id, status, trigger, scheduled_for, started_at, completed_at, duration_ms, input_tokens, output_tokens, total_tokens, prompt_snapshot, model_snapshot, error, created_at, updated_at)
|
|
806
|
+
VALUES (${id}, ${input.automationId}, ${this.actor.workspaceId}, ${this.actor.userId}, NULL, 'queued', ${input.trigger}, ${input.scheduledFor ?? null}, NULL, NULL, NULL, NULL, NULL, NULL, ${input.promptSnapshot}, ${input.modelSnapshot}, NULL, ${now}, ${now})
|
|
807
|
+
RETURNING *
|
|
808
|
+
`;
|
|
809
|
+
return toRun(rows[0]);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (isUniqueViolation(error)) {
|
|
812
|
+
if (constraintName(error) === "boring_automation_runs_active_once_idx") throw runAlreadyActive(input.automationId);
|
|
813
|
+
if (input.trigger === "scheduled" && input.scheduledFor) throw runAlreadyRecorded(input.automationId, input.scheduledFor);
|
|
814
|
+
}
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async updateRunLifecycle(runId, patch) {
|
|
819
|
+
const current = await this.findRun(runId);
|
|
820
|
+
if (!current) throw runNotFound(runId);
|
|
821
|
+
const next = { ...current, ...patch, updatedAt: this.clock().toISOString() };
|
|
822
|
+
const rows = await this.sql`
|
|
823
|
+
UPDATE boring_automation_runs
|
|
824
|
+
SET session_id = ${next.sessionId}, status = ${next.status}, started_at = ${next.startedAt}, completed_at = ${next.completedAt}, duration_ms = ${next.durationMs}, input_tokens = ${next.inputTokens}, output_tokens = ${next.outputTokens}, total_tokens = ${next.totalTokens}, error = ${next.error}, updated_at = ${next.updatedAt}
|
|
825
|
+
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
826
|
+
RETURNING *
|
|
827
|
+
`;
|
|
828
|
+
if (!rows[0]) throw runNotFound(runId);
|
|
829
|
+
return toRun(rows[0]);
|
|
830
|
+
}
|
|
831
|
+
async listRuns(automationId) {
|
|
832
|
+
const rows = await this.sql`
|
|
833
|
+
SELECT * FROM boring_automation_runs
|
|
834
|
+
WHERE automation_id = ${automationId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
835
|
+
ORDER BY created_at DESC, id DESC
|
|
836
|
+
`;
|
|
837
|
+
return rows.map(toRun);
|
|
838
|
+
}
|
|
839
|
+
async findRun(runId) {
|
|
840
|
+
const rows = await this.sql`
|
|
841
|
+
SELECT * FROM boring_automation_runs
|
|
842
|
+
WHERE id = ${runId} AND workspace_id = ${this.actor.workspaceId} AND owner_user_id = ${this.actor.userId}
|
|
843
|
+
`;
|
|
844
|
+
return rows[0] ? toRun(rows[0]) : null;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
async function listHostedAutomationCandidates(sql) {
|
|
848
|
+
const automationRows = await sql`
|
|
849
|
+
SELECT id, workspace_id, owner_user_id, title, enabled, cron, timezone, model, prompt, created_at, updated_at
|
|
850
|
+
FROM boring_automation_automations
|
|
851
|
+
ORDER BY id
|
|
852
|
+
`;
|
|
853
|
+
const runRows = await sql`
|
|
854
|
+
SELECT * FROM boring_automation_runs ORDER BY automation_id, created_at DESC, id DESC
|
|
855
|
+
`;
|
|
856
|
+
const runsByAutomation = /* @__PURE__ */ new Map();
|
|
857
|
+
for (const row of runRows) {
|
|
858
|
+
const list = runsByAutomation.get(row.automation_id) ?? [];
|
|
859
|
+
list.push(toRun(row));
|
|
860
|
+
runsByAutomation.set(row.automation_id, list);
|
|
861
|
+
}
|
|
862
|
+
return automationRows.map((row) => ({
|
|
863
|
+
automation: toAutomation(row),
|
|
864
|
+
actor: { workspaceId: row.workspace_id, userId: row.owner_user_id },
|
|
865
|
+
runs: runsByAutomation.get(row.id) ?? []
|
|
866
|
+
}));
|
|
867
|
+
}
|
|
868
|
+
function toAutomation(row) {
|
|
869
|
+
return { id: row.id, title: row.title, enabled: row.enabled, cron: row.cron, timezone: row.timezone, model: row.model, promptRef: `hosted:${row.id}`, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
870
|
+
}
|
|
871
|
+
function toRun(row) {
|
|
872
|
+
return { id: row.id, automationId: row.automation_id, sessionId: row.session_id, status: row.status, trigger: row.trigger, scheduledFor: nullableIso(row.scheduled_for), startedAt: nullableIso(row.started_at), completedAt: nullableIso(row.completed_at), durationMs: row.duration_ms, inputTokens: row.input_tokens, outputTokens: row.output_tokens, totalTokens: row.total_tokens, promptSnapshot: row.prompt_snapshot, modelSnapshot: row.model_snapshot, error: row.error, createdAt: iso(row.created_at), updatedAt: iso(row.updated_at) };
|
|
873
|
+
}
|
|
874
|
+
function iso(value) {
|
|
875
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
876
|
+
}
|
|
877
|
+
function nullableIso(value) {
|
|
878
|
+
return value === null ? null : iso(value);
|
|
879
|
+
}
|
|
880
|
+
function isUniqueViolation(error) {
|
|
881
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "23505");
|
|
882
|
+
}
|
|
883
|
+
function constraintName(error) {
|
|
884
|
+
return error && typeof error === "object" && "constraint_name" in error ? String(error.constraint_name) : void 0;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/server/hostedDueRunService.ts
|
|
888
|
+
var HostedDueRunService = class {
|
|
889
|
+
constructor(options) {
|
|
890
|
+
this.options = options;
|
|
891
|
+
this.clock = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
892
|
+
}
|
|
893
|
+
options;
|
|
894
|
+
clock;
|
|
895
|
+
async runDue(request) {
|
|
896
|
+
const now = this.clock();
|
|
897
|
+
const candidates = await listHostedAutomationCandidates(this.options.sql);
|
|
898
|
+
const outcomes = [];
|
|
899
|
+
for (const candidate of candidates) {
|
|
900
|
+
if (!await this.options.verifyActor(candidate.actor)) {
|
|
901
|
+
outcomes.push({
|
|
902
|
+
kind: "failed",
|
|
903
|
+
automationId: candidate.automation.id,
|
|
904
|
+
scheduledFor: now.toISOString(),
|
|
905
|
+
code: BORING_AUTOMATION_ERROR_CODES.OWNER_UNAUTHORIZED,
|
|
906
|
+
message: "automation creator is no longer authorized"
|
|
907
|
+
});
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const evaluated = evaluateAutomationSchedule({
|
|
911
|
+
automations: [candidate.automation],
|
|
912
|
+
runs: candidate.runs,
|
|
913
|
+
now
|
|
914
|
+
});
|
|
915
|
+
const decision = evaluated.due[0];
|
|
916
|
+
if (!decision) {
|
|
917
|
+
const skipped = evaluated.decisions[0];
|
|
918
|
+
if (skipped?.kind === "skip") outcomes.push({
|
|
919
|
+
kind: "skipped",
|
|
920
|
+
automationId: candidate.automation.id,
|
|
921
|
+
scheduledFor: skipped.scheduledFor,
|
|
922
|
+
reason: skipped.reason,
|
|
923
|
+
message: skipped.message
|
|
924
|
+
});
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const store = new PostgresAutomationStore(this.options.sql, candidate.actor, this.clock);
|
|
928
|
+
const executor = new ManualRunExecutor({
|
|
929
|
+
store,
|
|
930
|
+
dispatcherResolver: this.options.dispatcherResolver,
|
|
931
|
+
actorResolver: () => candidate.actor
|
|
932
|
+
});
|
|
933
|
+
try {
|
|
934
|
+
const run = await executor.run({
|
|
935
|
+
automationId: candidate.automation.id,
|
|
936
|
+
request,
|
|
937
|
+
trigger: "scheduled",
|
|
938
|
+
scheduledFor: decision.scheduledFor,
|
|
939
|
+
actor: candidate.actor
|
|
940
|
+
});
|
|
941
|
+
outcomes.push({
|
|
942
|
+
kind: "started",
|
|
943
|
+
automationId: candidate.automation.id,
|
|
944
|
+
scheduledFor: decision.scheduledFor,
|
|
945
|
+
run: toSummary(run)
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
outcomes.push({
|
|
949
|
+
kind: "failed",
|
|
950
|
+
automationId: candidate.automation.id,
|
|
951
|
+
scheduledFor: decision.scheduledFor,
|
|
952
|
+
code: error instanceof Error && "code" in error ? String(error.code) : BORING_AUTOMATION_ERROR_CODES.RUN_FAILED,
|
|
953
|
+
message: error instanceof Error ? error.message : "Automation run failed"
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
outcomes.sort((a, b) => a.automationId.localeCompare(b.automationId));
|
|
958
|
+
return { now: now.toISOString(), outcomes };
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
function toSummary(run) {
|
|
962
|
+
return {
|
|
963
|
+
id: run.id,
|
|
964
|
+
automationId: run.automationId,
|
|
965
|
+
sessionId: run.sessionId,
|
|
966
|
+
status: run.status,
|
|
967
|
+
trigger: run.trigger,
|
|
968
|
+
scheduledFor: run.scheduledFor,
|
|
969
|
+
startedAt: run.startedAt,
|
|
970
|
+
completedAt: run.completedAt,
|
|
971
|
+
durationMs: run.durationMs,
|
|
972
|
+
inputTokens: run.inputTokens,
|
|
973
|
+
outputTokens: run.outputTokens,
|
|
974
|
+
totalTokens: run.totalTokens
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
864
978
|
// src/server/routes.ts
|
|
865
979
|
import { ZodError } from "zod";
|
|
980
|
+
import { timingSafeEqual } from "crypto";
|
|
866
981
|
async function automationRoutes(app, opts) {
|
|
867
982
|
app.get(`${BORING_AUTOMATION_ROUTE_PREFIX}/automations`, async (_request, reply) => {
|
|
868
983
|
try {
|
|
@@ -925,6 +1040,16 @@ async function automationRoutes(app, opts) {
|
|
|
925
1040
|
return sendError(reply, cause);
|
|
926
1041
|
}
|
|
927
1042
|
});
|
|
1043
|
+
app.post(`${BORING_AUTOMATION_ROUTE_PREFIX}/due/hosted`, async (request, reply) => {
|
|
1044
|
+
try {
|
|
1045
|
+
if (!opts.hostedDueRunService || !opts.hostedTriggerToken || !hasBearerToken(request.headers.authorization, opts.hostedTriggerToken)) {
|
|
1046
|
+
throw new AutomationStoreError(BORING_AUTOMATION_ERROR_CODES.TRIGGER_UNAUTHORIZED, "hosted automation trigger is unauthorized");
|
|
1047
|
+
}
|
|
1048
|
+
return { ok: true, ...await opts.hostedDueRunService.runDue(request) };
|
|
1049
|
+
} catch (cause) {
|
|
1050
|
+
return sendError(reply, cause);
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
928
1053
|
app.post(`${BORING_AUTOMATION_ROUTE_PREFIX}/due`, async (request, reply) => {
|
|
929
1054
|
try {
|
|
930
1055
|
if (!isLoopbackAddress(request.ip)) {
|
|
@@ -1002,6 +1127,12 @@ function zodIssueErrorCode(issue) {
|
|
|
1002
1127
|
if (field === "timezone") return BORING_AUTOMATION_ERROR_CODES.INVALID_TIMEZONE;
|
|
1003
1128
|
return BORING_AUTOMATION_ERROR_CODES.INVALID_BODY;
|
|
1004
1129
|
}
|
|
1130
|
+
function hasBearerToken(header, expected) {
|
|
1131
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
1132
|
+
const actual = Buffer.from(header.slice(7), "utf8");
|
|
1133
|
+
const target = Buffer.from(expected, "utf8");
|
|
1134
|
+
return actual.length === target.length && timingSafeEqual(actual, target);
|
|
1135
|
+
}
|
|
1005
1136
|
function isLoopbackAddress(address) {
|
|
1006
1137
|
const normalized = address.trim().toLowerCase();
|
|
1007
1138
|
return normalized === "127.0.0.1" || normalized === "::1" || normalized.startsWith("::ffff:127.");
|
|
@@ -1021,6 +1152,10 @@ function httpStatusForStoreError(error) {
|
|
|
1021
1152
|
return 409;
|
|
1022
1153
|
case BORING_AUTOMATION_ERROR_CODES.TRIGGER_FORBIDDEN:
|
|
1023
1154
|
return 403;
|
|
1155
|
+
case BORING_AUTOMATION_ERROR_CODES.TRIGGER_UNAUTHORIZED:
|
|
1156
|
+
return 401;
|
|
1157
|
+
case BORING_AUTOMATION_ERROR_CODES.OWNER_UNAUTHORIZED:
|
|
1158
|
+
return 403;
|
|
1024
1159
|
case BORING_AUTOMATION_ERROR_CODES.RUN_EXECUTOR_UNAVAILABLE:
|
|
1025
1160
|
return 503;
|
|
1026
1161
|
case BORING_AUTOMATION_ERROR_CODES.RUN_FAILED:
|
|
@@ -1104,11 +1239,18 @@ function createBoringAutomationServerPlugin(options = {}) {
|
|
|
1104
1239
|
id: BORING_AUTOMATION_PLUGIN_ID,
|
|
1105
1240
|
label: BORING_AUTOMATION_PLUGIN_LABEL,
|
|
1106
1241
|
routes: async (app) => {
|
|
1107
|
-
await automationRoutes(app, {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1242
|
+
await automationRoutes(app, {
|
|
1243
|
+
store,
|
|
1244
|
+
storeForRequest: options.storeForRequest ? async (request) => {
|
|
1245
|
+
const actor = options.actorResolver ? await options.actorResolver(request) : void 0;
|
|
1246
|
+
if (!actor) throw new Error("automation actor resolver is unavailable");
|
|
1247
|
+
return await options.storeForRequest(request, actor);
|
|
1248
|
+
} : void 0,
|
|
1249
|
+
manualRunExecutor,
|
|
1250
|
+
dueRunService,
|
|
1251
|
+
hostedDueRunService: options.hostedDueRunService,
|
|
1252
|
+
hostedTriggerToken: options.hostedTriggerToken
|
|
1253
|
+
});
|
|
1112
1254
|
}
|
|
1113
1255
|
});
|
|
1114
1256
|
}
|
|
@@ -1126,7 +1268,14 @@ function defaultBoringAutomationServerPlugin(options, ctx) {
|
|
|
1126
1268
|
store: fallbackStore,
|
|
1127
1269
|
storeForRequest: async (_request, actor) => new PostgresAutomationStore(sql, actor),
|
|
1128
1270
|
dispatcherResolver: options?.dispatcherResolver ?? trusted.workspaceAgentDispatcherResolver,
|
|
1129
|
-
actorResolver: options?.actorResolver ?? trusted.actorResolver
|
|
1271
|
+
actorResolver: options?.actorResolver ?? trusted.actorResolver,
|
|
1272
|
+
actorVerifier: options?.actorVerifier ?? trusted.actorVerifier,
|
|
1273
|
+
hostedTriggerToken: options?.hostedTriggerToken ?? trusted.hostedAutomationTriggerToken,
|
|
1274
|
+
hostedDueRunService: options?.hostedDueRunService ?? new HostedDueRunService({
|
|
1275
|
+
sql,
|
|
1276
|
+
dispatcherResolver: options?.dispatcherResolver ?? trusted.workspaceAgentDispatcherResolver,
|
|
1277
|
+
verifyActor: options?.actorVerifier ?? trusted.actorVerifier
|
|
1278
|
+
})
|
|
1130
1279
|
});
|
|
1131
1280
|
}
|
|
1132
1281
|
return createBoringAutomationServerPlugin({
|
|
@@ -1151,6 +1300,7 @@ export {
|
|
|
1151
1300
|
BORING_AUTOMATION_ROUTE_PREFIX,
|
|
1152
1301
|
DueRunService,
|
|
1153
1302
|
FileAutomationStore,
|
|
1303
|
+
HostedDueRunService,
|
|
1154
1304
|
IdParamsSchema,
|
|
1155
1305
|
ManualRunExecutor,
|
|
1156
1306
|
PostgresAutomationStore,
|
|
@@ -1162,6 +1312,7 @@ export {
|
|
|
1162
1312
|
evaluateAutomationSchedule,
|
|
1163
1313
|
isValidFiveFieldCron,
|
|
1164
1314
|
isValidIanaTimeZone,
|
|
1315
|
+
listHostedAutomationCandidates,
|
|
1165
1316
|
parseAutomationModel,
|
|
1166
1317
|
runAlreadyActive,
|
|
1167
1318
|
runAlreadyRecorded,
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ declare const BORING_AUTOMATION_ERROR_CODES: {
|
|
|
16
16
|
readonly RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE";
|
|
17
17
|
readonly TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN";
|
|
18
18
|
readonly RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED";
|
|
19
|
+
readonly OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED";
|
|
20
|
+
readonly TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED";
|
|
19
21
|
};
|
|
20
22
|
type BoringAutomationErrorCode = typeof BORING_AUTOMATION_ERROR_CODES[keyof typeof BORING_AUTOMATION_ERROR_CODES];
|
|
21
23
|
|
package/dist/shared/index.js
CHANGED
|
@@ -15,7 +15,9 @@ var BORING_AUTOMATION_ERROR_CODES = {
|
|
|
15
15
|
RUN_ALREADY_RECORDED: "BORING_AUTOMATION_RUN_ALREADY_RECORDED",
|
|
16
16
|
RUN_EXECUTOR_UNAVAILABLE: "BORING_AUTOMATION_RUN_EXECUTOR_UNAVAILABLE",
|
|
17
17
|
TRIGGER_FORBIDDEN: "BORING_AUTOMATION_TRIGGER_FORBIDDEN",
|
|
18
|
-
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED"
|
|
18
|
+
RUN_FAILED: "BORING_AUTOMATION_RUN_FAILED",
|
|
19
|
+
OWNER_UNAUTHORIZED: "BORING_AUTOMATION_OWNER_UNAUTHORIZED",
|
|
20
|
+
TRIGGER_UNAUTHORIZED: "BORING_AUTOMATION_TRIGGER_UNAUTHORIZED"
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
// src/shared/schema.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-automation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.81",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
"fastify": "^5.9.0",
|
|
44
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
46
|
-
"@hachej/boring-workspace": "0.1.
|
|
46
|
+
"@hachej/boring-workspace": "0.1.81"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"croner": "^10.0.1",
|
|
50
50
|
"lucide-react": "^1.21.0",
|
|
51
51
|
"postgres": "^3.4.7",
|
|
52
52
|
"zod": "^3.23.0",
|
|
53
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
53
|
+
"@hachej/boring-ui-kit": "0.1.81"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"tsup": "^8.4.0",
|
|
67
67
|
"typescript": "~6.0.3",
|
|
68
68
|
"vitest": "^4.1.9",
|
|
69
|
-
"@hachej/boring-workspace": "0.1.
|
|
69
|
+
"@hachej/boring-workspace": "0.1.81"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"fastify": {
|