@cosmicdrift/kumiko-bundled-features 0.20.0 → 0.21.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/LICENSE
CHANGED
|
@@ -21,7 +21,7 @@ managed offering of the Licensed Work.
|
|
|
21
21
|
This restriction does not apply to the Licensor, any entity controlled by,
|
|
22
22
|
controlling, or under common control with the Licensor ("Affiliates"), or
|
|
23
23
|
contractors acting on their behalf. The Licensor remains free to use the
|
|
24
|
-
Licensed Work for any purpose, including for the operation of kumiko.
|
|
24
|
+
Licensed Work for any purpose, including for the operation of kumiko.rocks.
|
|
25
25
|
|
|
26
26
|
Change Date: 2030-05-05
|
|
27
27
|
Change License: Apache License, Version 2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
|
|
14
14
|
},
|
|
15
|
-
"homepage": "https://kumiko.
|
|
15
|
+
"homepage": "https://kumiko.rocks",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"kumiko": {
|
|
18
18
|
"runtime": "runtime"
|
|
@@ -72,13 +72,10 @@
|
|
|
72
72
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@
|
|
76
|
-
"@
|
|
77
|
-
"@
|
|
78
|
-
"@cosmicdrift/kumiko-
|
|
79
|
-
"@cosmicdrift/kumiko-framework": "0.14.0",
|
|
80
|
-
"@cosmicdrift/kumiko-renderer": "0.14.0",
|
|
81
|
-
"@cosmicdrift/kumiko-renderer-web": "0.14.0",
|
|
75
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.21.0",
|
|
76
|
+
"@cosmicdrift/kumiko-framework": "0.21.0",
|
|
77
|
+
"@cosmicdrift/kumiko-renderer": "0.21.0",
|
|
78
|
+
"@cosmicdrift/kumiko-renderer-web": "0.21.0",
|
|
82
79
|
"@mollie/api-client": "^4.5.0",
|
|
83
80
|
"@node-rs/argon2": "^2.0.2",
|
|
84
81
|
"@types/nodemailer": "^8.0.0",
|
|
@@ -60,7 +60,7 @@ export const incrementRollingCapHandler: WriteHandlerDef = {
|
|
|
60
60
|
const payload = event.payload as IncrementRollingPayload;
|
|
61
61
|
const aggregateId = rollingCapAggregateId(event.user.tenantId, payload.capName);
|
|
62
62
|
|
|
63
|
-
// unsafeAppendEvent — bundled-features-Pfad (apps mit
|
|
63
|
+
// unsafeAppendEvent — bundled-features-Pfad (apps mit bun kumiko
|
|
64
64
|
// codegen kriegen den strict-typed appendEvent-Wrapper). Schema-
|
|
65
65
|
// Validation läuft trotzdem, weil r.defineEvent das Schema
|
|
66
66
|
// registriert hat.
|
|
@@ -131,7 +131,7 @@ export function createSetWriteHandler(getRuntime: (() => GlobalFeatureToggleRunt
|
|
|
131
131
|
// This mirrors how `config` handles the same constraint for
|
|
132
132
|
// its config-changed events.
|
|
133
133
|
// unsafeAppendEvent — bundled-features ohne lokalen Wrapper. Apps
|
|
134
|
-
// mit `
|
|
134
|
+
// mit `bun kumiko codegen` kriegen `.kumiko/define.ts` als strict-
|
|
135
135
|
// path; bundled-features bleibt bei der unsafe-Variante. Schema-
|
|
136
136
|
// Validation läuft trotzdem via r.defineEvent("toggle-set", ...).
|
|
137
137
|
await ctx.unsafeAppendEvent({
|
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
import {
|
|
3
|
-
DeleteObjectCommand,
|
|
4
|
-
GetObjectCommand,
|
|
5
|
-
HeadObjectCommand,
|
|
6
|
-
PutObjectCommand,
|
|
7
|
-
S3Client,
|
|
8
|
-
} from "@aws-sdk/client-s3";
|
|
9
|
-
import { Upload } from "@aws-sdk/lib-storage";
|
|
10
|
-
import { getSignedUrl as presign } from "@aws-sdk/s3-request-presigner";
|
|
11
1
|
import type { FileStorageProvider, SignedUrlOptions } from "@cosmicdrift/kumiko-framework/files";
|
|
12
2
|
|
|
13
3
|
// =============================================================================
|
|
14
4
|
// Operator-Pflicht-Setup (Multipart-Upload-Cleanup)
|
|
15
5
|
// =============================================================================
|
|
16
6
|
//
|
|
17
|
-
// `writeStream` nutzt
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// wird (Pod-Restart, K8s-OOM-Kill, Process-Signal), bleibt die Multipart-
|
|
24
|
-
// Upload-Session in S3 OFFEN. S3 behaelt die bereits hochgeladenen Parts
|
|
25
|
-
// und berechnet Storage-Kosten dafuer — bis sie manuell oder via Lifecycle-
|
|
26
|
-
// Rule abgebrochen werden.
|
|
7
|
+
// `writeStream` nutzt Bun's S3-Writer fuer echtes multipart-streaming. S3
|
|
8
|
+
// created dabei eine Multipart-Upload-Session mit einer Upload-ID; bei
|
|
9
|
+
// normaler Completion wird sie geschlossen. Wird der Export-Worker mid-write
|
|
10
|
+
// gecancelt (Pod-Restart, K8s-OOM-Kill, Process-Signal), bleibt die Session
|
|
11
|
+
// in S3 OFFEN und berechnet Storage-Kosten fuer die bereits hochgeladenen
|
|
12
|
+
// Parts — bis sie via Lifecycle-Rule abgebrochen werden.
|
|
27
13
|
//
|
|
28
14
|
// **Pflicht-Operator-Setup auf jedem Bucket:**
|
|
29
15
|
//
|
|
@@ -39,16 +25,9 @@ import type { FileStorageProvider, SignedUrlOptions } from "@cosmicdrift/kumiko-
|
|
|
39
25
|
// AWS-CLI: `aws s3api put-bucket-lifecycle-configuration --bucket <name>
|
|
40
26
|
// --lifecycle-configuration file://lifecycle.json`. Hetzner Object Storage
|
|
41
27
|
// + R2 + Minio supporten dieselbe Syntax.
|
|
42
|
-
//
|
|
43
|
-
// **Code-side abort()** fuer graceful Worker-Shutdown ist follow-up. Das
|
|
44
|
-
// braucht Worker-Cancel-Semantik (AbortSignal-Propagation durch r.job),
|
|
45
|
-
// die im framework noch nicht existiert. Bis dahin ist die Lifecycle-
|
|
46
|
-
// Rule die einzige Garantie gegen Storage-Leakage.
|
|
47
28
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// the same way they'd pass createLocalProvider in dev.
|
|
51
|
-
//
|
|
29
|
+
const STREAM_PART_SIZE = 5 * 1024 * 1024;
|
|
30
|
+
|
|
52
31
|
// `endpoint` + `forcePathStyle` are the R2/Minio knobs: AWS-S3 uses
|
|
53
32
|
// virtual-host-style URLs (bucket.s3.region.amazonaws.com), Minio and many
|
|
54
33
|
// S3-compat providers need path-style (endpoint/bucket/key). Default
|
|
@@ -67,8 +46,7 @@ export type S3ProviderConfig = {
|
|
|
67
46
|
|
|
68
47
|
// Exported for unit testing — the branch logic (explicit override vs.
|
|
69
48
|
// auto-detect from endpoint) is small but load-bearing: Minio/R2 break
|
|
70
|
-
// silently if the virtual-host-style is picked.
|
|
71
|
-
// without constructing an S3Client means the rule stays honest.
|
|
49
|
+
// silently if the virtual-host-style is picked.
|
|
72
50
|
export function resolveForcePathStyle(config: S3ProviderConfig): boolean {
|
|
73
51
|
// Explicit override wins; otherwise: custom endpoint → path-style
|
|
74
52
|
// (that's the shape every non-AWS S3-compatible provider expects),
|
|
@@ -77,85 +55,58 @@ export function resolveForcePathStyle(config: S3ProviderConfig): boolean {
|
|
|
77
55
|
}
|
|
78
56
|
|
|
79
57
|
export function createS3Provider(config: S3ProviderConfig): FileStorageProvider {
|
|
80
|
-
const client = new S3Client({
|
|
58
|
+
const client = new Bun.S3Client({
|
|
81
59
|
region: config.region,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
60
|
+
accessKeyId: config.accessKeyId,
|
|
61
|
+
secretAccessKey: config.secretAccessKey,
|
|
62
|
+
bucket: config.bucket,
|
|
86
63
|
...(config.endpoint !== undefined && { endpoint: config.endpoint }),
|
|
87
|
-
forcePathStyle
|
|
64
|
+
// Bun's virtualHostedStyle is the inverse of the AWS-SDK forcePathStyle
|
|
65
|
+
// knob this config exposes: path-style ⇔ virtualHostedStyle=false.
|
|
66
|
+
virtualHostedStyle: !resolveForcePathStyle(config),
|
|
88
67
|
});
|
|
89
68
|
|
|
90
69
|
return {
|
|
91
70
|
async write(key, data, mimeType): Promise<void> {
|
|
92
|
-
await client.
|
|
93
|
-
new PutObjectCommand({
|
|
94
|
-
Bucket: config.bucket,
|
|
95
|
-
Key: key,
|
|
96
|
-
Body: data,
|
|
97
|
-
...(mimeType !== undefined && { ContentType: mimeType }),
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
71
|
+
await client.write(key, data, mimeType !== undefined ? { type: mimeType } : undefined);
|
|
100
72
|
},
|
|
101
73
|
|
|
102
74
|
async writeStream(key, source, options): Promise<void> {
|
|
103
|
-
// Echtes multipart-streaming via
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Readable.from(source) adapiert AsyncIterable → node:Readable —
|
|
114
|
-
// lib-storage's Body-Type akzeptiert Web-ReadableStream + node-
|
|
115
|
-
// Readable, nicht direkt AsyncIterable. Adapter ist zero-copy.
|
|
116
|
-
const body = Readable.from(source);
|
|
117
|
-
const upload = new Upload({
|
|
118
|
-
client,
|
|
119
|
-
params: {
|
|
120
|
-
Bucket: config.bucket,
|
|
121
|
-
Key: key,
|
|
122
|
-
Body: body,
|
|
123
|
-
...(options?.mimeType !== undefined && { ContentType: options.mimeType }),
|
|
124
|
-
},
|
|
75
|
+
// Echtes multipart-streaming via Bun's S3-Writer — der Source-
|
|
76
|
+
// AsyncIterable wird chunk-weise hochgeladen, niemals alles im Memory.
|
|
77
|
+
// Wir flushen sobald ein Part voll ist, damit der Heap-Footprint auf
|
|
78
|
+
// ~ein Part begrenzt bleibt, unabhaengig von der Total-Bundle-Size —
|
|
79
|
+
// macht 1GB+ Exports ohne OOM moeglich.
|
|
80
|
+
const writer = client.file(key).writer({
|
|
81
|
+
...(options?.mimeType !== undefined && { type: options.mimeType }),
|
|
82
|
+
retry: 3,
|
|
83
|
+
queueSize: 4,
|
|
84
|
+
partSize: STREAM_PART_SIZE,
|
|
125
85
|
});
|
|
126
|
-
|
|
86
|
+
let buffered = 0;
|
|
87
|
+
for await (const chunk of source) {
|
|
88
|
+
// write() returns a Promise when the writer applies backpressure —
|
|
89
|
+
// awaiting it bounds the in-flight queue instead of buffering ahead.
|
|
90
|
+
buffered += await writer.write(chunk);
|
|
91
|
+
if (buffered >= STREAM_PART_SIZE) {
|
|
92
|
+
await writer.flush();
|
|
93
|
+
buffered = 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await writer.end();
|
|
127
97
|
},
|
|
128
98
|
|
|
129
99
|
async read(key): Promise<Uint8Array> {
|
|
130
|
-
|
|
131
|
-
if (!response.Body) {
|
|
132
|
-
throw new Error(`s3_read_empty_body: ${key}`);
|
|
133
|
-
}
|
|
134
|
-
// transformToByteArray is the stream-to-bytes helper the v3 SDK ships
|
|
135
|
-
// with — avoids us reinventing a ReadableStream reader. Returns a
|
|
136
|
-
// Uint8Array, which is what FileStorageProvider.read() promises.
|
|
137
|
-
return response.Body.transformToByteArray();
|
|
100
|
+
return new Uint8Array(await client.file(key).arrayBuffer());
|
|
138
101
|
},
|
|
139
102
|
|
|
140
103
|
readStream(key): AsyncIterable<Uint8Array> {
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// GetObject-Request abgesetzt. Wenn der Key nicht existiert, faellt
|
|
145
|
-
// der Error genau dort (nicht beim readStream-Aufruf) — gleiches
|
|
146
|
-
// Lazy-Verhalten wie inmemory + local.
|
|
104
|
+
// Lazy: erst beim ersten chunk-pull wird der GET-Request abgesetzt.
|
|
105
|
+
// Existiert der Key nicht, faellt der Error genau dort (nicht beim
|
|
106
|
+
// readStream-Aufruf) — gleiches Lazy-Verhalten wie inmemory + local.
|
|
147
107
|
return {
|
|
148
108
|
async *[Symbol.asyncIterator]() {
|
|
149
|
-
const
|
|
150
|
-
new GetObjectCommand({ Bucket: config.bucket, Key: key }),
|
|
151
|
-
);
|
|
152
|
-
if (!response.Body) {
|
|
153
|
-
throw new Error(`s3_read_empty_body: ${key}`);
|
|
154
|
-
}
|
|
155
|
-
// SdkStream is AsyncIterable<Buffer> on node. Buffer extends
|
|
156
|
-
// Uint8Array; cast sichert die Surface ohne neue runtime-deps.
|
|
157
|
-
const body = response.Body as AsyncIterable<Uint8Array>; // @cast-boundary engine-bridge
|
|
158
|
-
for await (const chunk of body) {
|
|
109
|
+
for await (const chunk of client.file(key).stream()) {
|
|
159
110
|
yield chunk;
|
|
160
111
|
}
|
|
161
112
|
},
|
|
@@ -163,23 +114,11 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
163
114
|
},
|
|
164
115
|
|
|
165
116
|
async delete(key): Promise<void> {
|
|
166
|
-
await client.
|
|
117
|
+
await client.delete(key);
|
|
167
118
|
},
|
|
168
119
|
|
|
169
120
|
async exists(key): Promise<boolean> {
|
|
170
|
-
|
|
171
|
-
await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: key }));
|
|
172
|
-
return true;
|
|
173
|
-
} catch (error) {
|
|
174
|
-
// S3 SDK throws either NotFound or a generic 404. Check both the
|
|
175
|
-
// `.name` property (newer SDKs) and the `$metadata.httpStatusCode`
|
|
176
|
-
// (what the SDK guarantees on every error).
|
|
177
|
-
const err = error as { name?: string; $metadata?: { httpStatusCode?: number } }; // @cast-boundary error-details
|
|
178
|
-
if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
throw error;
|
|
182
|
-
}
|
|
121
|
+
return client.exists(key);
|
|
183
122
|
},
|
|
184
123
|
|
|
185
124
|
async getSignedUrl(
|
|
@@ -187,17 +126,16 @@ export function createS3Provider(config: S3ProviderConfig): FileStorageProvider
|
|
|
187
126
|
expiresInSeconds: number,
|
|
188
127
|
options?: SignedUrlOptions,
|
|
189
128
|
): Promise<string> {
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
129
|
+
// contentDisposition wird von Bun als response-content-disposition
|
|
130
|
+
// Query-Param signiert (Response-Override fuer den GET-Download) —
|
|
131
|
+
// der Browser sieht den Original-Dateinamen statt des UUID-Keys.
|
|
132
|
+
return client.presign(key, {
|
|
133
|
+
expiresIn: expiresInSeconds,
|
|
134
|
+
method: "GET",
|
|
196
135
|
...(options?.contentDisposition !== undefined && {
|
|
197
|
-
|
|
136
|
+
contentDisposition: options.contentDisposition,
|
|
198
137
|
}),
|
|
199
138
|
});
|
|
200
|
-
return presign(client, command, { expiresIn: expiresInSeconds });
|
|
201
139
|
},
|
|
202
140
|
};
|
|
203
141
|
}
|
|
@@ -10,7 +10,7 @@ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
10
10
|
* compile-time-checked.
|
|
11
11
|
*
|
|
12
12
|
* Die Tier-Engine selbst ist **agnostisch** zu konkreten Tier-Werten und
|
|
13
|
-
* Cap-Dimensionen. Jede App definiert ihre TierMap: kumiko.
|
|
13
|
+
* Cap-Dimensionen. Jede App definiert ihre TierMap: kumiko.rocks hat
|
|
14
14
|
* free/pro/business/enterprise/self-host, PublicStatus hat
|
|
15
15
|
* free/starter/team/agency. Die Engine speichert nur den Tier-Namen als
|
|
16
16
|
* String und vertraut der App's TierMap, was das beim Boot bedeutet.
|