@k2b/cloud 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/_internal/define-app.ts +8 -1
- package/src/_internal/process-identity.ts +7 -1
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -0
- package/src/ai/admin.ts +1 -0
- package/src/ai/browser-code-contracts.ts +46 -64
- package/src/ai/browser.ts +9 -1
- package/src/ai/capabilities.ts +82 -22
- package/src/ai/chat/blocks.tsx +16 -7
- package/src/ai/chat/builtin-tools.tsx +31 -23
- package/src/ai/chat/file-tools.tsx +4 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +15 -5
- package/src/ai/chat/message-actions.tsx +123 -101
- package/src/ai/chat/message-utils.ts +30 -1
- package/src/ai/chat/messages.ts +206 -2
- package/src/ai/chat/presentation.tsx +94 -28
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-view.tsx +102 -16
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/client/controller.ts +83 -37
- package/src/ai/client/file-source.ts +20 -3
- package/src/ai/client/projection.ts +7 -2
- package/src/ai/code-mode-skill.ts +32 -36
- package/src/ai/code-runtime-tools.ts +14 -1
- package/src/ai/code-source-contracts.ts +56 -6
- package/src/ai/code-source-tools.ts +10 -3
- package/src/ai/credentials.ts +17 -3
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +15 -16
- package/src/ai/executor.ts +281 -141
- package/src/ai/file-context.ts +14 -2
- package/src/ai/file-tools.ts +17 -3
- package/src/ai/files-store.ts +134 -11
- package/src/ai/grids-skill.ts +2 -2
- package/src/ai/index.ts +9 -0
- package/src/ai/memories.ts +14 -0
- package/src/ai/migrate.ts +125 -0
- package/src/ai/model-request-settings.ts +98 -0
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/protocol.ts +6 -0
- package/src/ai/provider.ts +7 -1
- package/src/ai/quota-provider.ts +2 -2
- package/src/ai/request-headers.ts +117 -0
- package/src/ai/routes.ts +40 -6
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +9 -1
- package/src/ai/settings.ts +19 -2
- package/src/ai/skill-seeds.ts +35 -7
- package/src/ai/skills.ts +26 -0
- package/src/ai/solid.ts +1 -1
- package/src/ai/store.ts +155 -71
- package/src/ai/stream.ts +180 -37
- package/src/ai/structured.ts +20 -5
- package/src/ai/system-prompt.ts +8 -0
- package/src/ai/tool-call-names.ts +45 -0
- package/src/ai/turn-failure.ts +100 -0
- package/src/ai/turn-policy.ts +248 -0
- package/src/ai/types.ts +52 -4
- package/src/api/admin-ai-quotas.ts +36 -1
- package/src/api/admin-core-settings.ts +16 -23
- package/src/api/admin-outgoing-mail.ts +242 -0
- package/src/api/index.ts +2 -0
- package/src/browser/FileChooser.tsx +11 -0
- package/src/browser/file-chooser-messages.ts +2 -0
- package/src/cli/admin/ai-quotas.ts +70 -1
- package/src/cli/admin/index.ts +8 -0
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +215 -0
- package/src/contracts/app.ts +2 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/outgoing-mail.ts +265 -0
- package/src/contracts/registry.ts +2 -0
- package/src/services/help/store.ts +2 -1
- package/src/services/index.ts +13 -0
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +57 -5
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -45
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +112 -0
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +397 -0
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +40 -0
- package/src/services/outgoing-mail/transport.ts +17 -0
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/core-settings.ts +19 -38
- package/src/services/settings/store.ts +5 -1
- package/src/shared/ai-model-request-settings.ts +21 -0
- package/src/shared/ai-platform-prompt.ts +49 -5
- package/src/shared/ai-request-options.ts +185 -0
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/admin-navigation.ts +1 -1
- package/src/ssr/platform-messages.ts +3 -1
- package/src/ssr/workspace-navigation.ts +7 -1
- package/src/styles/effects.css +15 -17
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { sql } from "bun";
|
|
2
|
+
import { type Context, Hono, type MiddlewareHandler } from "hono";
|
|
3
|
+
import { describeRoute, validator } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
AdminMailRecordSchema,
|
|
7
|
+
MailAppAccessSchema,
|
|
8
|
+
MailProfileInputSchema,
|
|
9
|
+
MailProfileKeySchema,
|
|
10
|
+
MailRetentionSchema,
|
|
11
|
+
MailStatusSchema,
|
|
12
|
+
} from "../contracts/outgoing-mail";
|
|
13
|
+
import { type AuthContext, auth, jsonResponse, requiresAdmin } from "../server";
|
|
14
|
+
import { audit } from "../services/audit";
|
|
15
|
+
import { outgoingMailLog } from "../services/outgoing-mail/admin";
|
|
16
|
+
import { MAIL_RETENTION_KEYS, readMailRetention } from "../services/outgoing-mail/retention";
|
|
17
|
+
import { type MailAuditContext, OutgoingMailError, outgoingMailStore } from "../services/outgoing-mail/store";
|
|
18
|
+
import { outgoingMailTest } from "../services/outgoing-mail/test-send";
|
|
19
|
+
import * as settings from "../services/settings";
|
|
20
|
+
|
|
21
|
+
const retentionInput = MailRetentionSchema.refine((value) => value.recordDays >= value.contentDays, {
|
|
22
|
+
message: "Record retention must be at least content retention.",
|
|
23
|
+
path: ["recordDays"],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const validate = <Target extends "param" | "json" | "query", Schema extends z.ZodType>(
|
|
27
|
+
target: Target,
|
|
28
|
+
schema: Schema,
|
|
29
|
+
code: "invalid_profile" | "bad_input" = "invalid_profile",
|
|
30
|
+
) =>
|
|
31
|
+
validator(target, schema, (result, c) => {
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
const issue = result.error?.[0];
|
|
34
|
+
const path = issue?.path?.map((part) => (typeof part === "object" && "key" in part ? String(part.key) : String(part))).join(".");
|
|
35
|
+
return c.json(
|
|
36
|
+
{
|
|
37
|
+
code,
|
|
38
|
+
message: issue ? `${path ? `${path}: ` : ""}${issue.message}` : "Invalid outgoing mail configuration.",
|
|
39
|
+
},
|
|
40
|
+
400,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const context = (c: Context<AuthContext>): MailAuditContext => ({
|
|
45
|
+
actor: { userId: c.get("user").id, uid: c.get("user").uid, provider: c.get("user").provider, roles: c.get("user").roles },
|
|
46
|
+
requestId: c.req.header("x-request-id"),
|
|
47
|
+
});
|
|
48
|
+
const idParam = z.object({ id: z.uuid() });
|
|
49
|
+
const logQuery = z
|
|
50
|
+
.object({
|
|
51
|
+
app: z.string().min(1).optional(),
|
|
52
|
+
profile: MailProfileKeySchema.optional(),
|
|
53
|
+
status: z
|
|
54
|
+
.union([z.string(), z.array(z.string())])
|
|
55
|
+
.optional()
|
|
56
|
+
.transform((value) =>
|
|
57
|
+
value === undefined ? undefined : typeof value === "string" ? value.split(",") : value.flatMap((part) => part.split(",")),
|
|
58
|
+
)
|
|
59
|
+
.pipe(z.array(MailStatusSchema).max(6).optional()),
|
|
60
|
+
since: z.iso.datetime({ offset: true }).optional(),
|
|
61
|
+
ref: z
|
|
62
|
+
.string()
|
|
63
|
+
.min(1)
|
|
64
|
+
.max(200)
|
|
65
|
+
.optional()
|
|
66
|
+
.transform((value) => {
|
|
67
|
+
if (value === undefined) return undefined;
|
|
68
|
+
const colon = value.indexOf(":");
|
|
69
|
+
return colon < 0 ? { scope: value } : { scope: value.slice(0, colon), id: value.slice(colon + 1) };
|
|
70
|
+
})
|
|
71
|
+
.pipe(z.object({ scope: z.string().min(1).max(200), id: z.string().min(1).max(200).optional() }).optional()),
|
|
72
|
+
recipient: z.string().max(320).optional(),
|
|
73
|
+
cursor: z.string().max(1024).optional(),
|
|
74
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
const logPage = z.object({
|
|
78
|
+
items: z.array(AdminMailRecordSchema),
|
|
79
|
+
page: z.int(),
|
|
80
|
+
perPage: z.int(),
|
|
81
|
+
total: z.int(),
|
|
82
|
+
hasNext: z.boolean(),
|
|
83
|
+
nextCursor: z.string().optional(),
|
|
84
|
+
});
|
|
85
|
+
const contentResponse = z.union([
|
|
86
|
+
z.object({ purged: z.literal(true), contentPurgedAt: z.string() }),
|
|
87
|
+
z.object({
|
|
88
|
+
purged: z.literal(false),
|
|
89
|
+
text: z.string().nullable(),
|
|
90
|
+
html: z.string().nullable(),
|
|
91
|
+
headers: z.record(z.string(), z.string()).nullable(),
|
|
92
|
+
}),
|
|
93
|
+
]);
|
|
94
|
+
export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin")) =>
|
|
95
|
+
new Hono<AuthContext>()
|
|
96
|
+
.use("*", authenticate)
|
|
97
|
+
.use("*", async (c, next) => {
|
|
98
|
+
c.header("Cache-Control", "no-store");
|
|
99
|
+
await next();
|
|
100
|
+
})
|
|
101
|
+
.onError((error, c) => {
|
|
102
|
+
if (error instanceof OutgoingMailError) return c.json({ error: error.code, code: error.code, message: error.message }, error.status);
|
|
103
|
+
throw error;
|
|
104
|
+
})
|
|
105
|
+
.get(
|
|
106
|
+
"/retention",
|
|
107
|
+
describeRoute({
|
|
108
|
+
summary: "Read outgoing mail retention",
|
|
109
|
+
tags: ["Outgoing mail"],
|
|
110
|
+
...requiresAdmin,
|
|
111
|
+
responses: { 200: jsonResponse(MailRetentionSchema, "Effective retention in days") },
|
|
112
|
+
}),
|
|
113
|
+
async (c) => c.json(await readMailRetention()),
|
|
114
|
+
)
|
|
115
|
+
.put(
|
|
116
|
+
"/retention",
|
|
117
|
+
describeRoute({
|
|
118
|
+
summary: "Update outgoing mail retention",
|
|
119
|
+
tags: ["Outgoing mail"],
|
|
120
|
+
...requiresAdmin,
|
|
121
|
+
responses: {
|
|
122
|
+
200: jsonResponse(MailRetentionSchema, "Updated retention in days"),
|
|
123
|
+
400: jsonResponse(z.object({ code: z.literal("bad_input"), message: z.string() }), "Invalid retention"),
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
validate("json", retentionInput, "bad_input"),
|
|
127
|
+
async (c) => {
|
|
128
|
+
const next = c.req.valid("json");
|
|
129
|
+
const old = await readMailRetention();
|
|
130
|
+
await sql.begin(async (tx) => {
|
|
131
|
+
await settings.set(MAIL_RETENTION_KEYS[0], next.contentDays, tx);
|
|
132
|
+
await settings.set(MAIL_RETENTION_KEYS[1], next.recordDays, tx);
|
|
133
|
+
await audit.record(
|
|
134
|
+
{
|
|
135
|
+
...context(c),
|
|
136
|
+
action: "outgoing_mail.retention.update",
|
|
137
|
+
outcome: "allowed",
|
|
138
|
+
target: { type: "outgoing_mail_retention" },
|
|
139
|
+
metadata: { old, new: next },
|
|
140
|
+
},
|
|
141
|
+
tx,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
await settings.invalidateSettingsCache(MAIL_RETENTION_KEYS);
|
|
145
|
+
return c.json(next);
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
.get(
|
|
149
|
+
"/messages",
|
|
150
|
+
describeRoute({
|
|
151
|
+
summary: "List outgoing mail metadata",
|
|
152
|
+
tags: ["Outgoing mail"],
|
|
153
|
+
...requiresAdmin,
|
|
154
|
+
responses: { 200: jsonResponse(logPage, "Send log") },
|
|
155
|
+
}),
|
|
156
|
+
validate("query", logQuery, "bad_input"),
|
|
157
|
+
async (c) => {
|
|
158
|
+
const { cursor, limit, ...filter } = c.req.valid("query");
|
|
159
|
+
return c.json(await outgoingMailLog.list(filter, { cursor, perPage: limit }));
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
.get(
|
|
163
|
+
"/messages/:id",
|
|
164
|
+
describeRoute({
|
|
165
|
+
summary: "Read outgoing mail metadata",
|
|
166
|
+
tags: ["Outgoing mail"],
|
|
167
|
+
...requiresAdmin,
|
|
168
|
+
responses: { 200: jsonResponse(AdminMailRecordSchema, "Message metadata") },
|
|
169
|
+
}),
|
|
170
|
+
validate("param", idParam, "bad_input"),
|
|
171
|
+
async (c) => c.json(await outgoingMailLog.get(c.req.valid("param").id)),
|
|
172
|
+
)
|
|
173
|
+
.get(
|
|
174
|
+
"/messages/:id/content",
|
|
175
|
+
describeRoute({
|
|
176
|
+
summary: "Read outgoing mail content with audit",
|
|
177
|
+
tags: ["Outgoing mail"],
|
|
178
|
+
...requiresAdmin,
|
|
179
|
+
responses: { 200: jsonResponse(contentResponse, "Message content or purge marker") },
|
|
180
|
+
}),
|
|
181
|
+
validate("param", idParam, "bad_input"),
|
|
182
|
+
async (c) => c.json(await outgoingMailLog.content(c.req.valid("param").id, context(c))),
|
|
183
|
+
)
|
|
184
|
+
.post(
|
|
185
|
+
"/messages/:id/cancel",
|
|
186
|
+
describeRoute({
|
|
187
|
+
summary: "Cancel queued outgoing mail",
|
|
188
|
+
tags: ["Outgoing mail"],
|
|
189
|
+
...requiresAdmin,
|
|
190
|
+
responses: {
|
|
191
|
+
200: jsonResponse(AdminMailRecordSchema, "Cancelled message"),
|
|
192
|
+
409: jsonResponse(
|
|
193
|
+
z.object({ code: z.literal("message_not_queued"), error: z.literal("message_not_queued"), message: z.string() }),
|
|
194
|
+
"Only queued mail can be cancelled",
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
validate("param", idParam, "bad_input"),
|
|
199
|
+
async (c) => c.json(await outgoingMailLog.cancel(c.req.valid("param").id, context(c))),
|
|
200
|
+
)
|
|
201
|
+
.get("/profiles", async (c) => c.json({ items: await outgoingMailStore.list() }))
|
|
202
|
+
.post(
|
|
203
|
+
"/batches/:batchId/cancel",
|
|
204
|
+
describeRoute({
|
|
205
|
+
summary: "Cancel queued outgoing mail in a batch",
|
|
206
|
+
tags: ["Outgoing mail"],
|
|
207
|
+
...requiresAdmin,
|
|
208
|
+
responses: {
|
|
209
|
+
200: jsonResponse(z.object({ batchId: z.uuid(), cancelled: z.int().nonnegative() }), "Cancelled queued messages"),
|
|
210
|
+
404: jsonResponse(
|
|
211
|
+
z.object({ code: z.literal("batch_unknown"), error: z.literal("batch_unknown"), message: z.string() }),
|
|
212
|
+
"Unknown batch",
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
validate("param", z.object({ batchId: z.uuid() }), "bad_input"),
|
|
217
|
+
async (c) => c.json(await outgoingMailLog.cancelBatch(c.req.valid("param").batchId, context(c))),
|
|
218
|
+
)
|
|
219
|
+
.get("/profiles/:key", async (c) => c.json(await outgoingMailStore.get(c.req.param("key"))))
|
|
220
|
+
.put(
|
|
221
|
+
"/profiles/:key",
|
|
222
|
+
validate("param", z.object({ key: MailProfileKeySchema })),
|
|
223
|
+
validate("json", MailProfileInputSchema),
|
|
224
|
+
async (c) => {
|
|
225
|
+
const result = await outgoingMailStore.put(c.req.valid("param").key, c.req.valid("json"), context(c));
|
|
226
|
+
return c.json(result.profile, result.created ? 201 : 200);
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
.post("/profiles/:key/default", async (c) => c.json(await outgoingMailStore.setDefault(c.req.param("key")!, context(c))))
|
|
230
|
+
.delete("/profiles/:key", async (c) => {
|
|
231
|
+
await outgoingMailStore.delete(c.req.param("key")!, context(c));
|
|
232
|
+
return c.body(null, 204);
|
|
233
|
+
})
|
|
234
|
+
.post("/profiles/:key/test", validate("json", z.object({ recipient: z.email() }).strict()), async (c) => {
|
|
235
|
+
await outgoingMailTest.send(c.req.param("key")!, c.req.valid("json").recipient, context(c));
|
|
236
|
+
return c.json({ ok: true as const });
|
|
237
|
+
})
|
|
238
|
+
.get("/apps", async (c) => c.json(await outgoingMailStore.apps()))
|
|
239
|
+
.put("/apps/:appId", validate("json", MailAppAccessSchema), async (c) =>
|
|
240
|
+
c.json(await outgoingMailStore.setAppAccess(c.req.param("appId")!, c.req.valid("json"), context(c))),
|
|
241
|
+
);
|
|
242
|
+
export default createAdminOutgoingMailRoutes();
|
package/src/api/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
|
29
29
|
import adminIdentityRoutes from "./admin-identity";
|
|
30
30
|
import adminLifecycleRoutes from "./admin-lifecycle";
|
|
31
31
|
import adminLinuxIdentityRoutes from "./admin-linux-identities";
|
|
32
|
+
import adminOutgoingMailRoutes from "./admin-outgoing-mail";
|
|
32
33
|
import adminRailRoutes from "./admin-rail";
|
|
33
34
|
import { adminAnnouncementRoutes, announcementRoutes } from "./announcements";
|
|
34
35
|
import { createAppApprovalRoutes } from "./app-approval";
|
|
@@ -77,6 +78,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
77
78
|
.route("/admin/core/ai-projects", adminAiProjectsRoutes)
|
|
78
79
|
.route("/admin/core/ai-skills", adminAiSkillsRoutes)
|
|
79
80
|
.route("/admin/core/announcements", adminAnnouncementRoutes)
|
|
81
|
+
.route("/admin/core/outgoing-mail", adminOutgoingMailRoutes)
|
|
80
82
|
.route("/admin/core/settings", adminCoreSettingsRoutes)
|
|
81
83
|
.route("/admin/core/rail", adminRailRoutes)
|
|
82
84
|
.route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
announce,
|
|
5
5
|
Button,
|
|
6
6
|
createCollectionSelection,
|
|
7
|
+
FileDropTarget,
|
|
7
8
|
FileGrid,
|
|
8
9
|
Format,
|
|
9
10
|
IconButton,
|
|
@@ -324,6 +325,16 @@ export function FileChooser(props: {
|
|
|
324
325
|
{/* `display: contents`, so header, body, and footer stay the panel's own rows. */}
|
|
325
326
|
<div ref={root} class="cloud-file-chooser">
|
|
326
327
|
<PanelDialog.Header title={t().title({ multiple })} close={() => props.close([])} />
|
|
328
|
+
{/* Files dropped anywhere on the open chooser are taken like files picked from this device. */}
|
|
329
|
+
<FileDropTarget
|
|
330
|
+
label={t().dropToChoose({ multiple })}
|
|
331
|
+
accept={props.options.accept}
|
|
332
|
+
multiple={multiple}
|
|
333
|
+
disabled={downloading()}
|
|
334
|
+
onDrop={(files) => {
|
|
335
|
+
if (files.length > 0) props.close(files);
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
327
338
|
<div class="cloud-file-chooser__toolbar">
|
|
328
339
|
<nav class="cloud-file-chooser__crumbs" aria-label={t().breadcrumbs}>
|
|
329
340
|
<IconButton size="sm" variant="ghost" label={t().up} disabled={!location() || downloading()} onClick={up}>
|
|
@@ -8,6 +8,7 @@ export const fileChooserMessages = i18n.define({
|
|
|
8
8
|
sources: "Sources",
|
|
9
9
|
sourceList: "Where the files come from",
|
|
10
10
|
device: "This device",
|
|
11
|
+
dropToChoose: ({ multiple }: { multiple: boolean }): string => (multiple ? "Drop to add these files" : "Drop to add this file"),
|
|
11
12
|
providersLoading: "Looking for apps with files…",
|
|
12
13
|
providersFailed: "Apps with files could not be loaded.",
|
|
13
14
|
retry: "Try again",
|
|
@@ -48,6 +49,7 @@ export const fileChooserMessages = i18n.define({
|
|
|
48
49
|
sources: "Quellen",
|
|
49
50
|
sourceList: "Woher die Dateien kommen",
|
|
50
51
|
device: "Dieses Gerät",
|
|
52
|
+
dropToChoose: ({ multiple }) => (multiple ? "Ablegen, um diese Dateien hinzuzufügen" : "Ablegen, um diese Datei hinzuzufügen"),
|
|
51
53
|
providersLoading: "Apps mit Dateien werden gesucht…",
|
|
52
54
|
providersFailed: "Apps mit Dateien konnten nicht geladen werden.",
|
|
53
55
|
retry: "Erneut versuchen",
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type AiModelPricing, AiModelPricingSchema } from "../../shared/ai-costs";
|
|
2
|
+
import {
|
|
3
|
+
type AiModelRequestSettings,
|
|
4
|
+
AiModelRequestSettingsSchema,
|
|
5
|
+
AiModelRequestSettingsUpdateSchema,
|
|
6
|
+
} from "../../shared/ai-model-request-settings";
|
|
2
7
|
import {
|
|
3
8
|
type AiQuotaConfig,
|
|
4
9
|
AiQuotaConfigSchema,
|
|
@@ -7,7 +12,7 @@ import {
|
|
|
7
12
|
AiQuotaReportQuerySchema,
|
|
8
13
|
AiQuotaResetSchema,
|
|
9
14
|
} from "../../shared/ai-quotas";
|
|
10
|
-
import { type CloudCliContext, cliText, command, confirmFlag, flag, printRows, printStructured } from "../index";
|
|
15
|
+
import { type CloudCliContext, cliText, command, confirmFlag, flag, printRows, printStructured, readCliInput } from "../index";
|
|
11
16
|
import { apiGet, apiJson, queryString, readJsonInput } from "./shared";
|
|
12
17
|
|
|
13
18
|
const path = "/api/admin/core/ai-quotas";
|
|
@@ -23,6 +28,70 @@ const confirm = (ctx: CloudCliContext, yes: boolean) => {
|
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
export const aiQuotaCommands = [
|
|
31
|
+
command("ai models settings get", {
|
|
32
|
+
summary: "Read model thinking level, extra parameters and configured header names",
|
|
33
|
+
flags: { id: flag.string({ required: true, description: "Model profile ID" }) },
|
|
34
|
+
async run({ ctx, flags }) {
|
|
35
|
+
print(ctx, AiModelRequestSettingsSchema.parse(await apiGet(ctx, `${path}/models/${encodeURIComponent(flags.id!)}/settings`)));
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
command("ai models settings set", {
|
|
39
|
+
summary: "Patch one model's request settings; header values are write-only",
|
|
40
|
+
flags: {
|
|
41
|
+
id: flag.string({ required: true, description: "Model profile ID" }),
|
|
42
|
+
thinkingLevel: flag.string({ name: "thinking-level", description: "Provider-specific thinking level" }),
|
|
43
|
+
clearThinkingLevel: flag.boolean({ name: "clear-thinking-level", description: "Use the model's default thinking level" }),
|
|
44
|
+
extraBody: flag.input({ name: "extra-body", description: "Extra parameters JSON: --extra-body, --extra-body-file or --stdin" }),
|
|
45
|
+
clearExtraBody: flag.boolean({ name: "clear-extra-body", description: "Remove extra parameters" }),
|
|
46
|
+
headers: flag.input({
|
|
47
|
+
stdinName: "headers-stdin",
|
|
48
|
+
description: "Header patch JSON; use --headers-file or --headers-stdin for secrets",
|
|
49
|
+
}),
|
|
50
|
+
clearHeaders: flag.boolean({ name: "clear-headers", description: "Remove all configured extra headers before applying the patch" }),
|
|
51
|
+
yes: confirmFlag("Confirm model request settings change"),
|
|
52
|
+
},
|
|
53
|
+
async run({ ctx, flags }) {
|
|
54
|
+
if (!flags.yes) throw new Error("Model request settings changes require --yes.");
|
|
55
|
+
if (flags.thinkingLevel !== undefined && flags.clearThinkingLevel)
|
|
56
|
+
throw new Error("Choose --thinking-level or --clear-thinking-level.");
|
|
57
|
+
if (flags.extraBody.provided && flags.clearExtraBody) throw new Error("Choose --extra-body or --clear-extra-body.");
|
|
58
|
+
if (
|
|
59
|
+
!flags.extraBody.provided &&
|
|
60
|
+
!flags.headers.provided &&
|
|
61
|
+
flags.thinkingLevel === undefined &&
|
|
62
|
+
!flags.clearThinkingLevel &&
|
|
63
|
+
!flags.clearExtraBody &&
|
|
64
|
+
!flags.clearHeaders
|
|
65
|
+
)
|
|
66
|
+
throw new Error("Select at least one model request setting to change.");
|
|
67
|
+
if (flags.extraBody.source === "stdin" && flags.headers.source === "stdin")
|
|
68
|
+
throw new Error("Read one JSON input from stdin and supply the other from a file.");
|
|
69
|
+
const parseInput = async (input: typeof flags.headers, label: string): Promise<unknown> => {
|
|
70
|
+
const raw = await readCliInput(input, { required: true, label });
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(raw!);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error(`Invalid ${label} JSON.`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const patch: Record<string, unknown> = {};
|
|
78
|
+
if (flags.thinkingLevel !== undefined || flags.clearThinkingLevel)
|
|
79
|
+
patch.reasoningEffort = flags.clearThinkingLevel ? null : flags.thinkingLevel;
|
|
80
|
+
if (flags.extraBody.provided || flags.clearExtraBody)
|
|
81
|
+
patch.extraBody = flags.clearExtraBody ? null : await parseInput(flags.extraBody, "extra parameters");
|
|
82
|
+
if (flags.headers.provided) patch.requestHeaders = await parseInput(flags.headers, "extra headers");
|
|
83
|
+
if (flags.clearHeaders) patch.clearRequestHeaders = true;
|
|
84
|
+
// Validate locally before reading the current revision or sending secrets.
|
|
85
|
+
const valid = AiModelRequestSettingsUpdateSchema.parse({ ...patch, expected: "pending" });
|
|
86
|
+
const current = await apiGet<AiModelRequestSettings>(ctx, `${path}/models/${encodeURIComponent(flags.id!)}/settings`);
|
|
87
|
+
print(
|
|
88
|
+
ctx,
|
|
89
|
+
AiModelRequestSettingsSchema.parse(
|
|
90
|
+
await apiJson(ctx, "PUT", `${path}/models/${encodeURIComponent(flags.id!)}/settings`, { ...valid, expected: current.revision }),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
26
95
|
command("ai models pricing get", {
|
|
27
96
|
summary: "List configured model input/output reference prices per million tokens",
|
|
28
97
|
async run({ ctx }) {
|
package/src/cli/admin/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { logCommands } from "./logs";
|
|
|
25
25
|
import { metricsCommands } from "./metrics";
|
|
26
26
|
import { natsCommands } from "./nats";
|
|
27
27
|
import { notificationCommands } from "./notifications";
|
|
28
|
+
import { outgoingMailCommands } from "./outgoing-mail";
|
|
28
29
|
import { syncCommands } from "./sync";
|
|
29
30
|
import { telemetryCommands } from "./telemetry";
|
|
30
31
|
import { webhookCommands } from "./webhooks";
|
|
@@ -38,6 +39,7 @@ export default defineCliCommands({
|
|
|
38
39
|
ai: "Manage AI models, quotas, Skills, and usage.",
|
|
39
40
|
"ai models": "Manage AI model configuration.",
|
|
40
41
|
"ai models pricing": "Manage model reference prices.",
|
|
42
|
+
"ai models settings": "Manage model thinking levels, extra parameters, and extra headers.",
|
|
41
43
|
"ai quotas background": "Inspect and release the background emergency stop.",
|
|
42
44
|
"ai skills": "Inspect Assistant Skills and trusted templates.",
|
|
43
45
|
"ai usage": "Inspect AI runs and usage facets.",
|
|
@@ -55,6 +57,11 @@ export default defineCliCommands({
|
|
|
55
57
|
"nats consumers": "Inspect NATS consumers.",
|
|
56
58
|
"nats streams": "Find NATS streams and buckets.",
|
|
57
59
|
"notification-batches": "Manage account notification batches.",
|
|
60
|
+
"outgoing-mail": "Manage outgoing mail sender profiles, application access, and the send log.",
|
|
61
|
+
"outgoing-mail profiles": "Manage outgoing mail sender profiles.",
|
|
62
|
+
"outgoing-mail apps": "Manage application outgoing mail access.",
|
|
63
|
+
"outgoing-mail log": "Read and cancel application mail in the send log.",
|
|
64
|
+
"outgoing-mail retention": "Show or change how long the send log keeps mail.",
|
|
58
65
|
notifications: "Inspect and resend email notifications.",
|
|
59
66
|
postgres: "Inspect Postgres diagnostics.",
|
|
60
67
|
redis: "Inspect Valkey diagnostics.",
|
|
@@ -96,6 +103,7 @@ export default defineCliCommands({
|
|
|
96
103
|
...workflowCommands,
|
|
97
104
|
...dataCommands,
|
|
98
105
|
...notificationCommands,
|
|
106
|
+
...outgoingMailCommands,
|
|
99
107
|
...webhookCommands,
|
|
100
108
|
...metricsCommands,
|
|
101
109
|
...natsCommands,
|
|
@@ -54,6 +54,8 @@ export type NotificationBatchRecipient = {
|
|
|
54
54
|
profile: "user" | "guest";
|
|
55
55
|
status: NotificationRecipientStatus;
|
|
56
56
|
notificationId: string | null;
|
|
57
|
+
outgoingMailId: string | null;
|
|
58
|
+
outgoingMailStatus: string | null;
|
|
57
59
|
error: string | null;
|
|
58
60
|
attemptCount: number;
|
|
59
61
|
sentAt: string | null;
|
|
@@ -370,6 +372,8 @@ export const notificationCommands = [
|
|
|
370
372
|
provider: recipient.provider,
|
|
371
373
|
profile: recipient.profile,
|
|
372
374
|
attempts: recipient.attemptCount,
|
|
375
|
+
mailId: recipient.outgoingMailId ?? "",
|
|
376
|
+
mailStatus: recipient.outgoingMailStatus ?? "",
|
|
373
377
|
error: truncate(recipient.error, 60),
|
|
374
378
|
userId: recipient.userId,
|
|
375
379
|
}));
|
|
@@ -381,6 +385,8 @@ export const notificationCommands = [
|
|
|
381
385
|
{ key: "provider" },
|
|
382
386
|
{ key: "profile" },
|
|
383
387
|
{ key: "attempts" },
|
|
388
|
+
{ key: "mailId" },
|
|
389
|
+
{ key: "mailStatus" },
|
|
384
390
|
{ key: "error" },
|
|
385
391
|
{ key: "userId" },
|
|
386
392
|
]);
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdminMailApp,
|
|
3
|
+
type AdminMailProfile,
|
|
4
|
+
type AdminMailRecord,
|
|
5
|
+
type MailPage,
|
|
6
|
+
MailProfileInputSchema,
|
|
7
|
+
type MailRetention,
|
|
8
|
+
MailRetentionSchema,
|
|
9
|
+
} from "../../contracts/outgoing-mail";
|
|
10
|
+
import { arg, type CloudCliContext, command, confirmFlag, flag, printStructured, readCliInput } from "../index";
|
|
11
|
+
import { apiGet, apiJson, printJsonOrTable } from "./shared";
|
|
12
|
+
|
|
13
|
+
const root = "/api/admin/core/outgoing-mail";
|
|
14
|
+
const profilePath = (key: string) => `${root}/profiles/${encodeURIComponent(key)}`;
|
|
15
|
+
const keyArgs = { key: arg.required({ valueLabel: "key" }) };
|
|
16
|
+
const print = (ctx: CloudCliContext, value: unknown) => {
|
|
17
|
+
if (ctx.options.output === "json") ctx.json(value);
|
|
18
|
+
else ctx.print(JSON.stringify(value, null, 2));
|
|
19
|
+
};
|
|
20
|
+
const confirmed = (yes: boolean) => {
|
|
21
|
+
if (!yes) throw new Error("Outgoing mail policy changes require --yes.");
|
|
22
|
+
};
|
|
23
|
+
export const outgoingMailCommands = [
|
|
24
|
+
command("outgoing-mail retention show", {
|
|
25
|
+
summary: "Show outgoing mail retention in days",
|
|
26
|
+
async run({ ctx }) {
|
|
27
|
+
const result = await apiGet<MailRetention>(ctx, `${root}/retention`);
|
|
28
|
+
if (!printStructured(ctx, result)) print(ctx, result);
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
command("outgoing-mail retention set", {
|
|
32
|
+
summary: "Set outgoing mail content and record retention",
|
|
33
|
+
flags: {
|
|
34
|
+
contentDays: flag.string({ required: true, description: "Content retention in days" }),
|
|
35
|
+
recordDays: flag.string({ required: true, description: "Record retention in days (at least content retention)" }),
|
|
36
|
+
yes: confirmFlag("Confirm changing outgoing mail retention"),
|
|
37
|
+
},
|
|
38
|
+
async run({ ctx, flags }) {
|
|
39
|
+
confirmed(flags.yes);
|
|
40
|
+
const retention = MailRetentionSchema.parse({ contentDays: Number(flags.contentDays), recordDays: Number(flags.recordDays) });
|
|
41
|
+
if (retention.recordDays < retention.contentDays) throw new Error("Record retention must be at least content retention.");
|
|
42
|
+
const result = await apiJson<MailRetention>(ctx, "PUT", `${root}/retention`, retention);
|
|
43
|
+
if (!printStructured(ctx, result)) print(ctx, result);
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
command("outgoing-mail log list", {
|
|
47
|
+
summary: "List outgoing mail metadata",
|
|
48
|
+
flags: {
|
|
49
|
+
app: flag.string({ description: "Application ID" }),
|
|
50
|
+
profile: flag.string({ description: "Sender profile key" }),
|
|
51
|
+
status: flag.string({ description: "Comma-separated statuses" }),
|
|
52
|
+
since: flag.string({ description: "ISO timestamp" }),
|
|
53
|
+
ref: flag.string({ description: "Reference scope[:id]" }),
|
|
54
|
+
recipient: flag.string({ description: "Recipient substring" }),
|
|
55
|
+
cursor: flag.string({ description: "Next-page cursor" }),
|
|
56
|
+
limit: flag.int({ description: "Page size (1–100)" }),
|
|
57
|
+
},
|
|
58
|
+
async run({ ctx, flags }) {
|
|
59
|
+
if (flags.limit !== undefined && (!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100))
|
|
60
|
+
throw new Error("--limit must be a whole number from 1 to 100.");
|
|
61
|
+
const query = new URLSearchParams();
|
|
62
|
+
for (const [key, value] of Object.entries(flags)) if (value !== undefined) query.set(key, String(value));
|
|
63
|
+
const result = await apiGet<MailPage<AdminMailRecord>>(ctx, `${root}/messages${query.size ? `?${query}` : ""}`);
|
|
64
|
+
printJsonOrTable(ctx, result, result.items, [
|
|
65
|
+
{ key: "id" },
|
|
66
|
+
{ key: "appId" },
|
|
67
|
+
{ key: "profile" },
|
|
68
|
+
{ key: "status" },
|
|
69
|
+
{ key: "createdAt" },
|
|
70
|
+
]);
|
|
71
|
+
if (ctx.options.output === "text" && result.nextCursor) ctx.print(`Next cursor: ${result.nextCursor}`);
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
command("outgoing-mail log show", {
|
|
75
|
+
summary: "Read outgoing mail metadata, or audited content",
|
|
76
|
+
args: { id: arg.required({ valueLabel: "id" }) },
|
|
77
|
+
flags: { content: flag.boolean({ description: "Read message content (audited)" }) },
|
|
78
|
+
async run({ ctx, args, flags }) {
|
|
79
|
+
print(ctx, await apiGet(ctx, `${root}/messages/${encodeURIComponent(args.id)}${flags.content ? "/content" : ""}`));
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
command("outgoing-mail log cancel", {
|
|
83
|
+
summary: "Cancel queued outgoing mail",
|
|
84
|
+
args: { id: arg.optional({ valueLabel: "id" }) },
|
|
85
|
+
flags: {
|
|
86
|
+
batch: flag.string({ description: "Cancel queued messages in this batch instead of one ID" }),
|
|
87
|
+
yes: confirmFlag("Confirm cancelling queued outgoing mail"),
|
|
88
|
+
},
|
|
89
|
+
async run({ ctx, args, flags }) {
|
|
90
|
+
if (!flags.yes) throw new Error("Cancelling outgoing mail requires --yes.");
|
|
91
|
+
if ((args.id !== undefined) === (flags.batch !== undefined)) throw new Error("Choose exactly one message ID or --batch <batchId>.");
|
|
92
|
+
const path = flags.batch !== undefined ? `batches/${encodeURIComponent(flags.batch)}` : `messages/${encodeURIComponent(args.id!)}`;
|
|
93
|
+
print(ctx, await apiJson(ctx, "POST", `${root}/${path}/cancel`));
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
command("outgoing-mail profiles list", {
|
|
97
|
+
summary: "List outgoing mail sender profiles",
|
|
98
|
+
async run({ ctx }) {
|
|
99
|
+
const result = await apiGet<{ items: AdminMailProfile[] }>(ctx, `${root}/profiles`);
|
|
100
|
+
printJsonOrTable(
|
|
101
|
+
ctx,
|
|
102
|
+
result,
|
|
103
|
+
result.items.map((p) => ({
|
|
104
|
+
key: p.key,
|
|
105
|
+
name: p.name,
|
|
106
|
+
from: p.fromAddress,
|
|
107
|
+
default: p.isDefault,
|
|
108
|
+
revision: p.revision,
|
|
109
|
+
imap: p.imap ? `${p.imap.host}/${p.imap.folder}` : "off",
|
|
110
|
+
bounces: p.bounces ? (p.bounces.error ?? p.bounces.checkedAt ?? "pending") : "off",
|
|
111
|
+
})),
|
|
112
|
+
[{ key: "key" }, { key: "name" }, { key: "from" }, { key: "default" }, { key: "revision" }, { key: "imap" }, { key: "bounces" }],
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
command("outgoing-mail profiles get", {
|
|
117
|
+
summary: "Read one outgoing mail sender profile",
|
|
118
|
+
args: keyArgs,
|
|
119
|
+
async run({ ctx, args }) {
|
|
120
|
+
const profile = await apiGet<AdminMailProfile>(ctx, profilePath(args.key));
|
|
121
|
+
print(ctx, profile);
|
|
122
|
+
if (ctx.options.output === "text")
|
|
123
|
+
ctx.print(`Bounces: ${profile.bounces ? (profile.bounces.error ?? profile.bounces.checkedAt ?? "pending") : "off"}`);
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
command("outgoing-mail profiles put", {
|
|
127
|
+
summary: "Create or replace a sender profile with its current revision",
|
|
128
|
+
args: keyArgs,
|
|
129
|
+
flags: {
|
|
130
|
+
config: flag.input({ required: true, description: "Profile JSON; SMTP and IMAP passwords only via --config-file or --stdin" }),
|
|
131
|
+
},
|
|
132
|
+
async run({ ctx, args, flags }) {
|
|
133
|
+
const input = await readCliInput(flags.config, { label: "outgoing mail profile", required: true });
|
|
134
|
+
let raw: unknown;
|
|
135
|
+
try {
|
|
136
|
+
raw = JSON.parse(input ?? "");
|
|
137
|
+
} catch {
|
|
138
|
+
throw new Error("Invalid outgoing mail profile JSON.");
|
|
139
|
+
}
|
|
140
|
+
if (flags.config.source === "value" && raw && typeof raw === "object" && "smtpPassword" in raw)
|
|
141
|
+
throw new Error("Pass smtpPassword only through --config-file or --stdin.");
|
|
142
|
+
const parsed = MailProfileInputSchema.safeParse(raw);
|
|
143
|
+
if (!parsed.success) throw new Error("Invalid outgoing mail profile configuration.");
|
|
144
|
+
if (flags.config.source === "value" && parsed.data.imap && "password" in parsed.data.imap)
|
|
145
|
+
throw new Error("Pass imap.password only through --config-file or --stdin.");
|
|
146
|
+
print(ctx, await apiJson(ctx, "PUT", profilePath(args.key), parsed.data));
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
command("outgoing-mail profiles set-default", {
|
|
150
|
+
summary: "Choose the default outgoing mail sender",
|
|
151
|
+
args: keyArgs,
|
|
152
|
+
flags: { yes: confirmFlag("Confirm changing the default sender") },
|
|
153
|
+
async run({ ctx, args, flags }) {
|
|
154
|
+
confirmed(flags.yes);
|
|
155
|
+
print(ctx, await apiJson(ctx, "POST", `${profilePath(args.key)}/default`));
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
command("outgoing-mail profiles delete", {
|
|
159
|
+
summary: "Delete a sender profile that is not the default",
|
|
160
|
+
args: keyArgs,
|
|
161
|
+
flags: { yes: confirmFlag("Confirm deleting the sender profile") },
|
|
162
|
+
async run({ ctx, args, flags }) {
|
|
163
|
+
confirmed(flags.yes);
|
|
164
|
+
const response = await ctx.fetch(profilePath(args.key), { method: "DELETE" });
|
|
165
|
+
if (!response.ok) await ctx.readJson(response);
|
|
166
|
+
print(ctx, { deleted: args.key });
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
command("outgoing-mail profiles test", {
|
|
170
|
+
summary: "Send a test email through a sender profile",
|
|
171
|
+
args: keyArgs,
|
|
172
|
+
flags: { to: flag.string({ required: true, description: "Recipient email address" }) },
|
|
173
|
+
async run({ ctx, args, flags }) {
|
|
174
|
+
print(ctx, await apiJson(ctx, "POST", `${profilePath(args.key)}/test`, { recipient: flags.to }));
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
command("outgoing-mail apps list", {
|
|
178
|
+
summary: "List application outgoing mail permissions and profile access",
|
|
179
|
+
async run({ ctx }) {
|
|
180
|
+
const result = await apiGet<{ defaultProfile: string | null; items: AdminMailApp[] }>(ctx, `${root}/apps`);
|
|
181
|
+
printJsonOrTable(
|
|
182
|
+
ctx,
|
|
183
|
+
result,
|
|
184
|
+
result.items.map((app) => ({ ...app, profiles: app.profiles.join(",") })),
|
|
185
|
+
[{ key: "appId" }, { key: "name" }, { key: "declared" }, { key: "mode" }, { key: "profiles" }],
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
command("outgoing-mail apps set", {
|
|
190
|
+
summary: "Set default, selected, or blocked outgoing mail access",
|
|
191
|
+
args: { app: arg.required({ valueLabel: "app" }) },
|
|
192
|
+
flags: {
|
|
193
|
+
default: flag.boolean({ description: "Use the default sender" }),
|
|
194
|
+
profiles: flag.string({ description: "Comma-separated profile keys" }),
|
|
195
|
+
none: flag.boolean({ description: "Block sending with an empty selected list" }),
|
|
196
|
+
yes: confirmFlag("Confirm application outgoing mail access change"),
|
|
197
|
+
},
|
|
198
|
+
async run({ ctx, args, flags }) {
|
|
199
|
+
confirmed(flags.yes);
|
|
200
|
+
if ([flags.default, flags.none, flags.profiles !== undefined].filter(Boolean).length !== 1)
|
|
201
|
+
throw new Error("Choose exactly one of --default, --profiles or --none.");
|
|
202
|
+
const profiles = flags.profiles?.split(",").map((key) => key.trim());
|
|
203
|
+
if (profiles?.some((key) => !key)) throw new Error("--profiles requires non-empty profile keys; use --none to block sending.");
|
|
204
|
+
print(
|
|
205
|
+
ctx,
|
|
206
|
+
await apiJson(
|
|
207
|
+
ctx,
|
|
208
|
+
"PUT",
|
|
209
|
+
`${root}/apps/${encodeURIComponent(args.app)}`,
|
|
210
|
+
flags.default ? { mode: "default" } : { mode: "selected", profiles: profiles ?? [] },
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
];
|