okengine 0.6.0 → 0.7.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.
Files changed (162) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +98 -71
  3. package/package.json +4 -4
  4. package/site/content/docs/console/clock.mdx +25 -5
  5. package/site/content/docs/console/signals.mdx +13 -4
  6. package/site/content/docs/elements/ai.mdx +29 -2
  7. package/site/content/docs/elements/channel.mdx +90 -20
  8. package/site/content/docs/elements/clock.mdx +56 -24
  9. package/site/content/docs/elements/flow.mdx +2 -0
  10. package/site/content/docs/elements/gate.mdx +8 -1
  11. package/site/content/docs/elements/signal.mdx +173 -29
  12. package/site/content/docs/elements/store.mdx +464 -73
  13. package/site/content/docs/elements/vault.mdx +15 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +13 -6
  15. package/site/content/docs/get-started/installation.mdx +49 -13
  16. package/site/content/docs/get-started/introduction.mdx +1 -1
  17. package/site/content/docs/reference/cli.md +30 -2
  18. package/site/content/docs/reference/configuration.mdx +18 -17
  19. package/site/content/docs/reference/environment-variables.mdx +44 -18
  20. package/site/content/docs/reference/errors.mdx +12 -11
  21. package/site/content/docs/reference/fx.mdx +15 -13
  22. package/site/content/docs/reference/security.md +3 -1
  23. package/src/cli/ai-setup/ai-setup.test.ts +144 -0
  24. package/src/cli/ai-setup/apply.ts +214 -0
  25. package/src/cli/ai-setup/catalog.ts +263 -0
  26. package/src/cli/ai-setup/detect-ollama.ts +166 -0
  27. package/src/cli/ai-setup/index.ts +228 -0
  28. package/src/cli/ai-setup/prompts.ts +649 -0
  29. package/src/cli/ai-setup/recommend.test.ts +100 -0
  30. package/src/cli/ai-setup/recommend.ts +203 -0
  31. package/src/cli/ai.ts +38 -0
  32. package/src/cli/db-auto-push.test.ts +6 -1
  33. package/src/cli/db-auto-push.ts +8 -3
  34. package/src/cli/dev-db-push.test.ts +59 -0
  35. package/src/cli/dev.test.ts +40 -0
  36. package/src/cli/dev.ts +55 -1
  37. package/src/cli/docker-clean.test.ts +179 -0
  38. package/src/cli/docker-clean.ts +406 -0
  39. package/src/cli/docker.ts +16 -4
  40. package/src/cli/ensure-drizzle-config.ts +1 -1
  41. package/src/cli/hero-meta.test.ts +6 -5
  42. package/src/cli/hero-meta.ts +7 -1
  43. package/src/cli/index.ts +5 -0
  44. package/src/cli/load-config.ts +4 -4
  45. package/src/cli/openbao-bootstrap.test.ts +1 -0
  46. package/src/cli/openbao-bootstrap.ts +9 -2
  47. package/src/cli/openbao-restart.integration.test.ts +106 -97
  48. package/src/cli/registry.ts +79 -1
  49. package/src/client/live-gap.test.ts +35 -0
  50. package/src/client/transport.test.ts +21 -0
  51. package/src/client/transport.ts +27 -5
  52. package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
  53. package/src/config/define-config.test.ts +6 -6
  54. package/src/config/index.ts +1 -1
  55. package/src/console/server/console.test.ts +45 -0
  56. package/src/console/server/flows.ts +39 -9
  57. package/src/console/server/i18n.ts +41 -0
  58. package/src/console/server/vault.ts +12 -38
  59. package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
  60. package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
  61. package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
  62. package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
  63. package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
  64. package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
  65. package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
  66. package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
  67. package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
  68. package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
  69. package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
  70. package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
  71. package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
  72. package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
  73. package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
  74. package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
  75. package/src/console/ui/dist/index.html +1 -1
  76. package/src/console/ui/shell/components/ui.tsx +5 -1
  77. package/src/console/ui/shell/setup/Wizard.tsx +26 -5
  78. package/src/docker/cleanup.test.ts +193 -0
  79. package/src/docker/cleanup.ts +355 -0
  80. package/src/docker/docker.test.ts +4 -0
  81. package/src/docker/dockerfile.integration.test.ts +126 -119
  82. package/src/docker/index.ts +19 -0
  83. package/src/docker/recipes/ollama.ts +10 -5
  84. package/src/docker/stack.integration.test.ts +118 -102
  85. package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
  86. package/src/drivers/ai-ollama.integration.test.ts +3 -19
  87. package/src/drivers/ai-ollama.ts +45 -2
  88. package/src/drivers/ai-providers.test.ts +31 -0
  89. package/src/drivers/channel-fcm.ts +49 -53
  90. package/src/drivers/channel-msegat.ts +61 -0
  91. package/src/drivers/channel-sently-map.ts +57 -0
  92. package/src/drivers/channel-sently.test.ts +99 -0
  93. package/src/drivers/channel-sndr.ts +28 -0
  94. package/src/drivers/channel-taqnyat.ts +57 -0
  95. package/src/drivers/channel-types.ts +79 -2
  96. package/src/drivers/channel-unifonic.ts +26 -43
  97. package/src/drivers/channel-wa-cloud.ts +33 -47
  98. package/src/drivers/channel-webpush.ts +39 -239
  99. package/src/drivers/index.ts +7 -0
  100. package/src/drivers/signal-engine.ts +98 -25
  101. package/src/drivers/signal-nats.ts +4 -4
  102. package/src/drivers/signal-postgres.ts +158 -64
  103. package/src/drivers/signal-redis.ts +4 -4
  104. package/src/drivers/signal-types.ts +61 -2
  105. package/src/drivers/vault-driver-removal.test.ts +5 -1
  106. package/src/elements/channel/costs.test.ts +2 -2
  107. package/src/elements/channel/costs.ts +14 -2
  108. package/src/elements/channel/mime.ts +11 -0
  109. package/src/elements/channel/runtime.ts +94 -0
  110. package/src/elements/channel/sndr-webhooks.test.ts +26 -0
  111. package/src/elements/channel.ts +10 -1
  112. package/src/elements/clock/chaos-child.ts +150 -0
  113. package/src/elements/clock/chaos.test.ts +204 -0
  114. package/src/elements/clock/reconcile.ts +110 -0
  115. package/src/elements/clock/runtime.ts +11 -0
  116. package/src/elements/clock.test.ts +164 -2
  117. package/src/elements/clock.ts +1 -0
  118. package/src/elements/gate/runtime.ts +6 -0
  119. package/src/elements/index.ts +9 -0
  120. package/src/elements/signal/chaos-child.ts +41 -4
  121. package/src/elements/signal/declare.ts +4 -1
  122. package/src/elements/signal/delivery-modes.test.ts +179 -0
  123. package/src/elements/signal/key-ordering.test.ts +306 -0
  124. package/src/elements/signal/lease-reclaim.test.ts +123 -0
  125. package/src/elements/signal/optional-emit.test.ts +76 -0
  126. package/src/elements/signal/order-lifecycle.test.ts +130 -0
  127. package/src/elements/signal/orphan-messages.test.ts +152 -0
  128. package/src/elements/signal/runtime.ts +11 -4
  129. package/src/elements/signal/schema-emit.test.ts +107 -0
  130. package/src/elements/signal.test.ts +89 -5
  131. package/src/elements/store/files-fx.ts +104 -0
  132. package/src/elements/store/files-image.test.ts +251 -0
  133. package/src/elements/store/files-image.ts +494 -0
  134. package/src/elements/store/runtime.ts +60 -0
  135. package/src/elements/store.ts +24 -0
  136. package/src/elements/vault/boot-chain.ts +150 -0
  137. package/src/elements/vault/runtime.ts +8 -0
  138. package/src/kernel/app.ts +3 -1
  139. package/src/kernel/boot-bind/channel.test.ts +68 -3
  140. package/src/kernel/boot-bind/channel.ts +93 -2
  141. package/src/kernel/boot-bind/clock.ts +52 -7
  142. package/src/kernel/boot-bind/gate.ts +71 -3
  143. package/src/kernel/boot-bind/honor-config.test.ts +247 -0
  144. package/src/kernel/boot-bind/signal.ts +64 -7
  145. package/src/kernel/boot-bind/vault.ts +27 -8
  146. package/src/kernel/boot.ts +10 -2
  147. package/src/kernel/errors.ts +8 -2
  148. package/src/kernel/fx.test.ts +13 -0
  149. package/src/kernel/fx.ts +27 -4
  150. package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
  151. package/src/release/exports.test.ts +26 -0
  152. package/src/release/exports.ts +64 -5
  153. package/src/release/index.ts +5 -0
  154. package/src/release/measure.exports.test.ts +13 -1
  155. package/src/release/measure.ts +76 -13
  156. package/src/release/official-plugins.ts +46 -0
  157. package/src/release/readme.test.ts +30 -2
  158. package/src/runtime/dev-request-log.test.ts +20 -1
  159. package/src/runtime/dev-request-log.ts +38 -0
  160. package/src/term.test.ts +15 -0
  161. package/src/term.ts +8 -3
  162. package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Files facet image pipeline — Bun.Image over bucket get/put.
3
+ *
4
+ * Drivers stay blob CRUD; decode / transform / encode live here so every
5
+ * store access still goes through the files handle (and fx gating).
6
+ */
7
+
8
+ /**
9
+ * Default decompression-bomb ceiling — `4096 × 4096` (16 MP).
10
+ *
11
+ * Rationale: covers typical phone / web uploads (4K ≈ 8.3 MP) while refusing
12
+ * header-claimed canvases that would allocate multi-hundred-MB pixel buffers.
13
+ * Raise with `maxPixels`, or pass `false` to disable (explicit opt-out).
14
+ */
15
+ export const DEFAULT_FILES_IMAGE_MAX_PIXELS = 4096 * 4096;
16
+
17
+ /** Image dimensions + sniffed format (header only — no full decode). */
18
+ export interface FilesImageMeta {
19
+ readonly width: number;
20
+ readonly height: number;
21
+ readonly format: string;
22
+ }
23
+
24
+ /** Bun.Image constructor guards. */
25
+ export interface FilesImageOptions {
26
+ /**
27
+ * Reject when width×height exceeds this (checked after header, before the
28
+ * pixel buffer). Default {@link DEFAULT_FILES_IMAGE_MAX_PIXELS}. Pass
29
+ * `false` to disable the guard.
30
+ */
31
+ readonly maxPixels?: number | false;
32
+ /** Apply JPEG EXIF Orientation before other ops. Default true. */
33
+ readonly autoOrient?: boolean;
34
+ }
35
+
36
+ /** Resize options (Bun.Image / Sharp-shaped). */
37
+ export interface FilesImageResizeOptions {
38
+ readonly filter?:
39
+ | "nearest"
40
+ | "box"
41
+ | "bilinear"
42
+ | "linear"
43
+ | "cubic"
44
+ | "mitchell"
45
+ | "lanczos2"
46
+ | "lanczos3"
47
+ | "mks2013"
48
+ | "mks2021";
49
+ readonly fit?: "fill" | "inside";
50
+ readonly withoutEnlargement?: boolean;
51
+ }
52
+
53
+ /** Brightness / saturation multipliers. */
54
+ export interface FilesImageModulateOptions {
55
+ readonly brightness?: number;
56
+ readonly saturation?: number;
57
+ }
58
+
59
+ /** JPEG encode options. */
60
+ export interface FilesImageJpegOptions {
61
+ readonly quality?: number;
62
+ readonly progressive?: boolean;
63
+ }
64
+
65
+ /** PNG encode options. */
66
+ export interface FilesImagePngOptions {
67
+ readonly compressionLevel?: number;
68
+ readonly palette?: boolean;
69
+ readonly colors?: number;
70
+ readonly dither?: boolean;
71
+ }
72
+
73
+ /** WebP encode options. */
74
+ export interface FilesImageWebpOptions {
75
+ readonly quality?: number;
76
+ readonly lossless?: boolean;
77
+ }
78
+
79
+ /** HEIC / AVIF quality (platform codecs). */
80
+ export interface FilesImageQualityOptions {
81
+ readonly quality?: number;
82
+ }
83
+
84
+ /** Portable + platform encode targets. */
85
+ export type FilesImageFormat = "jpeg" | "png" | "webp" | "heic" | "avif";
86
+
87
+ /**
88
+ * One named derivative for {@link PutImageOptions.variants}.
89
+ *
90
+ * `resize` is a Sharp-style tuple: `[width]`, `[width, height]`, or
91
+ * `[width, height, options]`. Set exactly one of `jpeg` / `png` / `webp` /
92
+ * `heic` / `avif` to choose the encode target (default: source format, or
93
+ * `webp` when the source is decode-only).
94
+ */
95
+ export interface ImageVariantSpec {
96
+ readonly resize?:
97
+ | readonly [width: number]
98
+ | readonly [width: number, height: number]
99
+ | readonly [width: number, height: number, options: FilesImageResizeOptions];
100
+ readonly rotate?: number;
101
+ readonly flip?: boolean;
102
+ readonly flop?: boolean;
103
+ readonly modulate?: FilesImageModulateOptions;
104
+ readonly jpeg?: FilesImageJpegOptions;
105
+ readonly png?: FilesImagePngOptions;
106
+ readonly webp?: FilesImageWebpOptions;
107
+ readonly heic?: FilesImageQualityOptions;
108
+ readonly avif?: FilesImageQualityOptions;
109
+ }
110
+
111
+ /** Options for {@link putImageToBucket}. */
112
+ export interface PutImageOptions {
113
+ /** Named derivatives written beside the original. */
114
+ readonly variants?: Readonly<Record<string, ImageVariantSpec>>;
115
+ /** Include a ThumbHash LQIP data URL on the result. */
116
+ readonly placeholder?: boolean;
117
+ /**
118
+ * Pixel ceiling — default {@link DEFAULT_FILES_IMAGE_MAX_PIXELS}.
119
+ * Pass `false` to disable.
120
+ */
121
+ readonly maxPixels?: number | false;
122
+ readonly autoOrient?: boolean;
123
+ }
124
+
125
+ /** Result of {@link putImageToBucket}. */
126
+ export interface PutImageResult {
127
+ readonly key: string;
128
+ readonly meta: FilesImageMeta;
129
+ readonly variants: Readonly<Record<string, string>>;
130
+ readonly placeholder?: string;
131
+ }
132
+
133
+ /** Bucket accessors used by the pipeline (gated or raw). */
134
+ export interface FilesImageBucketAccess {
135
+ get(key: string): Promise<Uint8Array | null>;
136
+ put(key: string, data: Uint8Array | string): Promise<void>;
137
+ }
138
+
139
+ /** Chainable image pipeline on a files store. */
140
+ export interface FilesImagePipeline {
141
+ resize(width: number, height?: number, options?: FilesImageResizeOptions): FilesImagePipeline;
142
+ rotate(degrees: number): FilesImagePipeline;
143
+ flip(): FilesImagePipeline;
144
+ flop(): FilesImagePipeline;
145
+ modulate(options: FilesImageModulateOptions): FilesImagePipeline;
146
+ jpeg(options?: FilesImageJpegOptions): FilesImagePipeline;
147
+ png(options?: FilesImagePngOptions): FilesImagePipeline;
148
+ webp(options?: FilesImageWebpOptions): FilesImagePipeline;
149
+ heic(options?: FilesImageQualityOptions): FilesImagePipeline;
150
+ avif(options?: FilesImageQualityOptions): FilesImagePipeline;
151
+ metadata(): Promise<FilesImageMeta>;
152
+ bytes(): Promise<Uint8Array>;
153
+ blob(): Promise<Blob>;
154
+ placeholder(): Promise<string>;
155
+ put(outKey: string): Promise<void>;
156
+ }
157
+
158
+ type Op =
159
+ | {
160
+ readonly kind: "resize";
161
+ readonly width: number;
162
+ readonly height?: number;
163
+ readonly options?: FilesImageResizeOptions;
164
+ }
165
+ | { readonly kind: "rotate"; readonly degrees: number }
166
+ | { readonly kind: "flip" }
167
+ | { readonly kind: "flop" }
168
+ | { readonly kind: "modulate"; readonly options: FilesImageModulateOptions };
169
+
170
+ type FormatOp =
171
+ | { readonly format: "jpeg"; readonly options?: FilesImageJpegOptions }
172
+ | { readonly format: "png"; readonly options?: FilesImagePngOptions }
173
+ | { readonly format: "webp"; readonly options?: FilesImageWebpOptions }
174
+ | { readonly format: "heic"; readonly options?: FilesImageQualityOptions }
175
+ | { readonly format: "avif"; readonly options?: FilesImageQualityOptions };
176
+
177
+ function isImageError(err: unknown, code: string): boolean {
178
+ return (
179
+ typeof err === "object" &&
180
+ err !== null &&
181
+ "code" in err &&
182
+ (err as { code: unknown }).code === code
183
+ );
184
+ }
185
+
186
+ function toBytes(data: Uint8Array | string): Uint8Array {
187
+ return typeof data === "string" ? new TextEncoder().encode(data) : data;
188
+ }
189
+
190
+ /**
191
+ * Resolve Bun.Image constructor options — `maxPixels` always set
192
+ * (secure-by-default; never omit the guard).
193
+ *
194
+ * @param opts - Caller options
195
+ */
196
+ export function resolveFilesImageCtorOptions(
197
+ opts?: FilesImageOptions,
198
+ ): Bun.Image.ConstructorOptions {
199
+ const maxPixels =
200
+ opts?.maxPixels === false
201
+ ? Number.POSITIVE_INFINITY
202
+ : (opts?.maxPixels ?? DEFAULT_FILES_IMAGE_MAX_PIXELS);
203
+ const out: Bun.Image.ConstructorOptions = { maxPixels };
204
+ if (opts?.autoOrient !== undefined) out.autoOrient = opts.autoOrient;
205
+ return out;
206
+ }
207
+
208
+ function applyOps(img: Bun.Image, ops: readonly Op[]): Bun.Image {
209
+ let cur = img;
210
+ for (const op of ops) {
211
+ if (op.kind === "resize") cur = cur.resize(op.width, op.height, op.options);
212
+ else if (op.kind === "rotate") cur = cur.rotate(op.degrees);
213
+ else if (op.kind === "flip") cur = cur.flip();
214
+ else if (op.kind === "flop") cur = cur.flop();
215
+ else cur = cur.modulate(op.options);
216
+ }
217
+ return cur;
218
+ }
219
+
220
+ function applyFormat(img: Bun.Image, formatOp: FormatOp | undefined): Bun.Image {
221
+ if (!formatOp) return img;
222
+ if (formatOp.format === "jpeg") return img.jpeg(formatOp.options);
223
+ if (formatOp.format === "png") return img.png(formatOp.options);
224
+ if (formatOp.format === "webp") return img.webp(formatOp.options);
225
+ if (formatOp.format === "heic") return img.heic(formatOp.options);
226
+ return img.avif(formatOp.options);
227
+ }
228
+
229
+ async function encodeBytes(
230
+ input: Uint8Array,
231
+ options: FilesImageOptions | undefined,
232
+ ops: readonly Op[],
233
+ formatOp: FormatOp | undefined,
234
+ ): Promise<Uint8Array> {
235
+ const buildBase = (): Bun.Image =>
236
+ applyOps(new Bun.Image(input, resolveFilesImageCtorOptions(options)), ops);
237
+ try {
238
+ return await applyFormat(buildBase(), formatOp).bytes();
239
+ } catch (err) {
240
+ if (
241
+ formatOp &&
242
+ (formatOp.format === "avif" || formatOp.format === "heic") &&
243
+ isImageError(err, "ERR_IMAGE_FORMAT_UNSUPPORTED")
244
+ ) {
245
+ const quality =
246
+ formatOp.options && "quality" in formatOp.options ? formatOp.options.quality : 80;
247
+ return await buildBase()
248
+ .webp({ quality: quality ?? 80 })
249
+ .bytes();
250
+ }
251
+ throw err;
252
+ }
253
+ }
254
+
255
+ function formatFromSpec(spec: ImageVariantSpec): FormatOp | undefined {
256
+ const set = [
257
+ spec.jpeg !== undefined ? ("jpeg" as const) : undefined,
258
+ spec.png !== undefined ? ("png" as const) : undefined,
259
+ spec.webp !== undefined ? ("webp" as const) : undefined,
260
+ spec.heic !== undefined ? ("heic" as const) : undefined,
261
+ spec.avif !== undefined ? ("avif" as const) : undefined,
262
+ ].filter((f): f is FilesImageFormat => f !== undefined);
263
+ if (set.length > 1) {
264
+ throw new Error(
265
+ `files image variant: set only one of jpeg/png/webp/heic/avif (got ${set.join(", ")})`,
266
+ );
267
+ }
268
+ const format = set[0];
269
+ if (!format) return undefined;
270
+ if (format === "jpeg") return { format, options: spec.jpeg };
271
+ if (format === "png") return { format, options: spec.png };
272
+ if (format === "webp") return { format, options: spec.webp };
273
+ if (format === "heic") return { format, options: spec.heic };
274
+ return { format, options: spec.avif };
275
+ }
276
+
277
+ function opsFromSpec(spec: ImageVariantSpec): Op[] {
278
+ const ops: Op[] = [];
279
+ if (spec.resize) {
280
+ const width = spec.resize[0];
281
+ const height = typeof spec.resize[1] === "number" ? spec.resize[1] : undefined;
282
+ const options = spec.resize.length === 3 ? spec.resize[2] : undefined;
283
+ ops.push({ kind: "resize", width, height, options });
284
+ }
285
+ if (spec.rotate !== undefined) ops.push({ kind: "rotate", degrees: spec.rotate });
286
+ if (spec.flip) ops.push({ kind: "flip" });
287
+ if (spec.flop) ops.push({ kind: "flop" });
288
+ if (spec.modulate) ops.push({ kind: "modulate", options: spec.modulate });
289
+ return ops;
290
+ }
291
+
292
+ /**
293
+ * Object key for a named variant: `photos/x.jpg` + `thumb` + `webp`
294
+ * → `photos/x.thumb.webp`.
295
+ *
296
+ * @param sourceKey - Original object key
297
+ * @param variantName - Variant id (e.g. `thumb`)
298
+ * @param format - Encode format id
299
+ */
300
+ export function variantObjectKey(
301
+ sourceKey: string,
302
+ variantName: string,
303
+ format: FilesImageFormat | string,
304
+ ): string {
305
+ const slash = sourceKey.lastIndexOf("/");
306
+ const dir = slash >= 0 ? sourceKey.slice(0, slash + 1) : "";
307
+ const base = slash >= 0 ? sourceKey.slice(slash + 1) : sourceKey;
308
+ const dot = base.lastIndexOf(".");
309
+ const stem = dot > 0 ? base.slice(0, dot) : base;
310
+ const ext = format === "jpeg" ? "jpg" : format;
311
+ return `${dir}${stem}.${variantName}.${ext}`;
312
+ }
313
+
314
+ function resolveEncodeFormat(
315
+ formatOp: FormatOp | undefined,
316
+ sourceFormat: string,
317
+ ): FilesImageFormat {
318
+ if (formatOp) return formatOp.format;
319
+ if (
320
+ sourceFormat === "jpeg" ||
321
+ sourceFormat === "png" ||
322
+ sourceFormat === "webp" ||
323
+ sourceFormat === "heic" ||
324
+ sourceFormat === "avif"
325
+ ) {
326
+ return sourceFormat;
327
+ }
328
+ // Decode-only sources (gif/bmp/tiff) — portable default.
329
+ return "webp";
330
+ }
331
+
332
+ /**
333
+ * Build a chainable image pipeline over bucket accessors.
334
+ *
335
+ * @param access - get/put (may be fx-gated)
336
+ * @param source - Object key or raw bytes
337
+ * @param options - Bun.Image constructor options
338
+ */
339
+ export function createFilesImagePipeline(
340
+ access: FilesImageBucketAccess,
341
+ source: string | Uint8Array,
342
+ options?: FilesImageOptions,
343
+ ): FilesImagePipeline {
344
+ const ops: Op[] = [];
345
+ let formatOp: FormatOp | undefined;
346
+
347
+ async function resolveSource(): Promise<Uint8Array> {
348
+ if (typeof source !== "string") return source;
349
+ const data = await access.get(source);
350
+ if (data == null) {
351
+ throw new Error(`files image: object not found: ${source}`);
352
+ }
353
+ return data;
354
+ }
355
+
356
+ const pipeline: FilesImagePipeline = {
357
+ resize(width, height, resizeOpts) {
358
+ ops.push({ kind: "resize", width, height, options: resizeOpts });
359
+ return pipeline;
360
+ },
361
+ rotate(degrees) {
362
+ ops.push({ kind: "rotate", degrees });
363
+ return pipeline;
364
+ },
365
+ flip() {
366
+ ops.push({ kind: "flip" });
367
+ return pipeline;
368
+ },
369
+ flop() {
370
+ ops.push({ kind: "flop" });
371
+ return pipeline;
372
+ },
373
+ modulate(modOpts) {
374
+ ops.push({ kind: "modulate", options: modOpts });
375
+ return pipeline;
376
+ },
377
+ jpeg(jpegOpts) {
378
+ formatOp = { format: "jpeg", options: jpegOpts };
379
+ return pipeline;
380
+ },
381
+ png(pngOpts) {
382
+ formatOp = { format: "png", options: pngOpts };
383
+ return pipeline;
384
+ },
385
+ webp(webpOpts) {
386
+ formatOp = { format: "webp", options: webpOpts };
387
+ return pipeline;
388
+ },
389
+ heic(heicOpts) {
390
+ formatOp = { format: "heic", options: heicOpts };
391
+ return pipeline;
392
+ },
393
+ avif(avifOpts) {
394
+ formatOp = { format: "avif", options: avifOpts };
395
+ return pipeline;
396
+ },
397
+ async metadata() {
398
+ const input = await resolveSource();
399
+ const meta = await new Bun.Image(input, resolveFilesImageCtorOptions(options)).metadata();
400
+ return { width: meta.width, height: meta.height, format: meta.format };
401
+ },
402
+ async bytes() {
403
+ return encodeBytes(await resolveSource(), options, ops, formatOp);
404
+ },
405
+ async blob() {
406
+ const input = await resolveSource();
407
+ const buildBase = (): Bun.Image =>
408
+ applyOps(new Bun.Image(input, resolveFilesImageCtorOptions(options)), ops);
409
+ try {
410
+ return await applyFormat(buildBase(), formatOp).blob();
411
+ } catch (err) {
412
+ if (
413
+ formatOp &&
414
+ (formatOp.format === "avif" || formatOp.format === "heic") &&
415
+ isImageError(err, "ERR_IMAGE_FORMAT_UNSUPPORTED")
416
+ ) {
417
+ const quality =
418
+ formatOp.options && "quality" in formatOp.options ? formatOp.options.quality : 80;
419
+ return await buildBase()
420
+ .webp({ quality: quality ?? 80 })
421
+ .blob();
422
+ }
423
+ throw err;
424
+ }
425
+ },
426
+ async placeholder() {
427
+ const input = await resolveSource();
428
+ return new Bun.Image(input, resolveFilesImageCtorOptions(options)).placeholder();
429
+ },
430
+ async put(outKey) {
431
+ const encoded = await encodeBytes(await resolveSource(), options, ops, formatOp);
432
+ await access.put(outKey, encoded);
433
+ },
434
+ };
435
+
436
+ return pipeline;
437
+ }
438
+
439
+ /**
440
+ * Write an original image plus optional named variants / LQIP.
441
+ *
442
+ * @param access - get/put (may be fx-gated)
443
+ * @param key - Object key for the original
444
+ * @param data - Image bytes (or UTF-8 string, same as `put`)
445
+ * @param opts - Variants / placeholder / Bun.Image guards
446
+ */
447
+ export async function putImageToBucket(
448
+ access: FilesImageBucketAccess,
449
+ key: string,
450
+ data: Uint8Array | string,
451
+ opts?: PutImageOptions,
452
+ ): Promise<PutImageResult> {
453
+ const input = toBytes(data);
454
+ const imageOpts: FilesImageOptions = {
455
+ maxPixels: opts?.maxPixels,
456
+ autoOrient: opts?.autoOrient,
457
+ };
458
+ const metaRaw = await new Bun.Image(input, resolveFilesImageCtorOptions(imageOpts)).metadata();
459
+ const meta: FilesImageMeta = {
460
+ width: metaRaw.width,
461
+ height: metaRaw.height,
462
+ format: metaRaw.format,
463
+ };
464
+
465
+ await access.put(key, input);
466
+
467
+ const variants: Record<string, string> = {};
468
+ for (const [name, spec] of Object.entries(opts?.variants ?? {})) {
469
+ const formatOp = formatFromSpec(spec);
470
+ const encodeFormat = resolveEncodeFormat(formatOp, meta.format);
471
+ const effectiveFormat: FormatOp =
472
+ formatOp ??
473
+ (encodeFormat === "jpeg"
474
+ ? { format: "jpeg" }
475
+ : encodeFormat === "png"
476
+ ? { format: "png" }
477
+ : encodeFormat === "heic"
478
+ ? { format: "heic" }
479
+ : encodeFormat === "avif"
480
+ ? { format: "avif" }
481
+ : { format: "webp" });
482
+ const outKey = variantObjectKey(key, name, encodeFormat);
483
+ const encoded = await encodeBytes(input, imageOpts, opsFromSpec(spec), effectiveFormat);
484
+ await access.put(outKey, encoded);
485
+ variants[name] = outKey;
486
+ }
487
+
488
+ let placeholder: string | undefined;
489
+ if (opts?.placeholder) {
490
+ placeholder = await new Bun.Image(input, resolveFilesImageCtorOptions(imageOpts)).placeholder();
491
+ }
492
+
493
+ return { key, meta, variants, placeholder };
494
+ }
@@ -42,6 +42,18 @@ import type {
42
42
  SqlStoreDecl,
43
43
  StoreDecl,
44
44
  } from "./declare.ts";
45
+ import { createGatedFilesStoreHandle, type GatedFilesFxBridge } from "./files-fx.ts";
46
+ import {
47
+ createFilesImagePipeline,
48
+ putImageToBucket,
49
+ type FilesImageOptions,
50
+ type FilesImagePipeline,
51
+ type PutImageOptions,
52
+ type PutImageResult,
53
+ } from "./files-image.ts";
54
+
55
+ export type { GatedFilesFxBridge } from "./files-fx.ts";
56
+ export { createGatedFilesStoreHandle } from "./files-fx.ts";
45
57
 
46
58
  /** Driver bundle wired into a runtime. */
47
59
  export interface StoreDriverBundle {
@@ -126,6 +138,21 @@ export interface FilesStoreFxHandle {
126
138
  get(key: string): Promise<Uint8Array | null>;
127
139
  delete(key: string): Promise<boolean>;
128
140
  list(prefix?: string): Promise<string[]>;
141
+ /**
142
+ * Chainable Bun.Image pipeline over a stored key or raw bytes.
143
+ *
144
+ * @param source - Object key or image bytes
145
+ * @param options - Decode guards (`maxPixels`, `autoOrient`)
146
+ */
147
+ image(source: string | Uint8Array, options?: FilesImageOptions): FilesImagePipeline;
148
+ /**
149
+ * Put an original image and optional named variants / LQIP.
150
+ *
151
+ * @param key - Object key for the original
152
+ * @param data - Image bytes (or UTF-8 string, same as {@link put})
153
+ * @param opts - Variants / placeholder / decode guards
154
+ */
155
+ putImage(key: string, data: Uint8Array | string, opts?: PutImageOptions): Promise<PutImageResult>;
129
156
  }
130
157
 
131
158
  /** Vector index handle on `fx.store`. */
@@ -198,6 +225,20 @@ export interface StoreRuntime {
198
225
  ): string[];
199
226
  /** Close all open connections. */
200
227
  close(): Promise<void>;
228
+ /**
229
+ * Capability-gated files handle for `fx.store` (CRUD + image pipeline).
230
+ *
231
+ * Lives on the runtime so the kernel edge profile does not pull Bun.Image.
232
+ *
233
+ * @param decl - Files store declaration
234
+ * @param ctx - Effects + reveal policy for {@link open}
235
+ * @param bridge - fx gate + dry-run hooks
236
+ */
237
+ openFilesFx(
238
+ decl: FilesStoreDecl,
239
+ ctx: StoreInvokeContext,
240
+ bridge: GatedFilesFxBridge,
241
+ ): FilesStoreFxHandle;
201
242
  }
202
243
 
203
244
  /**
@@ -347,6 +388,10 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
347
388
  });
348
389
  fileBuckets.set(decl.name, bucket);
349
390
  }
391
+ const access = {
392
+ get: (key: string) => bucket!.get(key),
393
+ put: (key: string, data: Uint8Array | string) => bucket!.put(key, data),
394
+ };
350
395
  return {
351
396
  ref: `files:${decl.name}`,
352
397
  driverId: bucket.driverId,
@@ -354,6 +399,8 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
354
399
  get: (key) => bucket!.get(key),
355
400
  delete: (key) => bucket!.delete(key),
356
401
  list: (prefix) => bucket!.list(prefix),
402
+ image: (source, imageOpts) => createFilesImagePipeline(access, source, imageOpts),
403
+ putImage: (key, data, putOpts) => putImageToBucket(access, key, data, putOpts),
357
404
  };
358
405
  }
359
406
 
@@ -448,6 +495,19 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
448
495
  fileBuckets.clear();
449
496
  indexes.clear();
450
497
  },
498
+ openFilesFx(decl, ctx, bridge) {
499
+ const cache: { handle?: FilesStoreFxHandle } = {};
500
+ return createGatedFilesStoreHandle(
501
+ decl,
502
+ async () => {
503
+ if (!cache.handle) {
504
+ cache.handle = (await runtime.open(decl, ctx)) as FilesStoreFxHandle;
505
+ }
506
+ return cache.handle;
507
+ },
508
+ bridge,
509
+ );
510
+ },
451
511
  };
452
512
 
453
513
  return runtime;
@@ -122,3 +122,27 @@ export type {
122
122
 
123
123
  export { fileKeyWarnings, contentAddressedKey, projectFileKeys } from "./store/files-policy.ts";
124
124
  export type { FileKeyWarning } from "./store/files-policy.ts";
125
+
126
+ export {
127
+ createFilesImagePipeline,
128
+ putImageToBucket,
129
+ variantObjectKey,
130
+ resolveFilesImageCtorOptions,
131
+ DEFAULT_FILES_IMAGE_MAX_PIXELS,
132
+ } from "./store/files-image.ts";
133
+ export type {
134
+ FilesImageMeta,
135
+ FilesImageOptions,
136
+ FilesImageResizeOptions,
137
+ FilesImageModulateOptions,
138
+ FilesImageJpegOptions,
139
+ FilesImagePngOptions,
140
+ FilesImageWebpOptions,
141
+ FilesImageQualityOptions,
142
+ FilesImageFormat,
143
+ FilesImagePipeline,
144
+ FilesImageBucketAccess,
145
+ ImageVariantSpec,
146
+ PutImageOptions,
147
+ PutImageResult,
148
+ } from "./store/files-image.ts";