@everystack/server 0.2.24 → 0.2.27
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/jest-preset.js +45 -16
- package/package.json +10 -10
- package/src/image.ts +66 -1
- package/src/plugin.ts +30 -0
package/jest-preset.js
CHANGED
|
@@ -9,28 +9,52 @@
|
|
|
9
9
|
* jest appends `/jest-preset.js` to the preset string itself, so it resolves
|
|
10
10
|
* this file as `@everystack/server/jest-preset.js`.
|
|
11
11
|
*
|
|
12
|
+
* IMPORTANT — this preset OWNS `transform`. Jest gives your config precedence on
|
|
13
|
+
* shared keys, so if your jest config also defines `transform`, it REPLACES the
|
|
14
|
+
* ts-jest block below and the explicit `@everystack/server/testing` pin is lost.
|
|
15
|
+
* A dedicated server-boundary project should set only
|
|
16
|
+
* `{ preset: '@everystack/server' }` (plus non-transform keys). If you must
|
|
17
|
+
* customize, spread this preset and leave `transform`/`moduleNameMapper` alone:
|
|
18
|
+
* const p = require('@everystack/server/jest-preset');
|
|
19
|
+
* module.exports = { ...p, testTimeout: 30000 };
|
|
20
|
+
*
|
|
12
21
|
* Why a preset is needed
|
|
13
22
|
* ----------------------
|
|
14
|
-
* @everystack
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the one mode that reads `exports` AND emits CommonJS for jest. Its hybrid
|
|
22
|
-
* module kind makes ts-jest warn TS151002; we silence only that one code
|
|
23
|
-
* (NOT via `isolatedModules: true`, which would switch ts-jest to
|
|
24
|
-
* transpile-only and drop type-checking entirely).
|
|
23
|
+
* @everystack ships TypeScript source, and `@everystack/server/testing` is a
|
|
24
|
+
* subpath reachable only through the package `exports` map. ts-jest's resolution
|
|
25
|
+
* of that subpath is unreliable on a COLD cache (fresh CI, `jest --no-cache`):
|
|
26
|
+
* it intermittently fails to read the exports map and throws
|
|
27
|
+
* `TS2307: Cannot find module '@everystack/server/testing'` — passing only once a
|
|
28
|
+
* warm cache exists, so it's green locally and red in CI. Flattening the export
|
|
29
|
+
* target does not fix it; an explicit pin does.
|
|
25
30
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
31
|
+
* So this preset resolves its OWN testing entry from disk (below) and pins it two
|
|
32
|
+
* ways, both cache-independent:
|
|
33
|
+
* - tsconfig `paths` → the type-checker resolves it deterministically.
|
|
34
|
+
* - `moduleNameMapper` → the jest runtime resolves it deterministically.
|
|
35
|
+
* It also opts `@everystack/*` back into transformation (source ships as `.ts`,
|
|
36
|
+
* and `node_modules` is not transformed by default).
|
|
30
37
|
*
|
|
31
38
|
* Requires `ts-jest` and `typescript` in the consuming project (already present
|
|
32
39
|
* for any TS jest setup).
|
|
33
40
|
*/
|
|
41
|
+
const path = require('path');
|
|
42
|
+
const fs = require('fs');
|
|
43
|
+
|
|
44
|
+
// Resolve this package's `testing` entry from the preset's own location, so the
|
|
45
|
+
// pin points at a real file regardless of layout (flat src/testing.ts or nested
|
|
46
|
+
// src/testing/index.ts) or where the package is installed.
|
|
47
|
+
const TESTING_ENTRY = ['src/testing.ts', 'src/testing/index.ts']
|
|
48
|
+
.map((rel) => path.join(__dirname, rel))
|
|
49
|
+
.find((abs) => fs.existsSync(abs));
|
|
50
|
+
|
|
51
|
+
const testingPin = TESTING_ENTRY
|
|
52
|
+
? { '@everystack/server/testing': [TESTING_ENTRY] }
|
|
53
|
+
: {};
|
|
54
|
+
const testingMapper = TESTING_ENTRY
|
|
55
|
+
? { '^@everystack/server/testing$': TESTING_ENTRY }
|
|
56
|
+
: {};
|
|
57
|
+
|
|
34
58
|
module.exports = {
|
|
35
59
|
testEnvironment: 'node',
|
|
36
60
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
@@ -46,6 +70,9 @@ module.exports = {
|
|
|
46
70
|
esModuleInterop: true,
|
|
47
71
|
skipLibCheck: true,
|
|
48
72
|
types: ['jest', 'node'],
|
|
73
|
+
baseUrl: __dirname,
|
|
74
|
+
// Pin the exports subpath so the type-checker resolves it cold.
|
|
75
|
+
paths: testingPin,
|
|
49
76
|
},
|
|
50
77
|
// 151002: "hybrid module kind needs isolatedModules" — expected under
|
|
51
78
|
// NodeNext; suppressing it keeps full type-checking on (real type errors
|
|
@@ -56,8 +83,10 @@ module.exports = {
|
|
|
56
83
|
},
|
|
57
84
|
// @everystack packages ship source, not built dist — transform them.
|
|
58
85
|
transformIgnorePatterns: ['/node_modules/(?!@everystack/)'],
|
|
59
|
-
// NodeNext consumers may write `.js` on relative ESM specifiers; map to source.
|
|
60
86
|
moduleNameMapper: {
|
|
87
|
+
// Pin the exports subpath so the jest runtime resolves it cold.
|
|
88
|
+
...testingMapper,
|
|
89
|
+
// NodeNext consumers may write `.js` on relative ESM specifiers; map to source.
|
|
61
90
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
62
91
|
},
|
|
63
92
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -74,6 +74,11 @@
|
|
|
74
74
|
"default": "./src/media.ts"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"test": "jest",
|
|
79
|
+
"build": "tsc --build",
|
|
80
|
+
"lint": "tsc --noEmit"
|
|
81
|
+
},
|
|
77
82
|
"peerDependencies": {
|
|
78
83
|
"esbuild": ">=0.20.0",
|
|
79
84
|
"@aws-sdk/client-cloudfront-keyvaluestore": ">=3.1053.0",
|
|
@@ -123,6 +128,8 @@
|
|
|
123
128
|
"devDependencies": {
|
|
124
129
|
"@aws-sdk/client-cloudfront-keyvaluestore": "3.1053.0",
|
|
125
130
|
"@aws-sdk/signature-v4a": "3.1063.0",
|
|
131
|
+
"@everystack/auth": "workspace:*",
|
|
132
|
+
"@everystack/cli": "workspace:*",
|
|
126
133
|
"@types/aws-lambda": "8.10.161",
|
|
127
134
|
"@types/jest": "29.5.14",
|
|
128
135
|
"@types/node": "22.19.18",
|
|
@@ -132,13 +139,6 @@
|
|
|
132
139
|
"postgres": "3.4.9",
|
|
133
140
|
"sst": "4.13.1",
|
|
134
141
|
"ts-jest": "29.4.9",
|
|
135
|
-
"typescript": "5.9.3"
|
|
136
|
-
"@everystack/auth": "0.2.6",
|
|
137
|
-
"@everystack/cli": "0.2.39"
|
|
138
|
-
},
|
|
139
|
-
"scripts": {
|
|
140
|
-
"test": "jest",
|
|
141
|
-
"build": "tsc --build",
|
|
142
|
-
"lint": "tsc --noEmit"
|
|
142
|
+
"typescript": "5.9.3"
|
|
143
143
|
}
|
|
144
|
-
}
|
|
144
|
+
}
|
package/src/image.ts
CHANGED
|
@@ -155,6 +155,63 @@ export function parseParams(query: Record<string, string | undefined>): Transfor
|
|
|
155
155
|
return params;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* ISOBMFF `ftyp` brands that mean "HEVC-coded HEIF" — i.e. a HEIC still image
|
|
160
|
+
* (iPhones shoot these by default). Sharp reports all of these as format
|
|
161
|
+
* `'heif'`, and our HEIC rescue path (heic-decode) can decode them.
|
|
162
|
+
*/
|
|
163
|
+
const HEIC_BRANDS = new Set([
|
|
164
|
+
'heic', 'heix', 'heim', 'heis', 'hevc', 'hevx', 'hevm', 'hevs', 'mif1', 'msf1',
|
|
165
|
+
]);
|
|
166
|
+
const AVIF_BRANDS = new Set(['avif', 'avis']);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Identify an image format from its magic bytes, independently of Sharp.
|
|
170
|
+
*
|
|
171
|
+
* Sharp builds without libheif cannot even *identify* a HEIF container —
|
|
172
|
+
* `sharp(buf).metadata()` throws, format detection yields `undefined`, and a
|
|
173
|
+
* real HEIC photo is misrouted down the non-image passthrough path (413 when
|
|
174
|
+
* over the passthrough cap, or raw `image/heic` bytes that only Safari renders).
|
|
175
|
+
* Trusting the bytes instead of Sharp makes HEIC detection build-independent and
|
|
176
|
+
* lets the existing `heif` rescue path fire.
|
|
177
|
+
*
|
|
178
|
+
* Returns a Sharp-compatible format string (`jpeg`, `png`, `gif`, `webp`,
|
|
179
|
+
* `heif`, `avif`) or `undefined` when the bytes aren't a recognized image.
|
|
180
|
+
*/
|
|
181
|
+
export function sniffImageFormat(data: Buffer): string | undefined {
|
|
182
|
+
if (data.length < 3) return undefined;
|
|
183
|
+
|
|
184
|
+
// JPEG: FF D8 FF
|
|
185
|
+
if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) return 'jpeg';
|
|
186
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
187
|
+
if (
|
|
188
|
+
data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47 &&
|
|
189
|
+
data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a
|
|
190
|
+
) return 'png';
|
|
191
|
+
// GIF: "GIF87a" / "GIF89a"
|
|
192
|
+
if (data.toString('ascii', 0, 3) === 'GIF') return 'gif';
|
|
193
|
+
// WebP: "RIFF"...."WEBP"
|
|
194
|
+
if (data.toString('ascii', 0, 4) === 'RIFF' && data.toString('ascii', 8, 12) === 'WEBP') return 'webp';
|
|
195
|
+
// TIFF: "II*\0" / "MM\0*"
|
|
196
|
+
if (
|
|
197
|
+
(data[0] === 0x49 && data[1] === 0x49 && data[2] === 0x2a && data[3] === 0x00) ||
|
|
198
|
+
(data[0] === 0x4d && data[1] === 0x4d && data[2] === 0x00 && data[3] === 0x2a)
|
|
199
|
+
) return 'tiff';
|
|
200
|
+
|
|
201
|
+
// ISOBMFF (HEIC/HEIF/AVIF): [4-byte size]["ftyp"][major brand][minor][compatible…]
|
|
202
|
+
if (data.toString('ascii', 4, 8) === 'ftyp') {
|
|
203
|
+
const boxSize = Math.min(data.readUInt32BE(0) || data.length, data.length);
|
|
204
|
+
const brands: string[] = [data.toString('ascii', 8, 12)];
|
|
205
|
+
for (let off = 16; off + 4 <= boxSize; off += 4) {
|
|
206
|
+
brands.push(data.toString('ascii', off, off + 4));
|
|
207
|
+
}
|
|
208
|
+
if (brands.some((b) => HEIC_BRANDS.has(b))) return 'heif';
|
|
209
|
+
if (brands.some((b) => AVIF_BRANDS.has(b))) return 'avif';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
158
215
|
function isNotFound(error: any): boolean {
|
|
159
216
|
return (
|
|
160
217
|
error.name === 'NotFound' ||
|
|
@@ -367,7 +424,15 @@ export function createImageHandler(
|
|
|
367
424
|
const meta = await sharp(originalData).metadata();
|
|
368
425
|
detectedFormat = meta.format; // jpeg, png, webp, gif, tiff, heif, etc.
|
|
369
426
|
} catch {
|
|
370
|
-
// Sharp can't parse it — not an image
|
|
427
|
+
// Sharp can't parse it — not an image, OR a HEIF container this Sharp
|
|
428
|
+
// build has no libheif to identify. Fall through to the magic-byte sniff.
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// A Sharp build without libheif throws on HEIF and leaves detectedFormat
|
|
432
|
+
// undefined, misrouting real HEIC photos to the non-image passthrough
|
|
433
|
+
// below. Sniff the bytes so `heif` is detected and the rescue path fires.
|
|
434
|
+
if (!detectedFormat) {
|
|
435
|
+
detectedFormat = sniffImageFormat(originalData);
|
|
371
436
|
}
|
|
372
437
|
|
|
373
438
|
if (!detectedFormat) {
|
package/src/plugin.ts
CHANGED
|
@@ -582,6 +582,36 @@ export function dbPlugin(options: DbPluginOptions): Plugin {
|
|
|
582
582
|
}
|
|
583
583
|
};
|
|
584
584
|
|
|
585
|
+
// --- db:authz:probe — the authorization red-team's execution path ---
|
|
586
|
+
// Runs the CLI-built probe SQL (SET ROLE + attempt per role/table/command, each in
|
|
587
|
+
// its own savepoint) inside ONE transaction that ALWAYS rolls back, then returns the
|
|
588
|
+
// outcome rows. Unlike db:query this permits writes — they are required to test
|
|
589
|
+
// INSERT/UPDATE/DELETE privileges — but the forced rollback guarantees nothing
|
|
590
|
+
// persists. IAM-gated like every action. The CLI owns the SQL (authz-redteam.ts);
|
|
591
|
+
// this is the thin, transactional, self-reverting runner it needs.
|
|
592
|
+
actions['db:authz:probe'] = async (payload, _ctx) => {
|
|
593
|
+
const { setup, read } = (payload ?? {}) as { setup?: string; read?: string };
|
|
594
|
+
if (!setup || !read) {
|
|
595
|
+
return { error: 'db:authz:probe requires { setup, read } SQL strings' };
|
|
596
|
+
}
|
|
597
|
+
const { sql } = await import('drizzle-orm');
|
|
598
|
+
const PROBE_ROLLBACK = Symbol('authz_probe_rollback');
|
|
599
|
+
let rows: any[] = [];
|
|
600
|
+
try {
|
|
601
|
+
await opsDb.transaction(async (tx: any) => {
|
|
602
|
+
await tx.execute(sql.raw(setup));
|
|
603
|
+
const result: any = await tx.execute(sql.raw(read));
|
|
604
|
+
rows = Array.isArray(result) ? result : result?.rows ?? [];
|
|
605
|
+
throw PROBE_ROLLBACK; // discard every probe write
|
|
606
|
+
});
|
|
607
|
+
} catch (err: unknown) {
|
|
608
|
+
if (err !== PROBE_ROLLBACK) {
|
|
609
|
+
return { error: (err as any)?.message || String(err) };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { rows };
|
|
613
|
+
};
|
|
614
|
+
|
|
585
615
|
// --- db:doctor — "is my database actually secure?" ---
|
|
586
616
|
// Probes the api connection (createDb / DATABASE_URL) and the operator connection
|
|
587
617
|
// (opsDb / ADMIN_DATABASE_URL) from inside the VPC and reports whether the api path
|