@xtrable-ltd/nanoesis 0.1.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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/adapter-azure-blob.d.ts +97 -0
  4. package/dist/adapter-azure-blob.js +127 -0
  5. package/dist/adapter-cloudflare.d.ts +28 -0
  6. package/dist/adapter-cloudflare.js +32 -0
  7. package/dist/adapter-fs.d.ts +38 -0
  8. package/dist/adapter-fs.js +54 -0
  9. package/dist/adapter-local-jwt.d.ts +205 -0
  10. package/dist/adapter-local-jwt.js +550 -0
  11. package/dist/adapter-sharp.d.ts +11 -0
  12. package/dist/adapter-sharp.js +39 -0
  13. package/dist/adapter-shell.d.ts +48 -0
  14. package/dist/adapter-shell.js +56 -0
  15. package/dist/adapter-trusted-header.d.ts +43 -0
  16. package/dist/adapter-trusted-header.js +21 -0
  17. package/dist/chunk-G2UEZTYC.js +2541 -0
  18. package/dist/editor-api.d.ts +198 -0
  19. package/dist/editor-api.js +592 -0
  20. package/dist/editor.d.ts +13 -0
  21. package/dist/editor.js +6 -0
  22. package/dist/index.d.ts +1238 -0
  23. package/dist/index.js +124 -0
  24. package/editor/assets/TemplatesPane-5qsDAK_B.js +792 -0
  25. package/editor/assets/TemplatesPane-B4_sg2u5.css +1 -0
  26. package/editor/assets/abap-BrgZPUOV.js +6 -0
  27. package/editor/assets/apex-DyP6w7ZV.js +6 -0
  28. package/editor/assets/azcli-BaLxmfj-.js +6 -0
  29. package/editor/assets/bat-CFOPXBzS.js +6 -0
  30. package/editor/assets/bicep-BfEKNvv3.js +7 -0
  31. package/editor/assets/cameligo-BFG1Mk7z.js +6 -0
  32. package/editor/assets/clojure-DTECt2xU.js +6 -0
  33. package/editor/assets/codicon-DCmgc-ay.ttf +0 -0
  34. package/editor/assets/coffee-CDGzqUPQ.js +6 -0
  35. package/editor/assets/cpp-CLLBncYj.js +6 -0
  36. package/editor/assets/csharp-dUCx_-0o.js +6 -0
  37. package/editor/assets/csp-5Rap-vPy.js +6 -0
  38. package/editor/assets/css-D3h14YRZ.js +8 -0
  39. package/editor/assets/css.worker-DaIe3gwK.js +84 -0
  40. package/editor/assets/cssMode-CGp4MIjR.js +9 -0
  41. package/editor/assets/cypher-DrQuvNYM.js +6 -0
  42. package/editor/assets/dart-CFKIUWau.js +6 -0
  43. package/editor/assets/dockerfile-Zznr-cwX.js +6 -0
  44. package/editor/assets/ecl-Ce3n6wWz.js +6 -0
  45. package/editor/assets/editor.worker-BCzxt1at.js +12 -0
  46. package/editor/assets/elixir-deUWdS0T.js +6 -0
  47. package/editor/assets/flow9-i9-g7ZhI.js +6 -0
  48. package/editor/assets/freemarker2-CJkwxmPv.js +8 -0
  49. package/editor/assets/fsharp-CzKuDChf.js +6 -0
  50. package/editor/assets/go-Cphgjts3.js +6 -0
  51. package/editor/assets/graphql-Cg7bfA9N.js +6 -0
  52. package/editor/assets/handlebars-CKb5i2nM.js +6 -0
  53. package/editor/assets/hcl-0cvrggvQ.js +6 -0
  54. package/editor/assets/html-DyMbQx0w.js +6 -0
  55. package/editor/assets/html.worker-CKrFyw_2.js +461 -0
  56. package/editor/assets/htmlMode-DVPeqtn-.js +9 -0
  57. package/editor/assets/index-CbuWEnUB.css +7 -0
  58. package/editor/assets/index-DJmSgobK.js +129 -0
  59. package/editor/assets/ini-Drc7WvVn.js +6 -0
  60. package/editor/assets/java-B_fMsGYe.js +6 -0
  61. package/editor/assets/javascript-Bp1Qh9wR.js +6 -0
  62. package/editor/assets/json.worker-B7c_PmGb.js +49 -0
  63. package/editor/assets/jsonMode-FLEeVtx7.js +15 -0
  64. package/editor/assets/julia-Bqgm2twL.js +6 -0
  65. package/editor/assets/kotlin-BSkB5QuD.js +6 -0
  66. package/editor/assets/less-BsTHnhdd.js +7 -0
  67. package/editor/assets/lexon-YWi4-JPR.js +6 -0
  68. package/editor/assets/liquid-Bh8c534t.js +6 -0
  69. package/editor/assets/lua-nf6ki56Z.js +6 -0
  70. package/editor/assets/m3-Cpb6xl2v.js +6 -0
  71. package/editor/assets/markdown-DSZPf7rp.js +6 -0
  72. package/editor/assets/mdx-BUbo8M9l.js +6 -0
  73. package/editor/assets/mips-B_c3zf-v.js +6 -0
  74. package/editor/assets/msdax-rUNN04Wq.js +6 -0
  75. package/editor/assets/mysql-DDwshQtU.js +6 -0
  76. package/editor/assets/nanoesis-logo-CgieIWPg.png +0 -0
  77. package/editor/assets/objective-c-B5zXfXm9.js +6 -0
  78. package/editor/assets/pascal-CXOwvkN_.js +6 -0
  79. package/editor/assets/pascaligo-Bc-ZgV77.js +6 -0
  80. package/editor/assets/perl-CwNk8-XU.js +6 -0
  81. package/editor/assets/pgsql-tGk8EFnU.js +6 -0
  82. package/editor/assets/php-CpIb_Oan.js +6 -0
  83. package/editor/assets/pla-B03wrqEc.js +6 -0
  84. package/editor/assets/postiats-BKlk5iyT.js +6 -0
  85. package/editor/assets/powerquery-Bhzvs7bI.js +6 -0
  86. package/editor/assets/powershell-Dd3NCNK9.js +6 -0
  87. package/editor/assets/protobuf-COyEY5Pt.js +7 -0
  88. package/editor/assets/pug-BaJupSGV.js +6 -0
  89. package/editor/assets/python-CuJlk8g3.js +6 -0
  90. package/editor/assets/qsharp-DXyYeYxl.js +6 -0
  91. package/editor/assets/r-CdQndTaG.js +6 -0
  92. package/editor/assets/razor-CuQT_1Ku.js +6 -0
  93. package/editor/assets/redis-CVwtpugi.js +6 -0
  94. package/editor/assets/redshift-25W9uPmb.js +6 -0
  95. package/editor/assets/restructuredtext-DfzH4Xui.js +6 -0
  96. package/editor/assets/ruby-Cp1zYvxS.js +6 -0
  97. package/editor/assets/rust-D5C2fndG.js +6 -0
  98. package/editor/assets/sb-CDntyWJ8.js +6 -0
  99. package/editor/assets/scala-BoFRg7Ot.js +6 -0
  100. package/editor/assets/scheme-Bio4gycK.js +6 -0
  101. package/editor/assets/scss-4Ik7cdeQ.js +8 -0
  102. package/editor/assets/shell-CX-rkNHf.js +6 -0
  103. package/editor/assets/solidity-Tw7wswEv.js +6 -0
  104. package/editor/assets/sophia-C5WLch3f.js +6 -0
  105. package/editor/assets/sparql-DHaeiCBh.js +6 -0
  106. package/editor/assets/sql-CCSDG5nI.js +6 -0
  107. package/editor/assets/st-pnP8ivHi.js +6 -0
  108. package/editor/assets/swift-DwJ7jVG9.js +8 -0
  109. package/editor/assets/systemverilog-B9Xyijhd.js +6 -0
  110. package/editor/assets/tcl-DnHyzjbg.js +6 -0
  111. package/editor/assets/ts.worker-BhkL8olL.js +51334 -0
  112. package/editor/assets/tsMode-CT2HUNtN.js +16 -0
  113. package/editor/assets/twig-CPajHgWi.js +6 -0
  114. package/editor/assets/typescript-CtMx97cn.js +6 -0
  115. package/editor/assets/typespec-D-MeaMDU.js +6 -0
  116. package/editor/assets/vb-DgyLZaXg.js +6 -0
  117. package/editor/assets/wgsl-BIv9DU6q.js +303 -0
  118. package/editor/assets/xml-CyfpINj_.js +6 -0
  119. package/editor/assets/yaml-BBWmgfMA.js +6 -0
  120. package/editor/config.json +3 -0
  121. package/editor/index.html +28 -0
  122. package/package.json +85 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xtrable Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @xtrable-ltd/nanoesis
2
+
3
+ A static-first **publishing compiler**: author content in a calm editor, compile it to plain
4
+ static HTML at publish time, and ship files with no runtime, no server, and nothing of the
5
+ tooling surviving into production.
6
+
7
+ This one package contains everything an adopter needs (DESIGN §11c): the engine, the
8
+ mountable editor API, the storage / image / auth adapters, and the editor UI bundle, each
9
+ reachable as a subpath import.
10
+
11
+ ```bash
12
+ npm install @xtrable-ltd/nanoesis
13
+ ```
14
+
15
+ ## What you import
16
+
17
+ ```ts
18
+ import { compile, type BlobStore } from '@xtrable-ltd/nanoesis'; // the engine
19
+ import { handleApi, type ApiDeps } from '@xtrable-ltd/nanoesis/editor-api'; // mount in your host
20
+ import { FsBlobStore, FsArtifactSink } from '@xtrable-ltd/nanoesis/adapter-fs'; // a local-folder store
21
+ import { createSharpEncoder } from '@xtrable-ltd/nanoesis/adapter-sharp'; // responsive <picture>
22
+ import { ShellPreBuildHook } from '@xtrable-ltd/nanoesis/adapter-shell'; // pre-publish build hook
23
+ import { editorDist } from '@xtrable-ltd/nanoesis/editor'; // path to the SPA you serve
24
+ ```
25
+
26
+ Also available: `/adapter-azure-blob`, `/adapter-cloudflare`, `/adapter-local-jwt`,
27
+ `/adapter-trusted-header`.
28
+
29
+ ## The whole integration
30
+
31
+ A nanoesis host is two things: a **store** the editor reads and writes through (`get` /
32
+ `put` / `delete`), and a **publish** step that compiles the working files to a static site.
33
+ Mount the API on any Node server and serve the bundled editor:
34
+
35
+ ```ts
36
+ import { createServer } from 'node:http';
37
+ import { readFile } from 'node:fs/promises';
38
+ import { join } from 'node:path';
39
+
40
+ import { handleApi } from '@xtrable-ltd/nanoesis/editor-api';
41
+ import { IndexedStore, contentTypeFor, publishSite } from '@xtrable-ltd/nanoesis';
42
+ import { FsBlobStore, FsArtifactSink } from '@xtrable-ltd/nanoesis/adapter-fs';
43
+ import { createSharpEncoder } from '@xtrable-ltd/nanoesis/adapter-sharp';
44
+ import { editorDist } from '@xtrable-ltd/nanoesis/editor';
45
+
46
+ const store = new IndexedStore(new FsBlobStore('site')); // your content lives in ./site
47
+ const sink = new FsArtifactSink('published'); // the compiled static site lands in ./published
48
+
49
+ const deps = {
50
+ store,
51
+ identity: { authenticate: async () => ({ userId: 'local', username: 'local', roles: ['admin'] }) },
52
+ publish: async () => {
53
+ await sink.wipe();
54
+ return publishSite(store, sink, { imageEncoder: createSharpEncoder() });
55
+ },
56
+ };
57
+
58
+ createServer((req, res) => {
59
+ /* /api/* -> handleApi(deps, ...); everything else -> serve files from editorDist */
60
+ }).listen(7071);
61
+ ```
62
+
63
+ A complete, runnable version of this is in the repo's `examples/minimal-host`. For a real
64
+ deployment swap `FsBlobStore` for `/adapter-azure-blob` (or your own `get`/`put`/`delete`
65
+ store), and the local-admin stub for `/adapter-local-jwt` or `/adapter-trusted-header`.
66
+
67
+ ## How it ships
68
+
69
+ - **The editor** (`editorDist`) is a static SPA you serve from your host.
70
+ - **The published site** (whatever `publishSite` writes to your sink) is plain static files;
71
+ point any static host or CDN at them. Production runs no nanoesis code.
72
+
73
+ MIT © Xtrable Ltd.
@@ -0,0 +1,97 @@
1
+ import { ContainerClient } from '@azure/storage-blob';
2
+ import { BlobStore, ArtifactSink } from '@nanoesis/engine';
3
+ export { contentTypeFor } from '@nanoesis/engine';
4
+
5
+ /**
6
+ * The narrow blob-container seam the adapters depend on. It is the *only* surface the
7
+ * Azure SDK is wrapped behind ({@link AzureBlobContainer}), which keeps the SDK out of
8
+ * the adapters' logic and lets every path-mapping rule be unit-tested against the
9
+ * in-memory container below, no real or emulated blob store in the default suite
10
+ * (CLAUDE.md §5). Names are flat blob names (forward slashes, no leading slash).
11
+ */
12
+ interface BlobContainer {
13
+ /** Every blob name beginning with `prefix` ("" lists the whole container). */
14
+ list(prefix: string): Promise<readonly string[]>;
15
+ /** A blob's bytes, or null if it does not exist. */
16
+ read(name: string): Promise<Uint8Array | null>;
17
+ /** Create or overwrite a blob with the given bytes and content type. */
18
+ write(name: string, data: Uint8Array, contentType: string): Promise<void>;
19
+ /** Delete a blob; a missing blob is a no-op (idempotent). */
20
+ remove(name: string): Promise<void>;
21
+ }
22
+ /**
23
+ * An in-memory {@link BlobContainer} backed by a name→bytes map. The test double for
24
+ * the blob adapters, the LSP guarantee that the real Azure container behaves the same.
25
+ */
26
+ declare class InMemoryBlobContainer implements BlobContainer {
27
+ private readonly blobs;
28
+ constructor(seed?: Readonly<Record<string, Uint8Array | string>>);
29
+ list(prefix: string): Promise<readonly string[]>;
30
+ read(name: string): Promise<Uint8Array | null>;
31
+ write(name: string, data: Uint8Array): Promise<void>;
32
+ remove(name: string): Promise<void>;
33
+ /** Test helper: the current blob names. */
34
+ get names(): readonly string[];
35
+ }
36
+
37
+ /**
38
+ * The real {@link BlobContainer}, wrapping `@azure/storage-blob`. This is the *only*
39
+ * file in the adapter that touches the SDK (CLAUDE.md §3, concretes injected via a
40
+ * narrow seam); everything else is pure logic tested against {@link InMemoryBlobContainer}.
41
+ * It talks to a real Azure account or to **Azurite** locally, unchanged, the
42
+ * `UseDevelopmentStorage=true` connection string points at the emulator.
43
+ */
44
+ declare class AzureBlobContainer implements BlobContainer {
45
+ private readonly client;
46
+ constructor(client: ContainerClient);
47
+ /** Build from a connection string (e.g. `UseDevelopmentStorage=true` for Azurite). */
48
+ static fromConnectionString(connectionString: string, containerName: string): AzureBlobContainer;
49
+ /** Create the container if it does not exist (idempotent host start-up step). */
50
+ ensureContainer(): Promise<void>;
51
+ list(prefix: string): Promise<readonly string[]>;
52
+ read(name: string): Promise<Uint8Array | null>;
53
+ write(name: string, data: Uint8Array, contentType: string): Promise<void>;
54
+ remove(name: string): Promise<void>;
55
+ }
56
+
57
+ /**
58
+ * The engine's {@link BlobStore} (get/put/delete, DESIGN §11c) over a {@link BlobContainer}.
59
+ * This is the adopter's storage primitive the index-backed working store (and the publish
60
+ * sink) run on, and it works over the real {@link AzureBlobContainer} in production or the
61
+ * in-memory container in tests (LSP). Keys are POSIX-style; `put` stamps a content type
62
+ * from the key's extension so a published blob serves correctly as a static-website file.
63
+ */
64
+ declare class BlobContainerStore implements BlobStore {
65
+ private readonly container;
66
+ constructor(container: BlobContainer);
67
+ get(key: string): Promise<Uint8Array | undefined>;
68
+ put(key: string, bytes: Uint8Array): Promise<void>;
69
+ delete(key: string): Promise<void>;
70
+ }
71
+
72
+ /**
73
+ * The **published store** over an Azure Blob container (DESIGN §11, §11b, §11c): the
74
+ * publish pipeline's {@link ArtifactSink}. Publish only writes now, clearing the target
75
+ * is the adopter's job (DESIGN §11c), so {@link wipe} (delete every blob in the container)
76
+ * is the host's own pre-clear that it calls before a wipe-and-reupload republish. Each
77
+ * artifact is uploaded with a content type inferred from its path so the container can be
78
+ * served as a static website (Azurite's `$web`/static-site endpoint locally).
79
+ */
80
+ declare class BlobArtifactSink implements ArtifactSink {
81
+ private readonly container;
82
+ constructor(container: BlobContainer);
83
+ wipe(): Promise<void>;
84
+ write(path: string, contents: string | Uint8Array): Promise<void>;
85
+ }
86
+
87
+ /**
88
+ * Pure path/name helper for the blob adapters. Blob storage is a *flat* namespace, a blob
89
+ * is just a name like "content/blog/post.json", so the only mapping the SDK glue needs is
90
+ * to normalise a site-relative path into a blob name. Enumeration lives in the engine's
91
+ * content index (DESIGN §11d), not here. Keeping this pure means it is unit-tested without
92
+ * a real (or emulated) blob store; the SDK glue stays thin (CLAUDE.md §5).
93
+ */
94
+ /** Normalise a site path to a blob name: forward slashes, no leading/trailing slash. */
95
+ declare function normalizeBlobName(path: string): string;
96
+
97
+ export { AzureBlobContainer, BlobArtifactSink, type BlobContainer, BlobContainerStore, InMemoryBlobContainer, normalizeBlobName };
@@ -0,0 +1,127 @@
1
+ import {
2
+ contentTypeFor
3
+ } from "./chunk-G2UEZTYC.js";
4
+
5
+ // ../../adapters/azure-blob/src/container.ts
6
+ var InMemoryBlobContainer = class {
7
+ blobs = /* @__PURE__ */ new Map();
8
+ constructor(seed = {}) {
9
+ for (const [name, value] of Object.entries(seed)) {
10
+ this.blobs.set(name, typeof value === "string" ? new TextEncoder().encode(value) : value);
11
+ }
12
+ }
13
+ async list(prefix) {
14
+ const names = [];
15
+ for (const name of this.blobs.keys()) {
16
+ if (name.startsWith(prefix)) names.push(name);
17
+ }
18
+ return names.sort();
19
+ }
20
+ async read(name) {
21
+ return this.blobs.get(name) ?? null;
22
+ }
23
+ async write(name, data) {
24
+ this.blobs.set(name, data);
25
+ }
26
+ async remove(name) {
27
+ this.blobs.delete(name);
28
+ }
29
+ /** Test helper: the current blob names. */
30
+ get names() {
31
+ return [...this.blobs.keys()].sort();
32
+ }
33
+ };
34
+
35
+ // ../../adapters/azure-blob/src/azure-container.ts
36
+ import { BlobServiceClient } from "@azure/storage-blob";
37
+ function isNotFound(error) {
38
+ return typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404;
39
+ }
40
+ var AzureBlobContainer = class _AzureBlobContainer {
41
+ constructor(client) {
42
+ this.client = client;
43
+ }
44
+ client;
45
+ /** Build from a connection string (e.g. `UseDevelopmentStorage=true` for Azurite). */
46
+ static fromConnectionString(connectionString, containerName) {
47
+ const service = BlobServiceClient.fromConnectionString(connectionString);
48
+ return new _AzureBlobContainer(service.getContainerClient(containerName));
49
+ }
50
+ /** Create the container if it does not exist (idempotent host start-up step). */
51
+ async ensureContainer() {
52
+ await this.client.createIfNotExists();
53
+ }
54
+ async list(prefix) {
55
+ const names = [];
56
+ for await (const blob of this.client.listBlobsFlat({ prefix })) {
57
+ names.push(blob.name);
58
+ }
59
+ return names;
60
+ }
61
+ async read(name) {
62
+ try {
63
+ const buffer = await this.client.getBlockBlobClient(name).downloadToBuffer();
64
+ return new Uint8Array(buffer);
65
+ } catch (error) {
66
+ if (isNotFound(error)) return null;
67
+ throw error;
68
+ }
69
+ }
70
+ async write(name, data, contentType) {
71
+ await this.client.getBlockBlobClient(name).uploadData(Buffer.from(data), {
72
+ blobHTTPHeaders: { blobContentType: contentType }
73
+ });
74
+ }
75
+ async remove(name) {
76
+ await this.client.getBlockBlobClient(name).deleteIfExists();
77
+ }
78
+ };
79
+
80
+ // ../../adapters/azure-blob/src/paths.ts
81
+ function normalizeBlobName(path) {
82
+ return path.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
83
+ }
84
+
85
+ // ../../adapters/azure-blob/src/blob-container-store.ts
86
+ var BlobContainerStore = class {
87
+ constructor(container) {
88
+ this.container = container;
89
+ }
90
+ container;
91
+ async get(key) {
92
+ return await this.container.read(normalizeBlobName(key)) ?? void 0;
93
+ }
94
+ async put(key, bytes) {
95
+ const name = normalizeBlobName(key);
96
+ await this.container.write(name, bytes, contentTypeFor(name));
97
+ }
98
+ async delete(key) {
99
+ await this.container.remove(normalizeBlobName(key));
100
+ }
101
+ };
102
+
103
+ // ../../adapters/azure-blob/src/blob-sink.ts
104
+ var BlobArtifactSink = class {
105
+ constructor(container) {
106
+ this.container = container;
107
+ }
108
+ container;
109
+ async wipe() {
110
+ for (const name of await this.container.list("")) {
111
+ await this.container.remove(name);
112
+ }
113
+ }
114
+ async write(path, contents) {
115
+ const name = normalizeBlobName(path);
116
+ const data = typeof contents === "string" ? new TextEncoder().encode(contents) : contents;
117
+ await this.container.write(name, data, contentTypeFor(name));
118
+ }
119
+ };
120
+ export {
121
+ AzureBlobContainer,
122
+ BlobArtifactSink,
123
+ BlobContainerStore,
124
+ InMemoryBlobContainer,
125
+ contentTypeFor,
126
+ normalizeBlobName
127
+ };
@@ -0,0 +1,28 @@
1
+ import { PurgeService } from '@nanoesis/engine';
2
+
3
+ interface CloudflarePurgeConfig {
4
+ /** The Cloudflare zone ID fronting the published site. */
5
+ readonly zoneId: string;
6
+ /** A scoped API token with the "Cache Purge" permission for the zone. */
7
+ readonly apiToken: string;
8
+ /** Injected for tests; defaults to the global `fetch`. */
9
+ readonly fetch?: typeof fetch;
10
+ /** Override the API base (defaults to the public Cloudflare endpoint). */
11
+ readonly baseUrl?: string;
12
+ }
13
+ /**
14
+ * A {@link PurgeService} that invalidates an entire Cloudflare zone after a publish
15
+ * (DESIGN §11, Phase 1 purges everything, no per-path batching). It is a single HTTP
16
+ * call (`POST /zones/{zone}/purge_cache` with `purge_everything: true`), so it needs no
17
+ * SDK; `fetch` is injected for hermetic testing. The host wires this in production; the
18
+ * local example uses the engine's no-op purge instead (no Cloudflare account needed).
19
+ */
20
+ declare class CloudflarePurge implements PurgeService {
21
+ private readonly config;
22
+ private readonly fetchFn;
23
+ private readonly endpoint;
24
+ constructor(config: CloudflarePurgeConfig);
25
+ purgeAll(): Promise<void>;
26
+ }
27
+
28
+ export { CloudflarePurge, type CloudflarePurgeConfig };
@@ -0,0 +1,32 @@
1
+ // ../../adapters/cloudflare/src/cloudflare-purge.ts
2
+ var CloudflarePurge = class {
3
+ constructor(config) {
4
+ this.config = config;
5
+ this.fetchFn = config.fetch ?? fetch;
6
+ const base = config.baseUrl ?? "https://api.cloudflare.com/client/v4";
7
+ this.endpoint = `${base}/zones/${config.zoneId}/purge_cache`;
8
+ }
9
+ config;
10
+ fetchFn;
11
+ endpoint;
12
+ async purgeAll() {
13
+ const response = await this.fetchFn(this.endpoint, {
14
+ method: "POST",
15
+ headers: {
16
+ authorization: `Bearer ${this.config.apiToken}`,
17
+ "content-type": "application/json"
18
+ },
19
+ body: JSON.stringify({ purge_everything: true })
20
+ });
21
+ if (!response.ok) {
22
+ throw new Error(`Cloudflare purge failed: HTTP ${response.status} ${response.statusText}`);
23
+ }
24
+ const body = await response.json();
25
+ if (body.success !== true) {
26
+ throw new Error(`Cloudflare purge rejected: ${JSON.stringify(body.errors ?? body)}`);
27
+ }
28
+ }
29
+ };
30
+ export {
31
+ CloudflarePurge
32
+ };
@@ -0,0 +1,38 @@
1
+ import { BlobStore, ArtifactSink } from '@nanoesis/engine';
2
+
3
+ /**
4
+ * Filesystem implementation of the engine's {@link BlobStore} port (DESIGN §11c): get /
5
+ * put / delete over a local directory, the no-cloud storage primitive for local
6
+ * development. Keys are POSIX-style site-relative paths mapped onto files under `root`
7
+ * (e.g. "content/blog/post.json" → `<root>/content/blog/post.json`); the index-backed
8
+ * working store runs on top of this exactly as it does on the blob adapter (LSP).
9
+ */
10
+ declare class FsBlobStore implements BlobStore {
11
+ private readonly root;
12
+ constructor(root: string);
13
+ private resolve;
14
+ get(key: string): Promise<Uint8Array | undefined>;
15
+ put(key: string, bytes: Uint8Array): Promise<void>;
16
+ delete(key: string): Promise<void>;
17
+ }
18
+
19
+ /**
20
+ * Filesystem implementation of the engine's {@link ArtifactSink} port (DESIGN §3/§11c):
21
+ * the publish pipeline emits every compiled artifact through {@link write}, here onto a
22
+ * local output directory. It is the no-cloud counterpart of the blob sink, the same
23
+ * behaviour (LSP), used by `@nanoesis/host-mcp` to publish a site locally. Paths are
24
+ * POSIX-style and relative to `root` (e.g. "blog/first/index.html").
25
+ *
26
+ * Publish only writes (DESIGN §11c); clearing the target is the host's job, so {@link wipe}
27
+ * is offered for the host to call before a wipe-and-republish, not part of the port.
28
+ */
29
+ declare class FsArtifactSink implements ArtifactSink {
30
+ private readonly root;
31
+ constructor(root: string);
32
+ private resolve;
33
+ write(path: string, contents: string | Uint8Array): Promise<void>;
34
+ /** Empty the output directory before a republish. A missing directory is a no-op. */
35
+ wipe(): Promise<void>;
36
+ }
37
+
38
+ export { FsArtifactSink, FsBlobStore };
@@ -0,0 +1,54 @@
1
+ // ../../adapters/fs/src/fs-blob-store.ts
2
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
3
+ import { dirname, join } from "path";
4
+ var FsBlobStore = class {
5
+ constructor(root) {
6
+ this.root = root;
7
+ }
8
+ root;
9
+ resolve(key) {
10
+ return join(this.root, ...key.split("/").filter((segment) => segment !== ""));
11
+ }
12
+ async get(key) {
13
+ try {
14
+ return new Uint8Array(await readFile(this.resolve(key)));
15
+ } catch (error) {
16
+ if (error.code === "ENOENT") return void 0;
17
+ throw error;
18
+ }
19
+ }
20
+ async put(key, bytes) {
21
+ const dest = this.resolve(key);
22
+ await mkdir(dirname(dest), { recursive: true });
23
+ await writeFile(dest, bytes);
24
+ }
25
+ async delete(key) {
26
+ await rm(this.resolve(key), { force: true });
27
+ }
28
+ };
29
+
30
+ // ../../adapters/fs/src/fs-artifact-sink.ts
31
+ import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
32
+ import { dirname as dirname2, join as join2 } from "path";
33
+ var FsArtifactSink = class {
34
+ constructor(root) {
35
+ this.root = root;
36
+ }
37
+ root;
38
+ resolve(path) {
39
+ return join2(this.root, ...path.split("/").filter((segment) => segment !== ""));
40
+ }
41
+ async write(path, contents) {
42
+ const dest = this.resolve(path);
43
+ await mkdir2(dirname2(dest), { recursive: true });
44
+ await writeFile2(dest, contents);
45
+ }
46
+ /** Empty the output directory before a republish. A missing directory is a no-op. */
47
+ async wipe() {
48
+ await rm2(this.root, { recursive: true, force: true });
49
+ }
50
+ };
51
+ export {
52
+ FsArtifactSink,
53
+ FsBlobStore
54
+ };
@@ -0,0 +1,205 @@
1
+ import { Role, IdentityProvider, AuthEndpoints, UserAdminEndpoints } from '@nanoesis/engine';
2
+
3
+ /**
4
+ * One user record. Every field is owned by the adapter, a custom UserStore must
5
+ * round-trip them all. `loginCount` is the revocation counter (DESIGN §11): every
6
+ * login/logout bumps it, and a JWT carrying a stale count is rejected; one cheap
7
+ * UserStore read per request gets per-user revocation without a SessionStore.
8
+ *
9
+ * `failedAttempts` + `lockedUntil` drive the brute-force lockout (3 / 1 min per
10
+ * DESIGN §11); they live alongside the credential so a custom store doesn't have
11
+ * to ship its own attempt-tracking surface.
12
+ */
13
+ interface UserRecord {
14
+ /** Stable identifier (set once at create time, never reused). */
15
+ readonly id: string;
16
+ /** Login handle the user types. Case-insensitive lookups recommended at the store level. */
17
+ readonly username: string;
18
+ /**
19
+ * Optional display name (bylines, account menu). Absent means "use {@link username}".
20
+ * Stored trimmed; blank is normalised to absent by the provider.
21
+ */
22
+ readonly displayName?: string;
23
+ readonly roles: readonly Role[];
24
+ /** scrypt hash; opaque to consumers, produced by the adapter's `password` module. */
25
+ readonly passwordHash: string;
26
+ /** Revocation counter, bumped on every login and logout. */
27
+ readonly loginCount: number;
28
+ /** Consecutive failed login attempts since the last success. */
29
+ readonly failedAttempts: number;
30
+ /** Epoch ms; when set and in the future, the account is locked out. */
31
+ readonly lockedUntil?: number;
32
+ /**
33
+ * Personal-access-token revocation counter (DESIGN §14). PATs (the MCP HTTP transport)
34
+ * carry this value; bumping it revokes every PAT for the user, independent of the login
35
+ * session (so editor logins never invalidate a PAT). Absent is treated as 0.
36
+ */
37
+ readonly patVersion?: number;
38
+ /** Epoch ms; record creation time. */
39
+ readonly createdAt: number;
40
+ /** Epoch ms; last write time. */
41
+ readonly updatedAt: number;
42
+ }
43
+ /**
44
+ * The credential store the local-JWT provider talks to. A user supplies a concrete
45
+ * implementation (file-backed is shipped; bring your own for blob/cosmos/sql). The
46
+ * port is deliberately small, every method maps onto one auth operation.
47
+ */
48
+ interface UserStore {
49
+ /** True when no records exist; drives the first-run-becomes-admin path (DESIGN §11). */
50
+ isEmpty(): Promise<boolean>;
51
+ getByUsername(username: string): Promise<UserRecord | null>;
52
+ getById(id: string): Promise<UserRecord | null>;
53
+ /** Insert or replace by `id`. */
54
+ put(user: UserRecord): Promise<void>;
55
+ remove(id: string): Promise<void>;
56
+ /** All records, in deterministic order, for admin "list users" tooling. */
57
+ list(): Promise<readonly UserRecord[]>;
58
+ }
59
+ /**
60
+ * An ephemeral in-process {@link UserStore}. Useful for tests and ephemeral examples;
61
+ * never persistent. Lookups by username are case-insensitive.
62
+ */
63
+ declare class InMemoryUserStore implements UserStore {
64
+ private readonly byId;
65
+ isEmpty(): Promise<boolean>;
66
+ getByUsername(username: string): Promise<UserRecord | null>;
67
+ getById(id: string): Promise<UserRecord | null>;
68
+ put(user: UserRecord): Promise<void>;
69
+ remove(id: string): Promise<void>;
70
+ list(): Promise<readonly UserRecord[]>;
71
+ }
72
+ /**
73
+ * A JSON-file {@link UserStore} for the local example and small self-hosted deploys.
74
+ * The whole file is read and rewritten atomically (write to `.tmp`, rename) so a crash
75
+ * mid-write leaves the previous good file. Suitable up to "tens of users on one
76
+ * process"; production deploys with real persistence should bring their own
77
+ * implementation of {@link UserStore}.
78
+ *
79
+ * The file is `{"users": UserRecord[]}`, versionable JSON, hand-editable in a pinch.
80
+ */
81
+ declare class FileUserStore implements UserStore {
82
+ private readonly path;
83
+ constructor(path: string);
84
+ private load;
85
+ private save;
86
+ isEmpty(): Promise<boolean>;
87
+ getByUsername(username: string): Promise<UserRecord | null>;
88
+ getById(id: string): Promise<UserRecord | null>;
89
+ put(user: UserRecord): Promise<void>;
90
+ remove(id: string): Promise<void>;
91
+ list(): Promise<readonly UserRecord[]>;
92
+ }
93
+
94
+ /** Thrown when a password is rejected at hash time (before the work). */
95
+ declare class WeakPasswordError extends Error {
96
+ constructor(reason: string);
97
+ }
98
+ /**
99
+ * Hash a password with scrypt and return a self-describing string of the form
100
+ * `scrypt$1$<base64 salt>$<base64 hash>`. Re-derivable later by `verifyPassword`
101
+ * with no other state, every parameter the hash needs is encoded in the prefix.
102
+ *
103
+ * Rejects an empty password (a kindness to the caller; the routes layer is expected to
104
+ * also require a non-empty password on the way in). No length floor is imposed, the
105
+ * deployment owns its password policy.
106
+ */
107
+ declare function hashPassword(password: string): Promise<string>;
108
+ /**
109
+ * Constant-time check of `password` against an `encoded` hash produced by
110
+ * {@link hashPassword}. Returns false on every shape of mismatch, wrong password,
111
+ * truncated/garbage hash string, unknown version, so the caller never has to branch
112
+ * on "is this hash valid". Constant time matters here: it's the login hot path.
113
+ */
114
+ declare function verifyPassword(password: string, encoded: string): Promise<boolean>;
115
+
116
+ /**
117
+ * The JWT payload nanoesis issues. Compact on purpose, every byte is sent on every
118
+ * authenticated request. `lc` is the loginCount revocation counter (DESIGN §11),
119
+ * compared against the user record on `authenticate` and `refresh`.
120
+ */
121
+ interface JwtPayload {
122
+ /** Subject: the user record id. */
123
+ readonly sub: string;
124
+ /** Roles snapshot at issue time. */
125
+ readonly roles: readonly Role[];
126
+ /** loginCount when the token was issued, revocation handle for session tokens. */
127
+ readonly lc: number;
128
+ /** Issued-at epoch seconds. */
129
+ readonly iat: number;
130
+ /** Expiry epoch seconds. */
131
+ readonly exp: number;
132
+ /** Marks a personal access token (DESIGN §14): validated against `patVersion`, not `lc`. */
133
+ readonly pat?: true;
134
+ /** patVersion at issue time (PATs only); the revocation handle independent of loginCount. */
135
+ readonly pv?: number;
136
+ }
137
+ /** Distinct failure modes for `verifyJwt`, the caller maps them to HTTP status. */
138
+ type JwtVerifyError = 'malformed' | 'bad-signature' | 'expired';
139
+ type JwtVerifyResult = {
140
+ readonly ok: true;
141
+ readonly payload: JwtPayload;
142
+ } | {
143
+ readonly ok: false;
144
+ readonly reason: JwtVerifyError;
145
+ };
146
+ /**
147
+ * Sign a JWT (compact serialization, HS256). Standard layout
148
+ * `base64url(header).base64url(payload).base64url(HMAC-SHA256(header.payload))`, no
149
+ * external dep, just `node:crypto`. The secret is opaque bytes; pick at least 32 of
150
+ * them from a CSPRNG.
151
+ */
152
+ declare function signJwt(payload: JwtPayload, secret: string | Buffer): string;
153
+ /**
154
+ * Verify a JWT and return either the payload or a structured reason for the rejection.
155
+ * Never throws on bad input, a malformed token returns `{ ok: false, reason: 'malformed' }`,
156
+ * which matches how the rest of the host handles boundary failures (data, not exceptions).
157
+ *
158
+ * `nowSeconds` is injected for tests so we can verify the expiry boundary without
159
+ * sleeping, and so the rest of the adapter doesn't need a Clock port.
160
+ */
161
+ declare function verifyJwt(token: string, secret: string | Buffer, nowSeconds?: number): JwtVerifyResult;
162
+
163
+ /**
164
+ * Configuration for {@link createLocalJwtAuth}. The defaults match DESIGN §11, 10-min
165
+ * access tokens forcing a re-check of the loginCount, 3 failed attempts before a
166
+ * 1-minute lockout, and bumping loginCount on every login/logout so a previously-
167
+ * issued token dies the next time its owner logs in.
168
+ *
169
+ * `now` and `newId` are injected for tests so the lockout windows and id generation
170
+ * are deterministic; production should leave them at their defaults.
171
+ */
172
+ interface LocalJwtConfig {
173
+ readonly users: UserStore;
174
+ /** HMAC-SHA256 secret. At least 32 bytes of CSPRNG output. */
175
+ readonly signingSecret: string | Buffer;
176
+ /** Access-token TTL in seconds. Default 600 (10 min). */
177
+ readonly accessTokenTtlSeconds?: number;
178
+ /** Personal-access-token (PAT) TTL in seconds. Long by design, a PAT lives in a client
179
+ * config (e.g. an MCP client). Default 90 days. */
180
+ readonly patTtlSeconds?: number;
181
+ /**
182
+ * Refresh grace window in seconds. A token whose `exp` has passed is still accepted
183
+ * by `refresh()` as long as it's within this window AND signature + loginCount check
184
+ * out. This makes the access TTL a "must refresh" deadline rather than a "must
185
+ * re-login" one, a user who closes their laptop for the weekend still gets a
186
+ * silent refresh on return, while revocation (loginCount mismatch) and explicit
187
+ * logout still force a re-login. Default 30 days (60 * 60 * 24 * 30).
188
+ */
189
+ readonly refreshGraceSeconds?: number;
190
+ /** Failed attempts before a lockout kicks in. Default 3. */
191
+ readonly maxFailedAttempts?: number;
192
+ /** Lockout duration once the threshold trips. Default 60s. */
193
+ readonly lockoutSeconds?: number;
194
+ /** Injected for tests; defaults to `Date.now()`. */
195
+ readonly now?: () => number;
196
+ /** Injected for tests; defaults to `crypto.randomUUID()`. */
197
+ readonly newId?: () => string;
198
+ }
199
+ declare function createLocalJwtAuth(config: LocalJwtConfig): {
200
+ readonly identity: IdentityProvider;
201
+ readonly endpoints: AuthEndpoints;
202
+ readonly userAdmin: UserAdminEndpoints;
203
+ };
204
+
205
+ export { FileUserStore, InMemoryUserStore, type JwtPayload, type JwtVerifyError, type JwtVerifyResult, type LocalJwtConfig, type UserRecord, type UserStore, WeakPasswordError, createLocalJwtAuth, hashPassword, signJwt, verifyJwt, verifyPassword };