@bloklabs/server 1.11.0 → 1.12.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/README.md +57 -24
- package/bin/blok-server.mjs +35 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
# @bloklabs/server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Blok's shared C# server handles file uploads and link previews. Use it inside an ASP.NET Core app, or run the same routes as a standalone npm binary or Docker image.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The current packages store no documents and include no database-block or MySQL integration. Those features follow this delivery migration.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## ASP.NET Core
|
|
8
|
+
|
|
9
|
+
Install the public integration package. NuGet brings in `Blok.Server` transitively.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dotnet add package Blok.Server.AspNetCore
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Register the services, use your application's authorization policy, and map the routes under one prefix:
|
|
16
|
+
|
|
17
|
+
```csharp
|
|
18
|
+
using Blok.Server.AspNetCore;
|
|
19
|
+
|
|
20
|
+
builder.Services
|
|
21
|
+
.AddBlokServer(options =>
|
|
22
|
+
{
|
|
23
|
+
options.StorageDirectory = "./blok-uploads";
|
|
24
|
+
options.PublicUrl = "https://uploads.example.com/files";
|
|
25
|
+
options.UnfurlDisabled = false;
|
|
26
|
+
})
|
|
27
|
+
.UseAuthorization<MyBlokAuthorization>();
|
|
28
|
+
|
|
29
|
+
var app = builder.Build();
|
|
30
|
+
|
|
31
|
+
app.MapBlokServer("/api/blok").RequireAuthorization();
|
|
32
|
+
app.Run();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The mapped group uses your ASP.NET Core authorization policy for the current upload and unfurl routes. Health and validated CORS preflight remain anonymous. `IBlokAuthorization` is the document/database permission seam for later document routes; it is not called for uploads or unfurling.
|
|
36
|
+
|
|
37
|
+
In-process defaults expose health only. Storage and outbound routes must be enabled explicitly, and local storage requires an explicit valid `PublicUrl`. `AddBlokServer(options => { ... })` also accepts the origin, upload-limit, and S3 settings used by the standalone host.
|
|
38
|
+
|
|
39
|
+
## Standalone
|
|
40
|
+
|
|
41
|
+
Run the self-contained host through npm:
|
|
8
42
|
|
|
9
43
|
```bash
|
|
10
44
|
npx @bloklabs/server --listen 127.0.0.1:4000
|
|
11
45
|
```
|
|
12
46
|
|
|
13
|
-
The npm package is a wrapper. On first run it downloads the
|
|
47
|
+
The npm package is a small wrapper. On first run it downloads the C# host for your platform from the matching GitHub release, verifies it against `checksums.txt`, and caches it.
|
|
48
|
+
|
|
49
|
+
The same host is available at the existing image name. This loopback example uses the host network so the service is not exposed directly to the internet:
|
|
14
50
|
|
|
15
51
|
```bash
|
|
16
|
-
docker run
|
|
52
|
+
docker run --network host ghcr.io/jackuait/blok-server \
|
|
53
|
+
--listen 127.0.0.1:4000 \
|
|
54
|
+
--auth proxy
|
|
17
55
|
```
|
|
18
56
|
|
|
57
|
+
For an internet-facing deployment, use `--auth ticket`, set `BLOK_SECRET`, and provide `--allow-origin`. The process refuses unsafe public configurations instead of starting with a warning.
|
|
58
|
+
|
|
19
59
|
## Point the editor at it
|
|
20
60
|
|
|
21
61
|
```ts
|
|
@@ -29,37 +69,30 @@ new Blok({
|
|
|
29
69
|
|
|
30
70
|
body.append('file', file);
|
|
31
71
|
|
|
32
|
-
const
|
|
72
|
+
const response = await fetch('/api/blok/upload', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body,
|
|
75
|
+
});
|
|
33
76
|
|
|
34
|
-
return
|
|
77
|
+
return response.json();
|
|
35
78
|
},
|
|
36
79
|
},
|
|
37
80
|
});
|
|
38
81
|
```
|
|
39
82
|
|
|
40
|
-
The
|
|
83
|
+
The standalone host uses routes at the root. An ASP.NET Core app uses the prefix passed to `MapBlokServer`.
|
|
41
84
|
|
|
42
85
|
## Routes
|
|
43
86
|
|
|
44
87
|
| Route | What it does |
|
|
45
88
|
| --- | --- |
|
|
46
|
-
| `GET /health` |
|
|
47
|
-
| `GET /unfurl?url=…` |
|
|
48
|
-
| `POST /upload` | Stores an uploaded file
|
|
49
|
-
| `POST /upload-by-url` | Fetches a remote
|
|
50
|
-
|
|
51
|
-
Both routes that fetch a URL the browser supplied go through one SSRF-guarded HTTP client, so a pasted `http://169.254.169.254/…` cannot reach your cloud metadata endpoint.
|
|
52
|
-
|
|
53
|
-
## Access modes
|
|
54
|
-
|
|
55
|
-
`--auth` picks who may call the service, and the process refuses to start on a combination that would leave it open:
|
|
56
|
-
|
|
57
|
-
- `none` — no check, and therefore only allowed to bind loopback.
|
|
58
|
-
- `proxy` — trusts every caller, for when something in front of it already authenticated them; loopback only for the same reason.
|
|
59
|
-
- `ticket` — each request carries a short-lived pass your app signs with the shared secret. The only mode allowed to bind a public address.
|
|
89
|
+
| `GET /health` | Reports liveness and the running version |
|
|
90
|
+
| `GET /unfurl?url=…` | Reads title, description, and image metadata |
|
|
91
|
+
| `POST /upload` | Stores an uploaded file |
|
|
92
|
+
| `POST /upload-by-url` | Fetches and stores a remote file |
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
Upload routes exist only when local or S3-compatible storage is configured. Consumer-supplied URLs pass through one guarded outbound client that blocks private and cloud-metadata addresses.
|
|
62
95
|
|
|
63
96
|
## Docs
|
|
64
97
|
|
|
65
|
-
Full
|
|
98
|
+
Full configuration and deployment guidance: [https://blokeditor.com/docs](https://blokeditor.com/docs)
|
package/bin/blok-server.mjs
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* npm wrapper for the blok-server
|
|
3
|
+
* npm wrapper for the published blok-server binary.
|
|
4
4
|
*
|
|
5
|
-
* The package ships no binary. On first run it resolves the archive
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* cache directory, and execs it. Later runs go straight to the cached binary.
|
|
5
|
+
* The package ships no binary. On first run it resolves the archive published
|
|
6
|
+
* for this platform on the matching GitHub release, verifies checksums.txt,
|
|
7
|
+
* unpacks it into a cache directory, and execs it. Later runs use the cache.
|
|
9
8
|
*
|
|
10
|
-
* The
|
|
11
|
-
* it whether or not the Go sources changed, which is why .github/workflows/
|
|
12
|
-
* release-server.yml publishes archives on EVERY tag and skips only the image.
|
|
9
|
+
* The release and npm package use the same family version.
|
|
13
10
|
*/
|
|
14
11
|
import { spawnSync } from 'node:child_process';
|
|
15
12
|
import { createHash } from 'node:crypto';
|
|
@@ -24,28 +21,33 @@ const CHECKSUMS_FILE = 'checksums.txt';
|
|
|
24
21
|
|
|
25
22
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
23
|
|
|
27
|
-
/** Mirrors the goos/goarch matrix in .goreleaser.yaml. */
|
|
28
24
|
const PLATFORMS = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
|
|
29
25
|
const ARCHITECTURES = { x64: 'amd64', arm64: 'arm64' };
|
|
26
|
+
const RUNTIMES = {
|
|
27
|
+
darwin: { x64: 'osx-x64', arm64: 'osx-arm64' },
|
|
28
|
+
linux: { x64: 'linux-x64', arm64: 'linux-arm64' },
|
|
29
|
+
win32: { x64: 'win-x64', arm64: 'win-arm64' },
|
|
30
|
+
};
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* @param {string} platform - process.platform
|
|
33
34
|
* @param {string} arch - process.arch
|
|
34
|
-
* @returns {{ os: string, arch: string, archive: string, binary: string } | null}
|
|
35
|
+
* @returns {{ os: string, arch: string, rid: string, archive: string, binary: string } | null}
|
|
35
36
|
*/
|
|
36
37
|
export function resolveTarget(platform, arch) {
|
|
37
38
|
const os = PLATFORMS[platform];
|
|
38
|
-
const
|
|
39
|
+
const archiveArch = ARCHITECTURES[arch];
|
|
40
|
+
const rid = RUNTIMES[platform]?.[arch];
|
|
39
41
|
|
|
40
|
-
if (os === undefined ||
|
|
42
|
+
if (os === undefined || archiveArch === undefined || rid === undefined) {
|
|
41
43
|
return null;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
// Must match archives.name_template + format_overrides in .goreleaser.yaml.
|
|
45
46
|
return {
|
|
46
47
|
os,
|
|
47
|
-
arch:
|
|
48
|
-
|
|
48
|
+
arch: archiveArch,
|
|
49
|
+
rid,
|
|
50
|
+
archive: `blok-server_${os}_${archiveArch}.${os === 'windows' ? 'zip' : 'tar.gz'}`,
|
|
49
51
|
binary: os === 'windows' ? 'blok-server.exe' : 'blok-server',
|
|
50
52
|
};
|
|
51
53
|
}
|
|
@@ -57,8 +59,8 @@ export function supportedTargets() {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
* Read one asset's expected digest
|
|
61
|
-
*
|
|
62
|
+
* Read one asset's expected digest from a release checksums file, whose lines
|
|
63
|
+
* are `<sha256><two spaces><filename>`.
|
|
62
64
|
*
|
|
63
65
|
* @param {string} contents - checksums.txt body
|
|
64
66
|
* @param {string} assetName - the archive filename to look up
|
|
@@ -131,10 +133,11 @@ function firstWritable(candidates) {
|
|
|
131
133
|
|
|
132
134
|
/**
|
|
133
135
|
* @param {string} url
|
|
136
|
+
* @param {typeof fetch} fetchImpl
|
|
134
137
|
* @returns {Promise<Buffer>}
|
|
135
138
|
*/
|
|
136
|
-
async function download(url) {
|
|
137
|
-
const response = await
|
|
139
|
+
async function download(url, fetchImpl) {
|
|
140
|
+
const response = await fetchImpl(url);
|
|
138
141
|
|
|
139
142
|
if (!response.ok) {
|
|
140
143
|
throw new Error(`GET ${url} → ${response.status} ${response.statusText}`);
|
|
@@ -151,13 +154,19 @@ async function download(url) {
|
|
|
151
154
|
* any point leaves a scratch directory, never a truncated binary at the path the
|
|
152
155
|
* next run would execute.
|
|
153
156
|
*
|
|
154
|
-
* @param {{
|
|
157
|
+
* @param {{
|
|
158
|
+
* version: string,
|
|
159
|
+
* target: NonNullable<ReturnType<typeof resolveTarget>>,
|
|
160
|
+
* destination: string,
|
|
161
|
+
* fetchImpl?: typeof fetch,
|
|
162
|
+
* }} opts
|
|
155
163
|
*/
|
|
156
|
-
async function
|
|
164
|
+
export async function installBinary({ version, target, destination, fetchImpl = fetch }) {
|
|
157
165
|
const base = `${REPO}/releases/download/v${version}`;
|
|
158
|
-
// A 404 here means an unsigned download, so
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
// A 404 here means an unsigned download, so the run must stop.
|
|
167
|
+
const checksums = (
|
|
168
|
+
await download(`${base}/${CHECKSUMS_FILE}`, fetchImpl)
|
|
169
|
+
).toString('utf-8');
|
|
161
170
|
const expected = checksumFor(checksums, target.archive);
|
|
162
171
|
|
|
163
172
|
if (expected === null) {
|
|
@@ -167,7 +176,7 @@ async function fetchBinary({ version, target, destination }) {
|
|
|
167
176
|
const scratch = mkdtempSync(join(dirname(destination), '.download-'));
|
|
168
177
|
|
|
169
178
|
try {
|
|
170
|
-
const archive = await download(`${base}/${target.archive}
|
|
179
|
+
const archive = await download(`${base}/${target.archive}`, fetchImpl);
|
|
171
180
|
const actual = createHash('sha256').update(archive).digest('hex');
|
|
172
181
|
|
|
173
182
|
if (actual !== expected) {
|
|
@@ -253,7 +262,7 @@ async function main() {
|
|
|
253
262
|
binary = join(root, target.binary);
|
|
254
263
|
|
|
255
264
|
if (!existsSync(binary)) {
|
|
256
|
-
await
|
|
265
|
+
await installBinary({ version, target, destination: binary });
|
|
257
266
|
}
|
|
258
267
|
} catch (error) {
|
|
259
268
|
console.error(fallbackMessage(version, error.message));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloklabs/server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Blok
|
|
3
|
+
"version": "1.12.0",
|
|
4
|
+
"description": "Blok server delivery for ASP.NET Core, npm, and Docker — shared C# routes for file uploads and link previews.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "JackUait",
|
|
7
7
|
"homepage": "https://blokeditor.com",
|