@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 CHANGED
@@ -1,21 +1,61 @@
1
1
  # @bloklabs/server
2
2
 
3
- A small Go sidecar for [Blok](https://blokeditor.com). It answers the two requests a rich-text editor cannot make from the browser — storing an uploaded file, and reading the title/description/image of a pasted link — so you do not write a backend for either.
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
- It stores no documents. There is no database, and nothing here is a hosted service you depend on: you run it.
5
+ The current packages store no documents and include no database-block or MySQL integration. Those features follow this delivery migration.
6
6
 
7
- ## Run it
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 binary for your platform from the matching GitHub release, checks it against the release's `checksums.txt`, and caches it. If your network blocks that download, the container image is the same build:
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 -p 4000:4000 ghcr.io/jackuait/blok-server --listen 0.0.0.0:4000
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 res = await fetch('http://127.0.0.1:4000/upload', { method: 'POST', body });
72
+ const response = await fetch('/api/blok/upload', {
73
+ method: 'POST',
74
+ body,
75
+ });
33
76
 
34
- return res.json();
77
+ return response.json();
35
78
  },
36
79
  },
37
80
  });
38
81
  ```
39
82
 
40
- The response shapes match what Blok's own parsers already expect, so nothing on the editor side needs configuring beyond the URL.
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` | Liveness plus the running version |
47
- | `GET /unfurl?url=…` | Title, description and image for a pasted link |
48
- | `POST /upload` | Stores an uploaded file, returns where it landed |
49
- | `POST /upload-by-url` | Fetches a remote URL server-side and re-hosts it |
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
- Storage is a local directory by default (`--storage-dir`), or any S3-compatible bucket via `--s3-bucket` and friends. Credentials are read from `BLOK_S3_ACCESS_KEY` / `BLOK_S3_SECRET_KEY` — never from flags, which land in process listings.
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 flag reference and deployment notes: [https://blokeditor.com/docs](https://blokeditor.com/docs)
98
+ Full configuration and deployment guidance: [https://blokeditor.com/docs](https://blokeditor.com/docs)
@@ -1,15 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * npm wrapper for the blok-server Go binary.
3
+ * npm wrapper for the published blok-server binary.
4
4
  *
5
- * The package ships no binary. On first run it resolves the archive goreleaser
6
- * published for this platform on the GitHub release matching this package's
7
- * version, verifies it against that release's checksums.txt, unpacks it into a
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 version is the family version, not a server version: every release moves
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 goarch = ARCHITECTURES[arch];
39
+ const archiveArch = ARCHITECTURES[arch];
40
+ const rid = RUNTIMES[platform]?.[arch];
39
41
 
40
- if (os === undefined || goarch === 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: goarch,
48
- archive: `blok-server_${os}_${goarch}.${os === 'windows' ? 'zip' : 'tar.gz'}`,
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 out of a goreleaser checksums file, whose
61
- * lines are `<sha256><two spaces><filename>`.
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 fetch(url);
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 {{ version: string, target: ReturnType<typeof resolveTarget>, destination: string }} opts
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 fetchBinary({ version, target, destination }) {
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 it aborts the run rather than
159
- // falling through to "verification unavailable, running it anyway".
160
- const checksums = (await download(`${base}/${CHECKSUMS_FILE}`)).toString('utf-8');
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 fetchBinary({ version, target, destination: binary });
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.11.0",
4
- "description": "Blok backend service — a small Go sidecar that handles file uploads and link previews, so you write no backend code for either.",
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",