@nuasite/cms-sidecar 0.43.0-beta.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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bun
2
+ import { createCmsCore, createNodeFs } from '@nuasite/cms-core';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { mediaFromEnv } from './media-from-env';
6
+ import { createServer } from './server';
7
+ /** Resolve the installed `@nuasite/cms-core` version for the capabilities/health payload. */
8
+ function resolveCoreVersion() {
9
+ try {
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require('@nuasite/cms-core/package.json');
12
+ if (pkg && typeof pkg === 'object' && 'version' in pkg && typeof pkg.version === 'string') {
13
+ return pkg.version;
14
+ }
15
+ }
16
+ catch {
17
+ // fall through
18
+ }
19
+ return '0.0.0';
20
+ }
21
+ /** Read `--<name> <value>` (or `--<name>=<value>`) from argv. */
22
+ function readFlag(args, name) {
23
+ const eq = `--${name}=`;
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg === `--${name}`)
27
+ return args[i + 1];
28
+ if (arg.startsWith(eq))
29
+ return arg.slice(eq.length);
30
+ }
31
+ return undefined;
32
+ }
33
+ function parseServeArgs(args) {
34
+ const rawPort = readFlag(args, 'port');
35
+ const port = rawPort !== undefined ? Number.parseInt(rawPort, 10) : Number.NaN;
36
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
37
+ throw new Error('serve requires a valid --port <0-65535>');
38
+ }
39
+ const rawRoot = readFlag(args, 'root');
40
+ const root = path.resolve(rawRoot ?? process.cwd());
41
+ const contentDir = readFlag(args, 'content-dir');
42
+ return { port, root, contentDir };
43
+ }
44
+ function printUsage() {
45
+ console.log('Usage: cms-sidecar serve --port <port> [--root <dir>] [--content-dir <dir>]');
46
+ console.log('');
47
+ console.log('Runs a thin HTTP server exposing @nuasite/cms-core over /cms/v1.');
48
+ console.log('Media adapter is selected by CMS_MEDIA_ADAPTER (contember|s3|local|none).');
49
+ }
50
+ function serve(args) {
51
+ const { port, root, contentDir } = parseServeArgs(args);
52
+ const fs = createNodeFs(root);
53
+ const media = mediaFromEnv();
54
+ const core = createCmsCore(fs, { contentDir, media: media.adapter });
55
+ const coreVersion = resolveCoreVersion();
56
+ const server = createServer({ core, fs, root, coreVersion, contentDir });
57
+ const bunServer = Bun.serve({ port, fetch: server.fetch });
58
+ // Ready line — the contract with the F2 service runtime (`readyPattern`).
59
+ // Must match /cms-sidecar listening on .*:\d+/.
60
+ console.log(`cms-sidecar listening on :${bunServer.port}`);
61
+ }
62
+ const [, , command, ...args] = process.argv;
63
+ switch (command) {
64
+ case 'serve':
65
+ serve(args);
66
+ break;
67
+ case 'help':
68
+ case '--help':
69
+ case '-h':
70
+ case undefined:
71
+ printUsage();
72
+ break;
73
+ default:
74
+ console.error(`Unknown command: ${command}`);
75
+ printUsage();
76
+ process.exit(1);
77
+ }
@@ -0,0 +1,26 @@
1
+ import type { CmsFileSystem } from '@nuasite/cms-core';
2
+ /**
3
+ * Hashing + per-file serialization for the sidecar layer.
4
+ *
5
+ * cms-core stays hash-agnostic: the optimistic-concurrency `baseHash`/`sourceHash`
6
+ * comparison and the in-process mutex live here. Hashing reads the on-disk source
7
+ * through the same `CmsFileSystem` port cms-core uses, so the hash reflects exactly
8
+ * the bytes a subsequent `getEntry` would parse.
9
+ */
10
+ /** Stable content hash of a UTF-8 string: `sha256:<hex>`. */
11
+ export declare function hashContent(content: string): string;
12
+ /**
13
+ * Hash the current on-disk source at `sourcePath`, or `null` when the file does
14
+ * not exist. Reads via the port so the hash matches the bytes cms-core sees.
15
+ */
16
+ export declare function hashSource(fs: CmsFileSystem, sourcePath: string): Promise<string | null>;
17
+ /**
18
+ * Serializes async work keyed by a string (a source path). Concurrent mutations
19
+ * of the same entry run one-after-another; different entries run in parallel.
20
+ * The chain self-cleans: a key's tail is dropped once its last waiter settles.
21
+ */
22
+ export declare class KeyedMutex {
23
+ private readonly tails;
24
+ runExclusive<T>(key: string, task: () => Promise<T>): Promise<T>;
25
+ }
26
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../../src/concurrency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAGtD;;;;;;;GAOG;AAEH,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAI9F;AAED;;;;GAIG;AACH,qBAAa,UAAU;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsC;IAEtD,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAetE"}
@@ -0,0 +1,47 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Hashing + per-file serialization for the sidecar layer.
4
+ *
5
+ * cms-core stays hash-agnostic: the optimistic-concurrency `baseHash`/`sourceHash`
6
+ * comparison and the in-process mutex live here. Hashing reads the on-disk source
7
+ * through the same `CmsFileSystem` port cms-core uses, so the hash reflects exactly
8
+ * the bytes a subsequent `getEntry` would parse.
9
+ */
10
+ /** Stable content hash of a UTF-8 string: `sha256:<hex>`. */
11
+ export function hashContent(content) {
12
+ return `sha256:${createHash('sha256').update(content, 'utf-8').digest('hex')}`;
13
+ }
14
+ /**
15
+ * Hash the current on-disk source at `sourcePath`, or `null` when the file does
16
+ * not exist. Reads via the port so the hash matches the bytes cms-core sees.
17
+ */
18
+ export async function hashSource(fs, sourcePath) {
19
+ if (!(await fs.exists(sourcePath)))
20
+ return null;
21
+ const raw = await fs.readFile(sourcePath);
22
+ return hashContent(raw);
23
+ }
24
+ /**
25
+ * Serializes async work keyed by a string (a source path). Concurrent mutations
26
+ * of the same entry run one-after-another; different entries run in parallel.
27
+ * The chain self-cleans: a key's tail is dropped once its last waiter settles.
28
+ */
29
+ export class KeyedMutex {
30
+ tails = new Map();
31
+ async runExclusive(key, task) {
32
+ const previous = this.tails.get(key) ?? Promise.resolve();
33
+ // Chain after the previous holder, swallowing its result/rejection so one
34
+ // failed mutation never poisons the next waiter on the same key.
35
+ const run = previous.then(() => task(), () => task());
36
+ this.tails.set(key, run);
37
+ try {
38
+ return await run;
39
+ }
40
+ finally {
41
+ // Drop the entry only if no later waiter has chained onto it.
42
+ if (this.tails.get(key) === run) {
43
+ this.tails.delete(key);
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,7 @@
1
+ export { FIELD_TYPES, isFieldType } from '@nuasite/cms-types';
2
+ export { hashContent, hashSource, KeyedMutex } from './concurrency';
3
+ export { type MediaAdapterKind, mediaFromEnv, type MediaFromEnvResult } from './media-from-env';
4
+ export { type CmsSidecarServer, createServer, type CreateServerOptions, SIDECAR_FEATURES } from './server';
5
+ export type { AddArrayItemBody, ApiError, Capabilities, ConflictResponse, CreateEntryBody, CreateFolderBody, EntriesListResult, EntriesQuery, ErrorCode, PageEntry, ProjectModel, RemoveArrayItemBody, RenameEntryBody, UpdateEntryBody, } from './types';
6
+ export { STATUS_BY_CODE } from './types';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AACnE,OAAO,EAAE,KAAK,gBAAgB,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAAE,KAAK,gBAAgB,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC1G,YAAY,EACX,gBAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,eAAe,GACf,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { hashContent, hashSource, KeyedMutex } from './concurrency';
2
+ export { mediaFromEnv } from './media-from-env';
3
+ export { createServer, SIDECAR_FEATURES } from './server';
4
+ export { STATUS_BY_CODE } from './types';
@@ -0,0 +1,32 @@
1
+ import type { MediaStorageAdapter } from '@nuasite/cms-types';
2
+ /**
3
+ * Pick the media storage adapter from the environment.
4
+ *
5
+ * `CMS_MEDIA_ADAPTER` selects the backend:
6
+ * - `contember` (default): proxies to the Contember worker's CMS media API.
7
+ * NUA_SITE_API_BASE_URL — worker API base, e.g. https://api.example.com
8
+ * NUA_SITE_PROJECT_SLUG — project slug used in the media API path
9
+ * NUA_SITE_SESSION_TOKEN — session token (NUA_SITE_SESSION_TOKEN cookie value)
10
+ * - `s3`: an S3-compatible bucket (needs the optional `@aws-sdk/client-s3` peer).
11
+ * CMS_MEDIA_S3_BUCKET — bucket name (required)
12
+ * CMS_MEDIA_S3_REGION — region (required)
13
+ * CMS_MEDIA_S3_ACCESS_KEY_ID — access key (optional; falls back to the SDK chain)
14
+ * CMS_MEDIA_S3_SECRET_ACCESS_KEY — secret key (optional)
15
+ * CMS_MEDIA_S3_ENDPOINT — custom endpoint (optional, e.g. R2)
16
+ * CMS_MEDIA_S3_CDN_PREFIX — public CDN URL prefix (optional)
17
+ * CMS_MEDIA_S3_PREFIX — key prefix (optional, default 'uploads')
18
+ * - `local`: filesystem under `public/uploads`.
19
+ * CMS_MEDIA_LOCAL_DIR — storage dir (optional, default 'public/uploads')
20
+ * CMS_MEDIA_LOCAL_URL_PREFIX — served URL prefix (optional, default '/uploads')
21
+ * - `none`: no media adapter (media routes answer 501 unsupported).
22
+ *
23
+ * Reads are confined to this module; the adapters themselves never touch
24
+ * `process.env`. Returns `undefined` when no adapter is configured.
25
+ */
26
+ export type MediaAdapterKind = 'contember' | 's3' | 'local' | 'none';
27
+ export interface MediaFromEnvResult {
28
+ kind: MediaAdapterKind;
29
+ adapter?: MediaStorageAdapter;
30
+ }
31
+ export declare function mediaFromEnv(env?: NodeJS.ProcessEnv): MediaFromEnvResult;
32
+ //# sourceMappingURL=media-from-env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-from-env.d.ts","sourceRoot":"","sources":["../../src/media-from-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,CAAA;AAQpE,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,gBAAgB,CAAA;IACtB,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAC7B;AAED,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,kBAAkB,CAuDrF"}
@@ -0,0 +1,57 @@
1
+ import { createContemberStorageAdapter, createLocalStorageAdapter, createS3StorageAdapter, } from '@nuasite/cms-core';
2
+ const VALID_KINDS = ['contember', 's3', 'local', 'none'];
3
+ function isMediaAdapterKind(value) {
4
+ return VALID_KINDS.includes(value);
5
+ }
6
+ export function mediaFromEnv(env = process.env) {
7
+ const requested = env.CMS_MEDIA_ADAPTER?.trim().toLowerCase();
8
+ const kind = requested && isMediaAdapterKind(requested) ? requested : 'contember';
9
+ switch (kind) {
10
+ case 'none':
11
+ return { kind };
12
+ case 'local':
13
+ return {
14
+ kind,
15
+ adapter: createLocalStorageAdapter({
16
+ dir: env.CMS_MEDIA_LOCAL_DIR,
17
+ urlPrefix: env.CMS_MEDIA_LOCAL_URL_PREFIX,
18
+ }),
19
+ };
20
+ case 's3': {
21
+ const bucket = env.CMS_MEDIA_S3_BUCKET;
22
+ const region = env.CMS_MEDIA_S3_REGION;
23
+ if (!bucket || !region) {
24
+ throw new Error('CMS_MEDIA_ADAPTER=s3 requires CMS_MEDIA_S3_BUCKET and CMS_MEDIA_S3_REGION');
25
+ }
26
+ return {
27
+ kind,
28
+ adapter: createS3StorageAdapter({
29
+ bucket,
30
+ region,
31
+ accessKeyId: env.CMS_MEDIA_S3_ACCESS_KEY_ID,
32
+ secretAccessKey: env.CMS_MEDIA_S3_SECRET_ACCESS_KEY,
33
+ endpoint: env.CMS_MEDIA_S3_ENDPOINT,
34
+ cdnPrefix: env.CMS_MEDIA_S3_CDN_PREFIX,
35
+ prefix: env.CMS_MEDIA_S3_PREFIX,
36
+ }),
37
+ };
38
+ }
39
+ case 'contember': {
40
+ const apiBaseUrl = env.NUA_SITE_API_BASE_URL;
41
+ const projectSlug = env.NUA_SITE_PROJECT_SLUG;
42
+ // Without the API base + project slug we cannot reach the worker; treat as
43
+ // "no media configured" rather than constructing a broken adapter.
44
+ if (!apiBaseUrl || !projectSlug) {
45
+ return { kind: 'none' };
46
+ }
47
+ return {
48
+ kind,
49
+ adapter: createContemberStorageAdapter({
50
+ apiBaseUrl,
51
+ projectSlug,
52
+ sessionToken: env.NUA_SITE_SESSION_TOKEN,
53
+ }),
54
+ };
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,22 @@
1
+ import type { CmsCore, CmsFileSystem } from '@nuasite/cms-core';
2
+ /** Features the sidecar advertises so older/newer clients can degrade gracefully. */
3
+ export declare const SIDECAR_FEATURES: readonly string[];
4
+ export interface CreateServerOptions {
5
+ core: CmsCore;
6
+ /** The same `CmsFileSystem` port the core was built over — used for hashing and the page walk. */
7
+ fs: CmsFileSystem;
8
+ /** Resolved project root (absolute), surfaced in `/health`. */
9
+ root: string;
10
+ /** Version reported as `coreVersion` (the cms-core package version). */
11
+ coreVersion: string;
12
+ /** Content collections directory, relative to root. Defaults to `src/content`. */
13
+ contentDir?: string;
14
+ /** Max accepted upload size in bytes. Defaults to 20 MiB. */
15
+ maxUploadSize?: number;
16
+ }
17
+ export interface CmsSidecarServer {
18
+ /** The `fetch` handler — pass to `Bun.serve`, or drive directly in tests. */
19
+ fetch(req: Request): Promise<Response>;
20
+ }
21
+ export declare function createServer(opts: CreateServerOptions): CmsSidecarServer;
22
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAqB/D,qFAAqF;AACrF,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAc7C,CAAA;AAMD,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,kGAAkG;IAClG,EAAE,EAAE,aAAa,CAAA;IACjB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAA;IACnB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAA;CACtB;AA+LD,MAAM,WAAW,gBAAgB;IAChC,6EAA6E;IAC7E,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtC;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,gBAAgB,CA+YxE"}