@k2b/cloud 0.28.0 → 0.29.1
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 +9 -6
- package/src/_internal/cli-plugins.ts +18 -0
- package/src/_internal/define-app.ts +21 -5
- package/src/_internal/registry-validation.ts +30 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.tsx +11 -4
- package/src/ai/assistant-models.ts +25 -4
- package/src/ai/audio-tool.ts +3 -10
- package/src/ai/capabilities.ts +35 -9
- package/src/ai/chat/blocks.tsx +259 -197
- package/src/ai/chat/builtin-tools.tsx +3 -1
- package/src/ai/chat/composer-adapter.ts +8 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +16 -3
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +28 -6
- package/src/ai/chat/messages.ts +20 -24
- package/src/ai/chat/presentation.tsx +1 -8
- package/src/ai/chat/tool-disclosure.tsx +21 -14
- package/src/ai/chat/tool-groups.ts +64 -37
- package/src/ai/chat/tool-names.ts +157 -0
- package/src/ai/chat/turn-layout.ts +14 -2
- package/src/ai/chat/turn-view.tsx +132 -70
- package/src/ai/chat-tasks.ts +16 -3
- package/src/ai/code-mode-skill.ts +4 -4
- package/src/ai/compaction.ts +22 -3
- package/src/ai/default-tools.ts +9 -2
- package/src/ai/executor.ts +127 -40
- package/src/ai/file-media-type.ts +4 -0
- package/src/ai/files-store.ts +17 -10
- package/src/ai/http.ts +11 -10
- package/src/ai/index.ts +1 -0
- package/src/ai/memories.ts +2 -0
- package/src/ai/memory-learning.ts +59 -5
- package/src/ai/memory-workflow-evidence.ts +35 -10
- package/src/ai/projects-routes.ts +5 -1
- package/src/ai/projects.ts +5 -1
- package/src/ai/protocol.ts +43 -5
- package/src/ai/provider.ts +16 -1
- package/src/ai/quota-provider.ts +4 -1
- package/src/ai/routes.ts +20 -10
- package/src/ai/settings.ts +54 -17
- package/src/ai/skill-seeds.ts +11 -3
- package/src/ai/store.ts +76 -33
- package/src/ai/structured.ts +2 -1
- package/src/ai/system-prompt.ts +5 -1
- package/src/ai/transcription.ts +58 -13
- package/src/ai/turn-failure.ts +5 -1
- package/src/ai/turn-guidance.ts +505 -0
- package/src/ai/types.ts +3 -1
- package/src/ai/ui.tsx +1 -0
- package/src/api/admin-core-settings.ts +16 -1
- package/src/api/admin-outgoing-mail.ts +18 -0
- package/src/api/me.ts +42 -1
- package/src/browser/FileChooser.tsx +71 -295
- package/src/browser/FileSaver.tsx +407 -0
- package/src/browser/app-badges.ts +16 -0
- package/src/browser/choose-files.browser-harness.tsx +22 -6
- package/src/browser/choose-files.tsx +23 -36
- package/src/browser/file-chooser-messages.ts +64 -0
- package/src/browser/file-providers.ts +258 -8
- package/src/browser/files.tsx +26 -3
- package/src/browser/notifications.ts +37 -1
- package/src/browser/provider-folder.tsx +301 -0
- package/src/browser/provider-list.ts +39 -0
- package/src/browser/save-files.browser-harness.tsx +35 -0
- package/src/browser/save-files.tsx +107 -0
- package/src/cli/admin/outgoing-mail.ts +30 -2
- package/src/contracts/app.ts +10 -0
- package/src/contracts/commands.ts +3 -1
- package/src/contracts/notification-group.ts +6 -0
- package/src/contracts/notification-types.ts +6 -0
- package/src/contracts/outgoing-mail.ts +56 -11
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/user-notifications.ts +74 -0
- package/src/server/middleware/request-logger.ts +11 -2
- package/src/services/accounts/avatar.ts +5 -2
- package/src/services/image-metadata.ts +438 -0
- package/src/services/index.ts +1 -0
- package/src/services/logging/redaction.ts +50 -0
- package/src/services/mandates/index.ts +4 -2
- package/src/services/mandates/policy.ts +20 -0
- package/src/services/notifications/browser.ts +48 -15
- package/src/services/notifications/dispatcher.ts +31 -1
- package/src/services/notifications/platform.ts +32 -4
- package/src/services/notifications/preview.ts +23 -0
- package/src/services/notifications/quiet.ts +153 -0
- package/src/services/notifications/user.ts +6 -0
- package/src/services/outbox.ts +24 -9
- package/src/services/outgoing-mail/index.ts +33 -5
- package/src/services/outgoing-mail/messages.ts +60 -19
- package/src/services/outgoing-mail/store.ts +63 -4
- package/src/services/pdf/template-preview.ts +2 -1
- package/src/services/settings/app.ts +3 -0
- package/src/services/settings/store.ts +4 -1
- package/src/services/weather/geo.ts +6 -2
- package/src/shared/ai-platform-prompt.ts +2 -2
- package/src/shared/network-address.ts +1 -0
- package/src/shared/redirect.ts +2 -1
- package/src/shared/template-rendering.ts +120 -7
- package/src/ssr/AppBadge.tsx +30 -0
- package/src/ssr/AppLaunchpadPanel.tsx +47 -26
- package/src/ssr/IslandError.tsx +33 -0
- package/src/ssr/Layout.tsx +3 -0
- package/src/ssr/LayoutHelp.tsx +4 -2
- package/src/ssr/RailApps.island.tsx +9 -2
- package/src/ssr/app-badges.ts +93 -0
- package/src/ssr/rail-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +2 -0
- package/src/styles/effects.css +165 -0
- package/src/styles/file-chooser.css +25 -0
- package/src/styles/utilities-markdown-table.css +5 -1
- package/src/styles/utilities-navigation.css +41 -0
|
@@ -2,7 +2,7 @@ import type { PageParams, Paginated, ServiceError } from "@k2b/stdlib";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { type RequestActor, ServiceAccountSchema, UserSchema } from "./shared";
|
|
4
4
|
|
|
5
|
-
export const PlatformPermissionSchema = z.enum(["mail:send"]);
|
|
5
|
+
export const PlatformPermissionSchema = z.enum(["mail:send", "mail:read"]);
|
|
6
6
|
export type PlatformPermission = z.infer<typeof PlatformPermissionSchema>;
|
|
7
7
|
export const MailRetentionSchema = z.object({ contentDays: z.int().min(1).max(36500), recordDays: z.int().min(1).max(36500) }).strict();
|
|
8
8
|
export type MailRetention = z.infer<typeof MailRetentionSchema>;
|
|
@@ -74,6 +74,7 @@ export type MailProfile = z.infer<typeof MailProfileSchema>;
|
|
|
74
74
|
export const MailErrorCodeSchema = z.enum([
|
|
75
75
|
"bad_input",
|
|
76
76
|
"profile_not_allowed",
|
|
77
|
+
"app_not_allowed",
|
|
77
78
|
"profile_required",
|
|
78
79
|
"quota_exceeded",
|
|
79
80
|
"backlog_full",
|
|
@@ -110,14 +111,31 @@ export const AdminMailProfileSchema = MailProfileInputSchema.omit({ smtpPassword
|
|
|
110
111
|
appCount: z.int(),
|
|
111
112
|
});
|
|
112
113
|
export type AdminMailProfile = z.infer<typeof AdminMailProfileSchema>;
|
|
113
|
-
export
|
|
114
|
-
appId: string
|
|
115
|
-
name: string
|
|
116
|
-
registered: boolean
|
|
117
|
-
declared: boolean
|
|
118
|
-
mode: "default"
|
|
119
|
-
profiles: string
|
|
120
|
-
|
|
114
|
+
export const AdminMailAppSchema = z.object({
|
|
115
|
+
appId: z.string(),
|
|
116
|
+
name: z.string(),
|
|
117
|
+
registered: z.boolean(),
|
|
118
|
+
declared: z.boolean(),
|
|
119
|
+
mode: z.enum(["default", "selected"]),
|
|
120
|
+
profiles: z.array(z.string()),
|
|
121
|
+
/** Whether the registered app declares `mail:read`; grants in `logApps` only apply with it. */
|
|
122
|
+
readDeclared: z.boolean(),
|
|
123
|
+
/** Other apps whose send log this app may read, besides its own. */
|
|
124
|
+
logApps: z.array(z.string()),
|
|
125
|
+
});
|
|
126
|
+
export type AdminMailApp = z.infer<typeof AdminMailAppSchema>;
|
|
127
|
+
/** Application IDs as registered, like sender access; Core's own mail is never readable by apps. */
|
|
128
|
+
export const MailLogAppIdSchema = z.string().min(1);
|
|
129
|
+
/** How many other apps' send logs an administrator can let one app read. */
|
|
130
|
+
const MAIL_LOG_GRANT_LIMIT = 100;
|
|
131
|
+
export const MailLogAccessSchema = z
|
|
132
|
+
.object({ apps: z.array(MailLogAppIdSchema).max(MAIL_LOG_GRANT_LIMIT) })
|
|
133
|
+
.strict()
|
|
134
|
+
.refine(
|
|
135
|
+
(value) => new Set(value.apps).size === value.apps.length && !value.apps.includes("core"),
|
|
136
|
+
"Choose distinct apps other than core",
|
|
137
|
+
);
|
|
138
|
+
export type MailLogAccess = z.infer<typeof MailLogAccessSchema>;
|
|
121
139
|
|
|
122
140
|
export const MailStatusSchema = z.enum(["queued", "sending", "sent", "failed", "bounced", "cancelled"]);
|
|
123
141
|
export type MailStatus = z.infer<typeof MailStatusSchema>;
|
|
@@ -218,11 +236,38 @@ export const MailFilterSchema = z
|
|
|
218
236
|
batchId: z.uuid().optional(),
|
|
219
237
|
status: z.array(MailStatusSchema).max(6).optional(),
|
|
220
238
|
since: z.iso.datetime({ offset: true }).optional(),
|
|
239
|
+
/** Exclusive upper bound on `createdAt`. */
|
|
240
|
+
until: z.iso.datetime({ offset: true }).optional(),
|
|
241
|
+
/**
|
|
242
|
+
* Apps whose records to read; omitted means only the calling app. Others need `mail:read` and an
|
|
243
|
+
* administrator grant. Holds everything `mail.readableApps()` returns: the caller and every grant.
|
|
244
|
+
*/
|
|
245
|
+
apps: z
|
|
246
|
+
.array(MailLogAppIdSchema)
|
|
247
|
+
.min(1)
|
|
248
|
+
.max(MAIL_LOG_GRANT_LIMIT + 1)
|
|
249
|
+
.refine((apps) => new Set(apps).size === apps.length, "Choose distinct apps")
|
|
250
|
+
.optional(),
|
|
251
|
+
/**
|
|
252
|
+
* Case-insensitive substring of a recipient address or the subject. Three letters or digits in a row
|
|
253
|
+
* give the trigram index something to look up; without them every record would be a candidate.
|
|
254
|
+
*/
|
|
255
|
+
q: z
|
|
256
|
+
.string()
|
|
257
|
+
.refine((value) => !/[\x00-\x1f\x7f]/.test(value), "Search must not contain control characters")
|
|
258
|
+
.trim()
|
|
259
|
+
.max(200)
|
|
260
|
+
.regex(/[\p{L}\p{N}]{3}/u, "Search needs three letters or digits in a row")
|
|
261
|
+
.optional(),
|
|
262
|
+
/** One recipient address, matched exactly but case-insensitively. */
|
|
263
|
+
recipient: z.string().trim().pipe(mailAddress).optional(),
|
|
221
264
|
})
|
|
222
265
|
.strict();
|
|
223
266
|
export type MailFilter = z.infer<typeof MailFilterSchema>;
|
|
224
267
|
export const MailRecordSchema = z.object({
|
|
225
268
|
id: z.uuid(),
|
|
269
|
+
/** The app that sent the record. */
|
|
270
|
+
appId: z.string(),
|
|
226
271
|
batchId: z.uuid().optional(),
|
|
227
272
|
profile: z.string(),
|
|
228
273
|
ref: ref.optional(),
|
|
@@ -241,7 +286,6 @@ export const MailRecordSchema = z.object({
|
|
|
241
286
|
});
|
|
242
287
|
export type MailRecord = z.infer<typeof MailRecordSchema>;
|
|
243
288
|
export const AdminMailRecordSchema = MailRecordSchema.omit({ text: true }).extend({
|
|
244
|
-
appId: z.string(),
|
|
245
289
|
errorCode: z.string().optional(),
|
|
246
290
|
response: z.string().optional(),
|
|
247
291
|
});
|
|
@@ -252,9 +296,10 @@ export type MailPage<T = MailRecord> = Paginated<T> & { nextCursor?: string };
|
|
|
252
296
|
export const MailPageParamsSchema = z
|
|
253
297
|
.object({ page: z.int().min(1).optional(), perPage: z.int().min(1).max(100).optional(), cursor: z.string().max(1024).optional() })
|
|
254
298
|
.strict();
|
|
255
|
-
export const AdminMailFilterSchema = MailFilterSchema.extend({
|
|
299
|
+
export const AdminMailFilterSchema = MailFilterSchema.omit({ apps: true }).extend({
|
|
256
300
|
app: z.string().min(1).optional(),
|
|
257
301
|
profile: MailProfileKeySchema.optional(),
|
|
302
|
+
/** Administrators search a recipient substring. */
|
|
258
303
|
recipient: z.string().max(320).optional(),
|
|
259
304
|
});
|
|
260
305
|
export type AdminMailFilter = z.infer<typeof AdminMailFilterSchema>;
|
|
@@ -58,6 +58,80 @@ export const UserNotificationHistoryResponseSchema = z.object({
|
|
|
58
58
|
});
|
|
59
59
|
export type UserNotificationHistoryResponse = z.infer<typeof UserNotificationHistoryResponseSchema>;
|
|
60
60
|
|
|
61
|
+
// Contracts reach the browser bundle, so this uses Intl directly instead of the stdlib date helpers.
|
|
62
|
+
const isTimeZone = (value: string): boolean => {
|
|
63
|
+
try {
|
|
64
|
+
new Intl.DateTimeFormat("en", { timeZone: value }).format(0);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Wall-clock time `HH:MM` in the quiet hours' time zone. */
|
|
72
|
+
const QuietTimeOfDaySchema = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use a time from 00:00 to 23:59");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One recurring quiet period. Days are ISO weekdays (1 = Monday … 7 = Sunday). A period that ends at or before
|
|
76
|
+
* its start runs overnight into the next day; equal start and end make 24 hours quiet, so 00:00 to 00:00 is the
|
|
77
|
+
* whole day.
|
|
78
|
+
*/
|
|
79
|
+
export const NotificationQuietPeriodSchema = z.object({
|
|
80
|
+
days: z
|
|
81
|
+
.array(z.number().int().min(1).max(7))
|
|
82
|
+
.min(1)
|
|
83
|
+
.max(7)
|
|
84
|
+
.refine((days) => new Set(days).size === days.length, "Each weekday can appear only once"),
|
|
85
|
+
start: QuietTimeOfDaySchema,
|
|
86
|
+
end: QuietTimeOfDaySchema,
|
|
87
|
+
});
|
|
88
|
+
export type NotificationQuietPeriod = z.infer<typeof NotificationQuietPeriodSchema>;
|
|
89
|
+
|
|
90
|
+
/** One period per weekday is the most a weekly schedule needs, so seven periods bound the schedule. */
|
|
91
|
+
export const NOTIFICATION_QUIET_PERIOD_LIMIT = 7;
|
|
92
|
+
|
|
93
|
+
export const NotificationQuietHoursSchema = z.object({
|
|
94
|
+
timeZone: z.string().trim().min(1).max(64).refine(isTimeZone, "Unknown time zone"),
|
|
95
|
+
periods: z.array(NotificationQuietPeriodSchema).max(NOTIFICATION_QUIET_PERIOD_LIMIT),
|
|
96
|
+
});
|
|
97
|
+
export type NotificationQuietHours = z.infer<typeof NotificationQuietHoursSchema>;
|
|
98
|
+
|
|
99
|
+
export const NotificationQuietReasonSchema = z.enum(["doNotDisturb", "quietHours"]);
|
|
100
|
+
export type NotificationQuietReason = z.infer<typeof NotificationQuietReasonSchema>;
|
|
101
|
+
|
|
102
|
+
/** Whether Cloud holds back browser notifications right now and, when known, until when. */
|
|
103
|
+
export const NotificationQuietStateSchema = z.object({
|
|
104
|
+
active: z.boolean(),
|
|
105
|
+
reason: NotificationQuietReasonSchema.nullable(),
|
|
106
|
+
/** End of the current quiet time; null when inactive or when no end lies within the next week. */
|
|
107
|
+
until: z.string().datetime().nullable(),
|
|
108
|
+
/** Start of the next quiet hours while inactive; null when active or when none starts within the next week. */
|
|
109
|
+
nextStart: z.string().datetime().nullable(),
|
|
110
|
+
});
|
|
111
|
+
export type NotificationQuietState = z.infer<typeof NotificationQuietStateSchema>;
|
|
112
|
+
|
|
113
|
+
export const NotificationQuietSettingsSchema = z.object({
|
|
114
|
+
doNotDisturbUntil: z.string().datetime().nullable(),
|
|
115
|
+
quietHours: NotificationQuietHoursSchema,
|
|
116
|
+
state: NotificationQuietStateSchema,
|
|
117
|
+
});
|
|
118
|
+
export type NotificationQuietSettings = z.infer<typeof NotificationQuietSettingsSchema>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Do not disturb is a pause, so it ends within a year (366 days covers the same date next year). Holding back
|
|
122
|
+
* browser notifications for longer is turning them off, which the notification preferences and the device do.
|
|
123
|
+
*/
|
|
124
|
+
export const NOTIFICATION_DO_NOT_DISTURB_MAX_DAYS = 366;
|
|
125
|
+
|
|
126
|
+
export const UpdateNotificationQuietSettingsSchema = z
|
|
127
|
+
.object({
|
|
128
|
+
/** A future instant at most `NOTIFICATION_DO_NOT_DISTURB_MAX_DAYS` ahead pauses browser notifications until then; null resumes them. */
|
|
129
|
+
doNotDisturbUntil: z.iso.datetime({ offset: true }).nullable().optional(),
|
|
130
|
+
quietHours: NotificationQuietHoursSchema.optional(),
|
|
131
|
+
})
|
|
132
|
+
.refine((value) => value.doNotDisturbUntil !== undefined || value.quietHours !== undefined, "Nothing to update");
|
|
133
|
+
export type UpdateNotificationQuietSettings = z.infer<typeof UpdateNotificationQuietSettingsSchema>;
|
|
134
|
+
|
|
61
135
|
export const isSafeBrowserPushEndpoint = (value: string): boolean => {
|
|
62
136
|
try {
|
|
63
137
|
if (/[\\\u0000-\u001f\u007f]/.test(value)) return false;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createMiddleware } from "hono/factory";
|
|
2
2
|
import { logger } from "../../services/logging";
|
|
3
|
+
import { redactSensitivePath } from "../../services/logging/redaction";
|
|
3
4
|
import type { AuthContext } from "./auth";
|
|
5
|
+
import { matchedRouteTemplate } from "./route-template";
|
|
4
6
|
|
|
5
7
|
const log = logger("http");
|
|
6
8
|
|
|
@@ -15,11 +17,18 @@ const SKIP_PREFIXES = ["/public/", "/_ssr/", "/favicon", "/branding/"];
|
|
|
15
17
|
* - Everything else (2xx, 3xx, 400, 404) → not logged (too noisy)
|
|
16
18
|
*/
|
|
17
19
|
export const requestLogger = createMiddleware<AuthContext>(async (c, next) => {
|
|
18
|
-
const
|
|
19
|
-
if (SKIP_PREFIXES.some((p) =>
|
|
20
|
+
const requestPath = c.req.path;
|
|
21
|
+
if (SKIP_PREFIXES.some((p) => requestPath.startsWith(p))) return next();
|
|
20
22
|
|
|
21
23
|
const start = Date.now();
|
|
22
24
|
await next();
|
|
25
|
+
let path: string | null = null;
|
|
26
|
+
try {
|
|
27
|
+
path = matchedRouteTemplate(c);
|
|
28
|
+
} catch {
|
|
29
|
+
// Best-effort: an unrouted context must never break request logging.
|
|
30
|
+
}
|
|
31
|
+
path ??= redactSensitivePath(requestPath);
|
|
23
32
|
const status = c.res.status;
|
|
24
33
|
const duration = Date.now() - start;
|
|
25
34
|
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { sql } from "bun";
|
|
3
3
|
import { MAX_AVATAR_BYTES, MAX_AVATAR_DATA_URL_LENGTH } from "../../contracts/profile";
|
|
4
4
|
import type { MutationResult } from "../../contracts/shared";
|
|
5
|
+
import { stripImageMetadata } from "../image-metadata";
|
|
5
6
|
|
|
6
7
|
export { MAX_AVATAR_BYTES, MAX_AVATAR_DATA_URL_LENGTH } from "../../contracts/profile";
|
|
7
8
|
|
|
@@ -95,10 +96,12 @@ export const setAvatar = async (params: { id: string; dataUrl: string }): Promis
|
|
|
95
96
|
const parsed = parseAvatarDataUrl(params.dataUrl);
|
|
96
97
|
if (!parsed.ok) return parsed;
|
|
97
98
|
|
|
99
|
+
const bytes = Buffer.from(stripImageMetadata(parsed.data.bytes));
|
|
100
|
+
const dataUrl = `data:${parsed.data.contentType};base64,${bytes.toString("base64")}`;
|
|
98
101
|
const rows = await sql<{ avatar_hash: string }[]>`
|
|
99
102
|
UPDATE auth.users
|
|
100
|
-
SET avatar_data_url = ${
|
|
101
|
-
avatar_hash = ${
|
|
103
|
+
SET avatar_data_url = ${dataUrl},
|
|
104
|
+
avatar_hash = ${hashAvatar(dataUrl)}
|
|
102
105
|
WHERE id = ${params.id}::uuid
|
|
103
106
|
RETURNING avatar_hash
|
|
104
107
|
`;
|