@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.
Files changed (112) hide show
  1. package/package.json +9 -6
  2. package/src/_internal/cli-plugins.ts +18 -0
  3. package/src/_internal/define-app.ts +21 -5
  4. package/src/_internal/registry-validation.ts +30 -1
  5. package/src/_internal/runtime-context.ts +1 -0
  6. package/src/access/PermissionEditor.tsx +11 -4
  7. package/src/ai/assistant-models.ts +25 -4
  8. package/src/ai/audio-tool.ts +3 -10
  9. package/src/ai/capabilities.ts +35 -9
  10. package/src/ai/chat/blocks.tsx +259 -197
  11. package/src/ai/chat/builtin-tools.tsx +3 -1
  12. package/src/ai/chat/composer-adapter.ts +8 -0
  13. package/src/ai/chat/live-turn.browser-harness.tsx +16 -3
  14. package/src/ai/chat/message-actions.tsx +1 -1
  15. package/src/ai/chat/message-utils.ts +28 -6
  16. package/src/ai/chat/messages.ts +20 -24
  17. package/src/ai/chat/presentation.tsx +1 -8
  18. package/src/ai/chat/tool-disclosure.tsx +21 -14
  19. package/src/ai/chat/tool-groups.ts +64 -37
  20. package/src/ai/chat/tool-names.ts +157 -0
  21. package/src/ai/chat/turn-layout.ts +14 -2
  22. package/src/ai/chat/turn-view.tsx +132 -70
  23. package/src/ai/chat-tasks.ts +16 -3
  24. package/src/ai/code-mode-skill.ts +4 -4
  25. package/src/ai/compaction.ts +22 -3
  26. package/src/ai/default-tools.ts +9 -2
  27. package/src/ai/executor.ts +127 -40
  28. package/src/ai/file-media-type.ts +4 -0
  29. package/src/ai/files-store.ts +17 -10
  30. package/src/ai/http.ts +11 -10
  31. package/src/ai/index.ts +1 -0
  32. package/src/ai/memories.ts +2 -0
  33. package/src/ai/memory-learning.ts +59 -5
  34. package/src/ai/memory-workflow-evidence.ts +35 -10
  35. package/src/ai/projects-routes.ts +5 -1
  36. package/src/ai/projects.ts +5 -1
  37. package/src/ai/protocol.ts +43 -5
  38. package/src/ai/provider.ts +16 -1
  39. package/src/ai/quota-provider.ts +4 -1
  40. package/src/ai/routes.ts +20 -10
  41. package/src/ai/settings.ts +54 -17
  42. package/src/ai/skill-seeds.ts +11 -3
  43. package/src/ai/store.ts +76 -33
  44. package/src/ai/structured.ts +2 -1
  45. package/src/ai/system-prompt.ts +5 -1
  46. package/src/ai/transcription.ts +58 -13
  47. package/src/ai/turn-failure.ts +5 -1
  48. package/src/ai/turn-guidance.ts +505 -0
  49. package/src/ai/types.ts +3 -1
  50. package/src/ai/ui.tsx +1 -0
  51. package/src/api/admin-core-settings.ts +16 -1
  52. package/src/api/admin-outgoing-mail.ts +18 -0
  53. package/src/api/me.ts +42 -1
  54. package/src/browser/FileChooser.tsx +71 -295
  55. package/src/browser/FileSaver.tsx +407 -0
  56. package/src/browser/app-badges.ts +16 -0
  57. package/src/browser/choose-files.browser-harness.tsx +22 -6
  58. package/src/browser/choose-files.tsx +23 -36
  59. package/src/browser/file-chooser-messages.ts +64 -0
  60. package/src/browser/file-providers.ts +258 -8
  61. package/src/browser/files.tsx +26 -3
  62. package/src/browser/notifications.ts +37 -1
  63. package/src/browser/provider-folder.tsx +301 -0
  64. package/src/browser/provider-list.ts +39 -0
  65. package/src/browser/save-files.browser-harness.tsx +35 -0
  66. package/src/browser/save-files.tsx +107 -0
  67. package/src/cli/admin/outgoing-mail.ts +30 -2
  68. package/src/contracts/app.ts +10 -0
  69. package/src/contracts/commands.ts +3 -1
  70. package/src/contracts/notification-group.ts +6 -0
  71. package/src/contracts/notification-types.ts +6 -0
  72. package/src/contracts/outgoing-mail.ts +56 -11
  73. package/src/contracts/registry.ts +1 -0
  74. package/src/contracts/user-notifications.ts +74 -0
  75. package/src/server/middleware/request-logger.ts +11 -2
  76. package/src/services/accounts/avatar.ts +5 -2
  77. package/src/services/image-metadata.ts +438 -0
  78. package/src/services/index.ts +1 -0
  79. package/src/services/logging/redaction.ts +50 -0
  80. package/src/services/mandates/index.ts +4 -2
  81. package/src/services/mandates/policy.ts +20 -0
  82. package/src/services/notifications/browser.ts +48 -15
  83. package/src/services/notifications/dispatcher.ts +31 -1
  84. package/src/services/notifications/platform.ts +32 -4
  85. package/src/services/notifications/preview.ts +23 -0
  86. package/src/services/notifications/quiet.ts +153 -0
  87. package/src/services/notifications/user.ts +6 -0
  88. package/src/services/outbox.ts +24 -9
  89. package/src/services/outgoing-mail/index.ts +33 -5
  90. package/src/services/outgoing-mail/messages.ts +60 -19
  91. package/src/services/outgoing-mail/store.ts +63 -4
  92. package/src/services/pdf/template-preview.ts +2 -1
  93. package/src/services/settings/app.ts +3 -0
  94. package/src/services/settings/store.ts +4 -1
  95. package/src/services/weather/geo.ts +6 -2
  96. package/src/shared/ai-platform-prompt.ts +2 -2
  97. package/src/shared/network-address.ts +1 -0
  98. package/src/shared/redirect.ts +2 -1
  99. package/src/shared/template-rendering.ts +120 -7
  100. package/src/ssr/AppBadge.tsx +30 -0
  101. package/src/ssr/AppLaunchpadPanel.tsx +47 -26
  102. package/src/ssr/IslandError.tsx +33 -0
  103. package/src/ssr/Layout.tsx +3 -0
  104. package/src/ssr/LayoutHelp.tsx +4 -2
  105. package/src/ssr/RailApps.island.tsx +9 -2
  106. package/src/ssr/app-badges.ts +93 -0
  107. package/src/ssr/rail-messages.ts +4 -0
  108. package/src/ssr/rail-navigation.ts +2 -0
  109. package/src/styles/effects.css +165 -0
  110. package/src/styles/file-chooser.css +25 -0
  111. package/src/styles/utilities-markdown-table.css +5 -1
  112. 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 type AdminMailApp = {
114
- appId: string;
115
- name: string;
116
- registered: boolean;
117
- declared: boolean;
118
- mode: "default" | "selected";
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>;
@@ -16,6 +16,7 @@ export type AppRegistryNav = {
16
16
  section: "primary" | "more" | "hidden";
17
17
  requiresAuth?: boolean;
18
18
  requiresRoles?: Role[];
19
+ badge?: string;
19
20
  adminHref?: string;
20
21
  };
21
22
 
@@ -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 path = c.req.path;
19
- if (SKIP_PREFIXES.some((p) => path.startsWith(p))) return next();
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 = ${parsed.data.dataUrl},
101
- avatar_hash = ${parsed.data.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
  `;