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.
- package/AGENTS.md +2 -2
- package/README.md +98 -71
- package/package.json +4 -4
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +90 -20
- package/site/content/docs/elements/clock.mdx +56 -24
- package/site/content/docs/elements/flow.mdx +2 -0
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +464 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +49 -13
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +30 -2
- package/site/content/docs/reference/configuration.mdx +18 -17
- package/site/content/docs/reference/environment-variables.mdx +44 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.ts +4 -4
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/openbao-restart.integration.test.ts +106 -97
- package/src/cli/registry.ts +79 -1
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +1 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/dockerfile.integration.test.ts +126 -119
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/docker/stack.integration.test.ts +118 -102
- package/src/drivers/ai-ollama-tools.integration.test.ts +8 -6
- package/src/drivers/ai-ollama.integration.test.ts +3 -19
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-fcm.ts +49 -53
- package/src/drivers/channel-msegat.ts +61 -0
- package/src/drivers/channel-sently-map.ts +57 -0
- package/src/drivers/channel-sently.test.ts +99 -0
- package/src/drivers/channel-sndr.ts +28 -0
- package/src/drivers/channel-taqnyat.ts +57 -0
- package/src/drivers/channel-types.ts +79 -2
- package/src/drivers/channel-unifonic.ts +26 -43
- package/src/drivers/channel-wa-cloud.ts +33 -47
- package/src/drivers/channel-webpush.ts +39 -239
- package/src/drivers/index.ts +7 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/costs.test.ts +2 -2
- package/src/elements/channel/costs.ts +14 -2
- package/src/elements/channel/mime.ts +11 -0
- package/src/elements/channel/runtime.ts +94 -0
- package/src/elements/channel/sndr-webhooks.test.ts +26 -0
- package/src/elements/channel.ts +10 -1
- package/src/elements/clock/chaos-child.ts +150 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +11 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +1 -0
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/index.ts +9 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store.ts +24 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/kernel/app.ts +3 -1
- package/src/kernel/boot-bind/channel.test.ts +68 -3
- package/src/kernel/boot-bind/channel.ts +93 -2
- package/src/kernel/boot-bind/clock.ts +52 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +247 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.ts +10 -2
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +13 -0
- package/src/kernel/fx.ts +27 -4
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +10 -4
- package/src/release/exports.test.ts +26 -0
- package/src/release/exports.ts +64 -5
- package/src/release/index.ts +5 -0
- package/src/release/measure.exports.test.ts +13 -1
- package/src/release/measure.ts +76 -13
- package/src/release/official-plugins.ts +46 -0
- package/src/release/readme.test.ts +30 -2
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- 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;
|
package/src/elements/store.ts
CHANGED
|
@@ -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";
|