@thebes/cadmus 0.2.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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/astro/index.cjs +149 -0
  4. package/dist/astro/index.cjs.map +1 -0
  5. package/dist/astro/index.d.cts +101 -0
  6. package/dist/astro/index.d.cts.map +1 -0
  7. package/dist/astro/index.d.ts +101 -0
  8. package/dist/astro/index.d.ts.map +1 -0
  9. package/dist/astro/index.js +146 -0
  10. package/dist/astro/index.js.map +1 -0
  11. package/dist/auth/index.cjs +59 -0
  12. package/dist/auth/index.cjs.map +1 -0
  13. package/dist/auth/index.d.cts +14 -0
  14. package/dist/auth/index.d.cts.map +1 -0
  15. package/dist/auth/index.d.ts +14 -0
  16. package/dist/auth/index.d.ts.map +1 -0
  17. package/dist/auth/index.js +54 -0
  18. package/dist/auth/index.js.map +1 -0
  19. package/dist/cache/index.cjs +18 -0
  20. package/dist/cache/index.cjs.map +1 -0
  21. package/dist/cache/index.d.cts +10 -0
  22. package/dist/cache/index.d.cts.map +1 -0
  23. package/dist/cache/index.d.ts +10 -0
  24. package/dist/cache/index.d.ts.map +1 -0
  25. package/dist/cache/index.js +17 -0
  26. package/dist/cache/index.js.map +1 -0
  27. package/dist/cms/index.cjs +763 -0
  28. package/dist/cms/index.cjs.map +1 -0
  29. package/dist/cms/index.d.cts +2 -0
  30. package/dist/cms/index.d.ts +2 -0
  31. package/dist/cms/index.js +743 -0
  32. package/dist/cms/index.js.map +1 -0
  33. package/dist/db/index.cjs +10 -0
  34. package/dist/db/index.cjs.map +1 -0
  35. package/dist/db/index.d.cts +7 -0
  36. package/dist/db/index.d.cts.map +1 -0
  37. package/dist/db/index.d.ts +7 -0
  38. package/dist/db/index.d.ts.map +1 -0
  39. package/dist/db/index.js +9 -0
  40. package/dist/db/index.js.map +1 -0
  41. package/dist/email/index.cjs +25 -0
  42. package/dist/email/index.cjs.map +1 -0
  43. package/dist/email/index.d.cts +12 -0
  44. package/dist/email/index.d.cts.map +1 -0
  45. package/dist/email/index.d.ts +12 -0
  46. package/dist/email/index.d.ts.map +1 -0
  47. package/dist/email/index.js +24 -0
  48. package/dist/email/index.js.map +1 -0
  49. package/dist/errors-CW6Lz0AQ.cjs +196 -0
  50. package/dist/errors-CW6Lz0AQ.cjs.map +1 -0
  51. package/dist/errors-mZIqZJO4.js +125 -0
  52. package/dist/errors-mZIqZJO4.js.map +1 -0
  53. package/dist/hono/index.cjs +132 -0
  54. package/dist/hono/index.cjs.map +1 -0
  55. package/dist/hono/index.d.cts +59 -0
  56. package/dist/hono/index.d.cts.map +1 -0
  57. package/dist/hono/index.d.ts +59 -0
  58. package/dist/hono/index.d.ts.map +1 -0
  59. package/dist/hono/index.js +130 -0
  60. package/dist/hono/index.js.map +1 -0
  61. package/dist/index-BUrCSGVb.d.cts +616 -0
  62. package/dist/index-BUrCSGVb.d.cts.map +1 -0
  63. package/dist/index-BUrCSGVb.d.ts +616 -0
  64. package/dist/index-BUrCSGVb.d.ts.map +1 -0
  65. package/dist/index.cjs +60 -0
  66. package/dist/index.d.cts +107 -0
  67. package/dist/index.d.cts.map +1 -0
  68. package/dist/index.d.ts +107 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +11 -0
  71. package/dist/queues/index.cjs +31 -0
  72. package/dist/queues/index.cjs.map +1 -0
  73. package/dist/queues/index.d.cts +22 -0
  74. package/dist/queues/index.d.cts.map +1 -0
  75. package/dist/queues/index.d.ts +22 -0
  76. package/dist/queues/index.d.ts.map +1 -0
  77. package/dist/queues/index.js +29 -0
  78. package/dist/queues/index.js.map +1 -0
  79. package/dist/rate-limit/index.cjs +38 -0
  80. package/dist/rate-limit/index.cjs.map +1 -0
  81. package/dist/rate-limit/index.d.cts +14 -0
  82. package/dist/rate-limit/index.d.cts.map +1 -0
  83. package/dist/rate-limit/index.d.ts +14 -0
  84. package/dist/rate-limit/index.d.ts.map +1 -0
  85. package/dist/rate-limit/index.js +37 -0
  86. package/dist/rate-limit/index.js.map +1 -0
  87. package/dist/session/index.cjs +48 -0
  88. package/dist/session/index.cjs.map +1 -0
  89. package/dist/session/index.d.cts +14 -0
  90. package/dist/session/index.d.cts.map +1 -0
  91. package/dist/session/index.d.ts +14 -0
  92. package/dist/session/index.d.ts.map +1 -0
  93. package/dist/session/index.js +45 -0
  94. package/dist/session/index.js.map +1 -0
  95. package/dist/storage/index.cjs +29 -0
  96. package/dist/storage/index.cjs.map +1 -0
  97. package/dist/storage/index.d.cts +38 -0
  98. package/dist/storage/index.d.cts.map +1 -0
  99. package/dist/storage/index.d.ts +38 -0
  100. package/dist/storage/index.d.ts.map +1 -0
  101. package/dist/storage/index.js +26 -0
  102. package/dist/storage/index.js.map +1 -0
  103. package/package.json +115 -0
@@ -0,0 +1,125 @@
1
+ //#region src/errors.ts
2
+ /**
3
+ * Base class for all Cadmus errors.
4
+ * All primitives throw CadmusError or a typed subclass — never a raw Error.
5
+ *
6
+ * @example
7
+ * try {
8
+ * await createMagicLink({ kv, email, to })
9
+ * } catch (e) {
10
+ * if (e instanceof CadmusAuthError) {
11
+ * // auth-specific handling
12
+ * } else if (e instanceof CadmusError) {
13
+ * // any cadmus error — e.code tells you which primitive threw
14
+ * } else {
15
+ * throw e // re-throw unknown errors
16
+ * }
17
+ * }
18
+ */
19
+ var CadmusError = class extends Error {
20
+ code;
21
+ cause;
22
+ constructor(message, code, cause) {
23
+ super(message);
24
+ this.code = code;
25
+ this.cause = cause;
26
+ this.name = "CadmusError";
27
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
28
+ }
29
+ };
30
+ /** Thrown by @thebes/cadmus/auth primitives */
31
+ var CadmusAuthError = class extends CadmusError {
32
+ constructor(message, cause) {
33
+ super(message, "AUTH_ERROR", cause);
34
+ this.name = "CadmusAuthError";
35
+ }
36
+ };
37
+ /** Thrown by @thebes/cadmus/db primitives */
38
+ var CadmusDbError = class extends CadmusError {
39
+ constructor(message, cause) {
40
+ super(message, "DB_ERROR", cause);
41
+ this.name = "CadmusDbError";
42
+ }
43
+ };
44
+ /** Thrown by @thebes/cadmus/storage primitives */
45
+ var CadmusStorageError = class extends CadmusError {
46
+ constructor(message, cause) {
47
+ super(message, "STORAGE_ERROR", cause);
48
+ this.name = "CadmusStorageError";
49
+ }
50
+ };
51
+ /** Thrown by @thebes/cadmus/cache primitives */
52
+ var CadmusCacheError = class extends CadmusError {
53
+ constructor(message, cause) {
54
+ super(message, "CACHE_ERROR", cause);
55
+ this.name = "CadmusCacheError";
56
+ }
57
+ };
58
+ /** Thrown by @thebes/cadmus/email primitives */
59
+ var CadmusEmailError = class extends CadmusError {
60
+ constructor(message, cause) {
61
+ super(message, "EMAIL_ERROR", cause);
62
+ this.name = "CadmusEmailError";
63
+ }
64
+ };
65
+ /** Thrown by @thebes/cadmus/session primitives */
66
+ var CadmusSessionError = class extends CadmusError {
67
+ constructor(message, cause) {
68
+ super(message, "SESSION_ERROR", cause);
69
+ this.name = "CadmusSessionError";
70
+ }
71
+ };
72
+ /** Thrown by @thebes/cadmus/rate-limit primitives */
73
+ var CadmusRateLimitError = class extends CadmusError {
74
+ constructor(message, cause) {
75
+ super(message, "RATE_LIMIT_ERROR", cause);
76
+ this.name = "CadmusRateLimitError";
77
+ }
78
+ };
79
+ /** Thrown by @thebes/cadmus/queues primitives */
80
+ var CadmusQueueError = class extends CadmusError {
81
+ constructor(message, cause) {
82
+ super(message, "QUEUE_ERROR", cause);
83
+ this.name = "CadmusQueueError";
84
+ }
85
+ };
86
+ /** Thrown by @thebes/cadmus/cms primitives */
87
+ var CadmusCmsError = class extends CadmusError {
88
+ constructor(message, cause) {
89
+ super(message, "CMS_ERROR", cause);
90
+ this.name = "CadmusCmsError";
91
+ }
92
+ };
93
+ /**
94
+ * Thrown by @thebes/cadmus/cms's createLocalApi when a collection's
95
+ * `access` function rejects an operation. A distinct subclass (rather than
96
+ * a plain CadmusCmsError) so consumers like mountCmsRoutes can map it to
97
+ * 403 by `instanceof`, not by matching on message text.
98
+ */
99
+ var CadmusAccessDeniedError = class extends CadmusCmsError {
100
+ constructor(message, cause) {
101
+ super(message, cause);
102
+ this.name = "CadmusAccessDeniedError";
103
+ }
104
+ };
105
+ /**
106
+ * Thrown by @thebes/cadmus/hono's `createCmsApiClient` when a request
107
+ * against a `mountCmsRoutes` surface returns a non-2xx response. Carries
108
+ * the HTTP status and parsed body so callers can branch on `status`
109
+ * (e.g. 403 → access denied, 404 → not found) instead of re-parsing
110
+ * `{ error: string }` response bodies by hand.
111
+ */
112
+ var CadmusApiError = class extends CadmusError {
113
+ status;
114
+ body;
115
+ constructor(message, status, body) {
116
+ super(message, "API_ERROR");
117
+ this.status = status;
118
+ this.body = body;
119
+ this.name = "CadmusApiError";
120
+ }
121
+ };
122
+ //#endregion
123
+ export { CadmusCmsError as a, CadmusError as c, CadmusSessionError as d, CadmusStorageError as f, CadmusCacheError as i, CadmusQueueError as l, CadmusApiError as n, CadmusDbError as o, CadmusAuthError as r, CadmusEmailError as s, CadmusAccessDeniedError as t, CadmusRateLimitError as u };
124
+
125
+ //# sourceMappingURL=errors-mZIqZJO4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-mZIqZJO4.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// Cadmus is MIT licensed. See LICENSE in the repo root.\n\n// `Error.captureStackTrace` is a real V8 engine feature available in\n// workerd's V8 isolates — it's just not part of any spec, so it isn't in\n// TypeScript's standard lib types without pulling in @types/node, which\n// Cadmus deliberately doesn't (V8-first, no Node assumptions).\ndeclare global {\n interface ErrorConstructor {\n // biome-ignore lint/complexity/noBannedTypes: matches the real V8 signature — this.constructor is typed as Function by TS itself\n captureStackTrace?(targetObject: object, constructorOpt?: Function): void;\n }\n}\n\n/**\n * Base class for all Cadmus errors.\n * All primitives throw CadmusError or a typed subclass — never a raw Error.\n *\n * @example\n * try {\n * await createMagicLink({ kv, email, to })\n * } catch (e) {\n * if (e instanceof CadmusAuthError) {\n * // auth-specific handling\n * } else if (e instanceof CadmusError) {\n * // any cadmus error — e.code tells you which primitive threw\n * } else {\n * throw e // re-throw unknown errors\n * }\n * }\n */\nexport class CadmusError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly cause?: unknown,\n ) {\n super(message);\n this.name = \"CadmusError\";\n // Maintains proper stack trace in V8\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\n/** Thrown by @thebes/cadmus/auth primitives */\nexport class CadmusAuthError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"AUTH_ERROR\", cause);\n this.name = \"CadmusAuthError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/db primitives */\nexport class CadmusDbError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"DB_ERROR\", cause);\n this.name = \"CadmusDbError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/storage primitives */\nexport class CadmusStorageError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"STORAGE_ERROR\", cause);\n this.name = \"CadmusStorageError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/cache primitives */\nexport class CadmusCacheError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"CACHE_ERROR\", cause);\n this.name = \"CadmusCacheError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/email primitives */\nexport class CadmusEmailError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"EMAIL_ERROR\", cause);\n this.name = \"CadmusEmailError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/session primitives */\nexport class CadmusSessionError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"SESSION_ERROR\", cause);\n this.name = \"CadmusSessionError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/rate-limit primitives */\nexport class CadmusRateLimitError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"RATE_LIMIT_ERROR\", cause);\n this.name = \"CadmusRateLimitError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/queues primitives */\nexport class CadmusQueueError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"QUEUE_ERROR\", cause);\n this.name = \"CadmusQueueError\";\n }\n}\n\n/** Thrown by @thebes/cadmus/cms primitives */\nexport class CadmusCmsError extends CadmusError {\n constructor(message: string, cause?: unknown) {\n super(message, \"CMS_ERROR\", cause);\n this.name = \"CadmusCmsError\";\n }\n}\n\n/**\n * Thrown by @thebes/cadmus/cms's createLocalApi when a collection's\n * `access` function rejects an operation. A distinct subclass (rather than\n * a plain CadmusCmsError) so consumers like mountCmsRoutes can map it to\n * 403 by `instanceof`, not by matching on message text.\n */\nexport class CadmusAccessDeniedError extends CadmusCmsError {\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = \"CadmusAccessDeniedError\";\n }\n}\n\n/**\n * Thrown by @thebes/cadmus/hono's `createCmsApiClient` when a request\n * against a `mountCmsRoutes` surface returns a non-2xx response. Carries\n * the HTTP status and parsed body so callers can branch on `status`\n * (e.g. 403 → access denied, 404 → not found) instead of re-parsing\n * `{ error: string }` response bodies by hand.\n */\nexport class CadmusApiError extends CadmusError {\n constructor(\n message: string,\n public readonly status: number,\n public readonly body: unknown,\n ) {\n super(message, \"API_ERROR\");\n this.name = \"CadmusApiError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA+BA,IAAa,cAAb,cAAiC,MAAM;CAGnB;CACA;CAHlB,YACE,SACA,MACA,OACA;EACA,MAAM,OAAO;EAHG,KAAA,OAAA;EACA,KAAA,QAAA;EAGhB,KAAK,OAAO;EAEZ,IAAI,MAAM,mBACR,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAElD;AACF;;AAGA,IAAa,kBAAb,cAAqC,YAAY;CAC/C,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,cAAc,KAAK;EAClC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,gBAAb,cAAmC,YAAY;CAC7C,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,YAAY,KAAK;EAChC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,qBAAb,cAAwC,YAAY;CAClD,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,iBAAiB,KAAK;EACrC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,mBAAb,cAAsC,YAAY;CAChD,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,eAAe,KAAK;EACnC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,mBAAb,cAAsC,YAAY;CAChD,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,eAAe,KAAK;EACnC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,qBAAb,cAAwC,YAAY;CAClD,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,iBAAiB,KAAK;EACrC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,uBAAb,cAA0C,YAAY;CACpD,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,oBAAoB,KAAK;EACxC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,mBAAb,cAAsC,YAAY;CAChD,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,eAAe,KAAK;EACnC,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,iBAAb,cAAoC,YAAY;CAC9C,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,aAAa,KAAK;EACjC,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,IAAa,0BAAb,cAA6C,eAAe;CAC1D,YAAY,SAAiB,OAAiB;EAC5C,MAAM,SAAS,KAAK;EACpB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,IAAa,iBAAb,cAAoC,YAAY;CAG5B;CACA;CAHlB,YACE,SACA,QACA,MACA;EACA,MAAM,SAAS,WAAW;EAHV,KAAA,SAAA;EACA,KAAA,OAAA;EAGhB,KAAK,OAAO;CACd;AACF"}
@@ -0,0 +1,132 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors-CW6Lz0AQ.cjs");
3
+ let hono = require("hono");
4
+ //#region src/hono/client.ts
5
+ async function parseBody(response) {
6
+ const text = await response.text();
7
+ if (!text) return void 0;
8
+ try {
9
+ return JSON.parse(text);
10
+ } catch {
11
+ return text;
12
+ }
13
+ }
14
+ function errorMessage(body, status) {
15
+ if (body && typeof body === "object" && "error" in body) {
16
+ const error = body.error;
17
+ if (typeof error === "string") return error;
18
+ }
19
+ return `Request failed with status ${status}`;
20
+ }
21
+ /**
22
+ * The client-side counterpart to `mountCmsRoutes` — talks to exactly the
23
+ * REST surface that function mounts (`GET/POST/PATCH/DELETE
24
+ * /api/:collection[...]`), via plain `fetch()`. No Node APIs, works from
25
+ * any environment (browser, Worker, Astro SSR).
26
+ *
27
+ * This is for callers *outside* the Worker process that's actually running
28
+ * the CMS — an Astro island, an external operator's own client, anything
29
+ * that can't call a `LocalApi` in-process. In-process callers (the same
30
+ * Worker's own server functions, Cadmea's own Panel) should keep calling
31
+ * the `LocalApi`/Hono RPC (`hc<AppType>`) directly — this client adds a
32
+ * network hop neither of those needs.
33
+ */
34
+ function createCmsApiClient(baseUrl, options = {}) {
35
+ async function request(method, path, body) {
36
+ const headers = {};
37
+ const authHeader = await options.getAuthHeader?.();
38
+ if (authHeader) headers.Authorization = authHeader;
39
+ if (body !== void 0) headers["Content-Type"] = "application/json";
40
+ let response;
41
+ try {
42
+ response = await fetch(`${baseUrl}/api${path}`, {
43
+ method,
44
+ headers,
45
+ body: body !== void 0 ? JSON.stringify(body) : void 0
46
+ });
47
+ } catch (cause) {
48
+ throw new require_errors.CadmusApiError(`Request to "${baseUrl}/api${path}" failed`, 0, cause);
49
+ }
50
+ const parsed = await parseBody(response);
51
+ if (!response.ok) throw new require_errors.CadmusApiError(errorMessage(parsed, response.status), response.status, parsed);
52
+ return parsed;
53
+ }
54
+ return {
55
+ find(collection) {
56
+ return request("GET", `/${collection}`);
57
+ },
58
+ findByID(collection, id) {
59
+ return request("GET", `/${collection}/${id}`);
60
+ },
61
+ search(collection, query) {
62
+ return request("GET", `/${collection}/search?q=${encodeURIComponent(query)}`);
63
+ },
64
+ create(collection, data) {
65
+ return request("POST", `/${collection}`, data);
66
+ },
67
+ update(collection, id, data) {
68
+ return request("PATCH", `/${collection}/${id}`, data);
69
+ },
70
+ delete(collection, id) {
71
+ return request("DELETE", `/${collection}/${id}`);
72
+ }
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region src/hono/cms.ts
77
+ function statusForError(error) {
78
+ if (error instanceof require_errors.CadmusAccessDeniedError) return 403;
79
+ if (error.message.includes("document found with id")) return 404;
80
+ if (error.message.includes("Unique constraint violated")) return 409;
81
+ return 400;
82
+ }
83
+ function getApi(collections, slug) {
84
+ const api = collections[slug];
85
+ if (!api) throw new require_errors.CadmusCmsError(`Unknown collection "${slug}"`);
86
+ return api;
87
+ }
88
+ function mountCmsRoutes(app, options) {
89
+ const router = new hono.Hono();
90
+ router.onError((error, c) => {
91
+ if (error instanceof require_errors.CadmusCmsError) return c.json({ error: error.message }, statusForError(error));
92
+ throw error;
93
+ });
94
+ router.get("/:collection", async (c) => {
95
+ const api = getApi(options.collections, c.req.param("collection"));
96
+ const context = await options.resolveContext(c);
97
+ return c.json(await api.find(context));
98
+ });
99
+ router.get("/:collection/search", async (c) => {
100
+ const api = getApi(options.collections, c.req.param("collection"));
101
+ const context = await options.resolveContext(c);
102
+ return c.json(await api.search(context, c.req.query("q") ?? ""));
103
+ });
104
+ router.get("/:collection/:id", async (c) => {
105
+ const api = getApi(options.collections, c.req.param("collection"));
106
+ const context = await options.resolveContext(c);
107
+ return c.json(await api.findByID(context, Number(c.req.param("id"))));
108
+ });
109
+ router.post("/:collection", async (c) => {
110
+ const api = getApi(options.collections, c.req.param("collection"));
111
+ const context = await options.resolveContext(c);
112
+ return c.json(await api.create(context, await c.req.json()), 201);
113
+ });
114
+ router.patch("/:collection/:id", async (c) => {
115
+ const api = getApi(options.collections, c.req.param("collection"));
116
+ const context = await options.resolveContext(c);
117
+ const id = Number(c.req.param("id"));
118
+ return c.json(await api.update(context, id, await c.req.json()));
119
+ });
120
+ router.delete("/:collection/:id", async (c) => {
121
+ const api = getApi(options.collections, c.req.param("collection"));
122
+ const context = await options.resolveContext(c);
123
+ return c.json(await api.deleteByID(context, Number(c.req.param("id"))));
124
+ });
125
+ app.route("/api", router);
126
+ return app;
127
+ }
128
+ //#endregion
129
+ exports.createCmsApiClient = createCmsApiClient;
130
+ exports.mountCmsRoutes = mountCmsRoutes;
131
+
132
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["CadmusApiError","CadmusAccessDeniedError","CadmusCmsError","Hono"],"sources":["../../src/hono/client.ts","../../src/hono/cms.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// Cadmus is MIT licensed. See LICENSE in the repo root.\n\nimport { CadmusApiError } from \"../errors.js\";\n\nexport interface CmsApiClientOptions {\n /**\n * Returns the value sent verbatim as the request's `Authorization`\n * header, or `undefined`/`\"\"` to send no `Authorization` header at all.\n * This is the client's *only* auth surface — it never generates, stores,\n * refreshes, or validates a token itself. A bearer token, an OAuth2\n * access token obtained elsewhere, a shared service key — all the\n * caller's problem. No OAuth flow lives here; see EXTENDING.md's\n * provider-interface note if a real OAuth client flow is ever needed —\n * that's separate scope, not an extension of this option.\n */\n getAuthHeader?: () => string | undefined | Promise<string | undefined>;\n}\n\nasync function parseBody(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction errorMessage(body: unknown, status: number): string {\n if (body && typeof body === \"object\" && \"error\" in body) {\n const error = (body as { error: unknown }).error;\n if (typeof error === \"string\") return error;\n }\n return `Request failed with status ${status}`;\n}\n\n/**\n * The client-side counterpart to `mountCmsRoutes` — talks to exactly the\n * REST surface that function mounts (`GET/POST/PATCH/DELETE\n * /api/:collection[...]`), via plain `fetch()`. No Node APIs, works from\n * any environment (browser, Worker, Astro SSR).\n *\n * This is for callers *outside* the Worker process that's actually running\n * the CMS — an Astro island, an external operator's own client, anything\n * that can't call a `LocalApi` in-process. In-process callers (the same\n * Worker's own server functions, Cadmea's own Panel) should keep calling\n * the `LocalApi`/Hono RPC (`hc<AppType>`) directly — this client adds a\n * network hop neither of those needs.\n */\nexport function createCmsApiClient(\n baseUrl: string,\n options: CmsApiClientOptions = {},\n) {\n async function request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<unknown> {\n const headers: Record<string, string> = {};\n const authHeader = await options.getAuthHeader?.();\n if (authHeader) headers.Authorization = authHeader;\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/api${path}`, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n } catch (cause) {\n throw new CadmusApiError(\n `Request to \"${baseUrl}/api${path}\" failed`,\n 0,\n cause,\n );\n }\n\n const parsed = await parseBody(response);\n if (!response.ok) {\n throw new CadmusApiError(\n errorMessage(parsed, response.status),\n response.status,\n parsed,\n );\n }\n return parsed;\n }\n\n return {\n find(collection: string): Promise<unknown[]> {\n return request(\"GET\", `/${collection}`) as Promise<unknown[]>;\n },\n findByID(collection: string, id: number): Promise<unknown> {\n return request(\"GET\", `/${collection}/${id}`);\n },\n search(collection: string, query: string): Promise<unknown[]> {\n return request(\n \"GET\",\n `/${collection}/search?q=${encodeURIComponent(query)}`,\n ) as Promise<unknown[]>;\n },\n create(\n collection: string,\n data: Record<string, unknown>,\n ): Promise<unknown> {\n return request(\"POST\", `/${collection}`, data);\n },\n update(\n collection: string,\n id: number,\n data: Record<string, unknown>,\n ): Promise<unknown> {\n return request(\"PATCH\", `/${collection}/${id}`, data);\n },\n delete(collection: string, id: number): Promise<unknown> {\n return request(\"DELETE\", `/${collection}/${id}`);\n },\n };\n}\n\nexport type CmsApiClient = ReturnType<typeof createCmsApiClient>;\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// Cadmus is MIT licensed. See LICENSE in the repo root.\n\nimport type { Context } from \"hono\";\nimport { Hono } from \"hono\";\nimport type { ClientErrorStatusCode } from \"hono/utils/http-status\";\nimport type { LocalApi } from \"../cms/index.js\";\nimport { CadmusAccessDeniedError, CadmusCmsError } from \"../errors.js\";\n\nexport interface CmsRoutesOptions<TContext> {\n // biome-ignore lint/suspicious/noExplicitAny: see above\n collections: Record<string, LocalApi<any, TContext>>;\n /**\n * Resolves the per-request access context passed as the first argument\n * to every Local API call below — called once per request, not once per\n * collection method, so e.g. a session lookup only happens once even\n * though a write touches `create` and its `afterChange` hooks. Cadmus\n * doesn't standardize the context shape (see LocalApi's `TContext`) —\n * the caller's `resolveContext` is the one place that decides it, the\n * same way Cadmea's server functions each call `requireAuthOrThrow()`\n * themselves today.\n */\n resolveContext: (c: Context) => Promise<TContext>;\n}\n\n// Coupled to the exact message strings localApi.ts's notFound() and\n// wrapWriteError() author — both files are Cadmus-internal, so this is\n// matching a contract we control, not arbitrary third-party text. The\n// honest long-term fix is a status/discriminated-code field on\n// CadmusCmsError; flagged as a follow-up, not built here (it would\n// ripple across every existing primitive error).\nfunction statusForError(error: CadmusCmsError): ClientErrorStatusCode {\n if (error instanceof CadmusAccessDeniedError) return 403;\n if (error.message.includes(\"document found with id\")) return 404;\n if (error.message.includes(\"Unique constraint violated\")) return 409;\n return 400;\n}\n\nfunction getApi<TContext>(\n collections: CmsRoutesOptions<TContext>[\"collections\"],\n slug: string,\n // biome-ignore lint/suspicious/noExplicitAny: see CmsRoutesOptions\n): LocalApi<any, TContext> {\n const api = collections[slug];\n if (!api) throw new CadmusCmsError(`Unknown collection \"${slug}\"`);\n return api;\n}\n\n// Mounts a Payload-equivalent REST surface at /api:\n// GET /api/:collection\n// GET /api/:collection/search?q=...\n// GET /api/:collection/:id\n// POST /api/:collection\n// PATCH /api/:collection/:id\n// DELETE /api/:collection/:id\nexport function mountCmsRoutes<TContext>(\n app: Hono,\n options: CmsRoutesOptions<TContext>,\n): Hono {\n const router = new Hono();\n\n router.onError((error, c) => {\n if (error instanceof CadmusCmsError) {\n return c.json({ error: error.message }, statusForError(error));\n }\n throw error;\n });\n\n // `resolveContext` runs once per request, before any Local API call —\n // every route below shares the one resolved context across its method\n // call and that method's own hooks (e.g. create()'s afterChange).\n router.get(\"/:collection\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.find(context));\n });\n\n // Registered before \"/:collection/:id\" — Hono's router prioritizes a\n // static path segment (\"search\") over a dynamic one (\":id\") regardless\n // of registration order, but the ordering here documents the intent\n // either way: a request for /api/pages/search must never be parsed as\n // findByID with id=\"search\".\n router.get(\"/:collection/search\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.search(context, c.req.query(\"q\") ?? \"\"));\n });\n\n router.get(\"/:collection/:id\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.findByID(context, Number(c.req.param(\"id\"))));\n });\n\n router.post(\"/:collection\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.create(context, await c.req.json()), 201);\n });\n\n router.patch(\"/:collection/:id\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n const id = Number(c.req.param(\"id\"));\n return c.json(await api.update(context, id, await c.req.json()));\n });\n\n router.delete(\"/:collection/:id\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.deleteByID(context, Number(c.req.param(\"id\"))));\n });\n\n app.route(\"/api\", router);\n return app;\n}\n"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import { l as LocalApi } from "../index-BUrCSGVb.cjs";
2
+ import { Context, Hono } from "hono";
3
+
4
+ //#region src/hono/client.d.ts
5
+ interface CmsApiClientOptions {
6
+ /**
7
+ * Returns the value sent verbatim as the request's `Authorization`
8
+ * header, or `undefined`/`""` to send no `Authorization` header at all.
9
+ * This is the client's *only* auth surface — it never generates, stores,
10
+ * refreshes, or validates a token itself. A bearer token, an OAuth2
11
+ * access token obtained elsewhere, a shared service key — all the
12
+ * caller's problem. No OAuth flow lives here; see EXTENDING.md's
13
+ * provider-interface note if a real OAuth client flow is ever needed —
14
+ * that's separate scope, not an extension of this option.
15
+ */
16
+ getAuthHeader?: () => string | undefined | Promise<string | undefined>;
17
+ }
18
+ /**
19
+ * The client-side counterpart to `mountCmsRoutes` — talks to exactly the
20
+ * REST surface that function mounts (`GET/POST/PATCH/DELETE
21
+ * /api/:collection[...]`), via plain `fetch()`. No Node APIs, works from
22
+ * any environment (browser, Worker, Astro SSR).
23
+ *
24
+ * This is for callers *outside* the Worker process that's actually running
25
+ * the CMS — an Astro island, an external operator's own client, anything
26
+ * that can't call a `LocalApi` in-process. In-process callers (the same
27
+ * Worker's own server functions, Cadmea's own Panel) should keep calling
28
+ * the `LocalApi`/Hono RPC (`hc<AppType>`) directly — this client adds a
29
+ * network hop neither of those needs.
30
+ */
31
+ declare function createCmsApiClient(baseUrl: string, options?: CmsApiClientOptions): {
32
+ find(collection: string): Promise<unknown[]>;
33
+ findByID(collection: string, id: number): Promise<unknown>;
34
+ search(collection: string, query: string): Promise<unknown[]>;
35
+ create(collection: string, data: Record<string, unknown>): Promise<unknown>;
36
+ update(collection: string, id: number, data: Record<string, unknown>): Promise<unknown>;
37
+ delete(collection: string, id: number): Promise<unknown>;
38
+ };
39
+ type CmsApiClient = ReturnType<typeof createCmsApiClient>;
40
+ //#endregion
41
+ //#region src/hono/cms.d.ts
42
+ interface CmsRoutesOptions<TContext> {
43
+ collections: Record<string, LocalApi<any, TContext>>;
44
+ /**
45
+ * Resolves the per-request access context passed as the first argument
46
+ * to every Local API call below — called once per request, not once per
47
+ * collection method, so e.g. a session lookup only happens once even
48
+ * though a write touches `create` and its `afterChange` hooks. Cadmus
49
+ * doesn't standardize the context shape (see LocalApi's `TContext`) —
50
+ * the caller's `resolveContext` is the one place that decides it, the
51
+ * same way Cadmea's server functions each call `requireAuthOrThrow()`
52
+ * themselves today.
53
+ */
54
+ resolveContext: (c: Context) => Promise<TContext>;
55
+ }
56
+ declare function mountCmsRoutes<TContext>(app: Hono, options: CmsRoutesOptions<TContext>): Hono;
57
+ //#endregion
58
+ export { CmsApiClient, CmsApiClientOptions, CmsRoutesOptions, createCmsApiClient, mountCmsRoutes };
59
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/hono/client.ts","../../src/hono/cms.ts"],"mappings":";;;;UAKiB,mBAAA;;;;AAAjB;;;;AAWoD;AAkCpD;;EAlCE,aAAA,8BAA2C,OAAO;AAAA;;;;;;;;;;;;;;iBAkCpC,kBAAA,CACd,OAAA,UACA,OAAA,GAAS,mBAAA;4BAuCmB,OAAA;+BAGC,EAAA,WAAe,OAAA;6BAGjB,KAAA,WAAkB,OAAA;6BAOvB,IAAA,EACZ,MAAA,oBACL,OAAA;6BAIiB,EAAA,UACR,IAAA,EACJ,MAAA,oBACL,OAAA;6BAGsB,EAAA,WAAe,OAAA;AAAA;AAAA,KAMhC,YAAA,GAAe,UAAU,QAAQ,kBAAA;;;UCjH5B,gBAAA;EAEf,WAAA,EAAa,MAAA,SAAe,QAAA,MAAc,QAAA;EDNR;;;AAWgB;AAkCpD;;;;;;EC5BE,cAAA,GAAiB,CAAA,EAAG,OAAA,KAAY,OAAA,CAAQ,QAAA;AAAA;AAAA,iBAiC1B,cAAA,WACd,GAAA,EAAK,IAAA,EACL,OAAA,EAAS,gBAAA,CAAiB,QAAA,IACzB,IAAA"}
@@ -0,0 +1,59 @@
1
+ import { l as LocalApi } from "../index-BUrCSGVb.js";
2
+ import { Context, Hono } from "hono";
3
+
4
+ //#region src/hono/client.d.ts
5
+ interface CmsApiClientOptions {
6
+ /**
7
+ * Returns the value sent verbatim as the request's `Authorization`
8
+ * header, or `undefined`/`""` to send no `Authorization` header at all.
9
+ * This is the client's *only* auth surface — it never generates, stores,
10
+ * refreshes, or validates a token itself. A bearer token, an OAuth2
11
+ * access token obtained elsewhere, a shared service key — all the
12
+ * caller's problem. No OAuth flow lives here; see EXTENDING.md's
13
+ * provider-interface note if a real OAuth client flow is ever needed —
14
+ * that's separate scope, not an extension of this option.
15
+ */
16
+ getAuthHeader?: () => string | undefined | Promise<string | undefined>;
17
+ }
18
+ /**
19
+ * The client-side counterpart to `mountCmsRoutes` — talks to exactly the
20
+ * REST surface that function mounts (`GET/POST/PATCH/DELETE
21
+ * /api/:collection[...]`), via plain `fetch()`. No Node APIs, works from
22
+ * any environment (browser, Worker, Astro SSR).
23
+ *
24
+ * This is for callers *outside* the Worker process that's actually running
25
+ * the CMS — an Astro island, an external operator's own client, anything
26
+ * that can't call a `LocalApi` in-process. In-process callers (the same
27
+ * Worker's own server functions, Cadmea's own Panel) should keep calling
28
+ * the `LocalApi`/Hono RPC (`hc<AppType>`) directly — this client adds a
29
+ * network hop neither of those needs.
30
+ */
31
+ declare function createCmsApiClient(baseUrl: string, options?: CmsApiClientOptions): {
32
+ find(collection: string): Promise<unknown[]>;
33
+ findByID(collection: string, id: number): Promise<unknown>;
34
+ search(collection: string, query: string): Promise<unknown[]>;
35
+ create(collection: string, data: Record<string, unknown>): Promise<unknown>;
36
+ update(collection: string, id: number, data: Record<string, unknown>): Promise<unknown>;
37
+ delete(collection: string, id: number): Promise<unknown>;
38
+ };
39
+ type CmsApiClient = ReturnType<typeof createCmsApiClient>;
40
+ //#endregion
41
+ //#region src/hono/cms.d.ts
42
+ interface CmsRoutesOptions<TContext> {
43
+ collections: Record<string, LocalApi<any, TContext>>;
44
+ /**
45
+ * Resolves the per-request access context passed as the first argument
46
+ * to every Local API call below — called once per request, not once per
47
+ * collection method, so e.g. a session lookup only happens once even
48
+ * though a write touches `create` and its `afterChange` hooks. Cadmus
49
+ * doesn't standardize the context shape (see LocalApi's `TContext`) —
50
+ * the caller's `resolveContext` is the one place that decides it, the
51
+ * same way Cadmea's server functions each call `requireAuthOrThrow()`
52
+ * themselves today.
53
+ */
54
+ resolveContext: (c: Context) => Promise<TContext>;
55
+ }
56
+ declare function mountCmsRoutes<TContext>(app: Hono, options: CmsRoutesOptions<TContext>): Hono;
57
+ //#endregion
58
+ export { CmsApiClient, CmsApiClientOptions, CmsRoutesOptions, createCmsApiClient, mountCmsRoutes };
59
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/hono/client.ts","../../src/hono/cms.ts"],"mappings":";;;;UAKiB,mBAAA;;;;AAAjB;;;;AAWoD;AAkCpD;;EAlCE,aAAA,8BAA2C,OAAO;AAAA;;;;;;;;;;;;;;iBAkCpC,kBAAA,CACd,OAAA,UACA,OAAA,GAAS,mBAAA;4BAuCmB,OAAA;+BAGC,EAAA,WAAe,OAAA;6BAGjB,KAAA,WAAkB,OAAA;6BAOvB,IAAA,EACZ,MAAA,oBACL,OAAA;6BAIiB,EAAA,UACR,IAAA,EACJ,MAAA,oBACL,OAAA;6BAGsB,EAAA,WAAe,OAAA;AAAA;AAAA,KAMhC,YAAA,GAAe,UAAU,QAAQ,kBAAA;;;UCjH5B,gBAAA;EAEf,WAAA,EAAa,MAAA,SAAe,QAAA,MAAc,QAAA;EDNR;;;AAWgB;AAkCpD;;;;;;EC5BE,cAAA,GAAiB,CAAA,EAAG,OAAA,KAAY,OAAA,CAAQ,QAAA;AAAA;AAAA,iBAiC1B,cAAA,WACd,GAAA,EAAK,IAAA,EACL,OAAA,EAAS,gBAAA,CAAiB,QAAA,IACzB,IAAA"}
@@ -0,0 +1,130 @@
1
+ import { a as CadmusCmsError, n as CadmusApiError, t as CadmusAccessDeniedError } from "../errors-mZIqZJO4.js";
2
+ import { Hono } from "hono";
3
+ //#region src/hono/client.ts
4
+ async function parseBody(response) {
5
+ const text = await response.text();
6
+ if (!text) return void 0;
7
+ try {
8
+ return JSON.parse(text);
9
+ } catch {
10
+ return text;
11
+ }
12
+ }
13
+ function errorMessage(body, status) {
14
+ if (body && typeof body === "object" && "error" in body) {
15
+ const error = body.error;
16
+ if (typeof error === "string") return error;
17
+ }
18
+ return `Request failed with status ${status}`;
19
+ }
20
+ /**
21
+ * The client-side counterpart to `mountCmsRoutes` — talks to exactly the
22
+ * REST surface that function mounts (`GET/POST/PATCH/DELETE
23
+ * /api/:collection[...]`), via plain `fetch()`. No Node APIs, works from
24
+ * any environment (browser, Worker, Astro SSR).
25
+ *
26
+ * This is for callers *outside* the Worker process that's actually running
27
+ * the CMS — an Astro island, an external operator's own client, anything
28
+ * that can't call a `LocalApi` in-process. In-process callers (the same
29
+ * Worker's own server functions, Cadmea's own Panel) should keep calling
30
+ * the `LocalApi`/Hono RPC (`hc<AppType>`) directly — this client adds a
31
+ * network hop neither of those needs.
32
+ */
33
+ function createCmsApiClient(baseUrl, options = {}) {
34
+ async function request(method, path, body) {
35
+ const headers = {};
36
+ const authHeader = await options.getAuthHeader?.();
37
+ if (authHeader) headers.Authorization = authHeader;
38
+ if (body !== void 0) headers["Content-Type"] = "application/json";
39
+ let response;
40
+ try {
41
+ response = await fetch(`${baseUrl}/api${path}`, {
42
+ method,
43
+ headers,
44
+ body: body !== void 0 ? JSON.stringify(body) : void 0
45
+ });
46
+ } catch (cause) {
47
+ throw new CadmusApiError(`Request to "${baseUrl}/api${path}" failed`, 0, cause);
48
+ }
49
+ const parsed = await parseBody(response);
50
+ if (!response.ok) throw new CadmusApiError(errorMessage(parsed, response.status), response.status, parsed);
51
+ return parsed;
52
+ }
53
+ return {
54
+ find(collection) {
55
+ return request("GET", `/${collection}`);
56
+ },
57
+ findByID(collection, id) {
58
+ return request("GET", `/${collection}/${id}`);
59
+ },
60
+ search(collection, query) {
61
+ return request("GET", `/${collection}/search?q=${encodeURIComponent(query)}`);
62
+ },
63
+ create(collection, data) {
64
+ return request("POST", `/${collection}`, data);
65
+ },
66
+ update(collection, id, data) {
67
+ return request("PATCH", `/${collection}/${id}`, data);
68
+ },
69
+ delete(collection, id) {
70
+ return request("DELETE", `/${collection}/${id}`);
71
+ }
72
+ };
73
+ }
74
+ //#endregion
75
+ //#region src/hono/cms.ts
76
+ function statusForError(error) {
77
+ if (error instanceof CadmusAccessDeniedError) return 403;
78
+ if (error.message.includes("document found with id")) return 404;
79
+ if (error.message.includes("Unique constraint violated")) return 409;
80
+ return 400;
81
+ }
82
+ function getApi(collections, slug) {
83
+ const api = collections[slug];
84
+ if (!api) throw new CadmusCmsError(`Unknown collection "${slug}"`);
85
+ return api;
86
+ }
87
+ function mountCmsRoutes(app, options) {
88
+ const router = new Hono();
89
+ router.onError((error, c) => {
90
+ if (error instanceof CadmusCmsError) return c.json({ error: error.message }, statusForError(error));
91
+ throw error;
92
+ });
93
+ router.get("/:collection", async (c) => {
94
+ const api = getApi(options.collections, c.req.param("collection"));
95
+ const context = await options.resolveContext(c);
96
+ return c.json(await api.find(context));
97
+ });
98
+ router.get("/:collection/search", async (c) => {
99
+ const api = getApi(options.collections, c.req.param("collection"));
100
+ const context = await options.resolveContext(c);
101
+ return c.json(await api.search(context, c.req.query("q") ?? ""));
102
+ });
103
+ router.get("/:collection/:id", async (c) => {
104
+ const api = getApi(options.collections, c.req.param("collection"));
105
+ const context = await options.resolveContext(c);
106
+ return c.json(await api.findByID(context, Number(c.req.param("id"))));
107
+ });
108
+ router.post("/:collection", async (c) => {
109
+ const api = getApi(options.collections, c.req.param("collection"));
110
+ const context = await options.resolveContext(c);
111
+ return c.json(await api.create(context, await c.req.json()), 201);
112
+ });
113
+ router.patch("/:collection/:id", async (c) => {
114
+ const api = getApi(options.collections, c.req.param("collection"));
115
+ const context = await options.resolveContext(c);
116
+ const id = Number(c.req.param("id"));
117
+ return c.json(await api.update(context, id, await c.req.json()));
118
+ });
119
+ router.delete("/:collection/:id", async (c) => {
120
+ const api = getApi(options.collections, c.req.param("collection"));
121
+ const context = await options.resolveContext(c);
122
+ return c.json(await api.deleteByID(context, Number(c.req.param("id"))));
123
+ });
124
+ app.route("/api", router);
125
+ return app;
126
+ }
127
+ //#endregion
128
+ export { createCmsApiClient, mountCmsRoutes };
129
+
130
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/hono/client.ts","../../src/hono/cms.ts"],"sourcesContent":["// Copyright (c) 2026 BowenLabs. All rights reserved.\n// Cadmus is MIT licensed. See LICENSE in the repo root.\n\nimport { CadmusApiError } from \"../errors.js\";\n\nexport interface CmsApiClientOptions {\n /**\n * Returns the value sent verbatim as the request's `Authorization`\n * header, or `undefined`/`\"\"` to send no `Authorization` header at all.\n * This is the client's *only* auth surface — it never generates, stores,\n * refreshes, or validates a token itself. A bearer token, an OAuth2\n * access token obtained elsewhere, a shared service key — all the\n * caller's problem. No OAuth flow lives here; see EXTENDING.md's\n * provider-interface note if a real OAuth client flow is ever needed —\n * that's separate scope, not an extension of this option.\n */\n getAuthHeader?: () => string | undefined | Promise<string | undefined>;\n}\n\nasync function parseBody(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction errorMessage(body: unknown, status: number): string {\n if (body && typeof body === \"object\" && \"error\" in body) {\n const error = (body as { error: unknown }).error;\n if (typeof error === \"string\") return error;\n }\n return `Request failed with status ${status}`;\n}\n\n/**\n * The client-side counterpart to `mountCmsRoutes` — talks to exactly the\n * REST surface that function mounts (`GET/POST/PATCH/DELETE\n * /api/:collection[...]`), via plain `fetch()`. No Node APIs, works from\n * any environment (browser, Worker, Astro SSR).\n *\n * This is for callers *outside* the Worker process that's actually running\n * the CMS — an Astro island, an external operator's own client, anything\n * that can't call a `LocalApi` in-process. In-process callers (the same\n * Worker's own server functions, Cadmea's own Panel) should keep calling\n * the `LocalApi`/Hono RPC (`hc<AppType>`) directly — this client adds a\n * network hop neither of those needs.\n */\nexport function createCmsApiClient(\n baseUrl: string,\n options: CmsApiClientOptions = {},\n) {\n async function request(\n method: string,\n path: string,\n body?: unknown,\n ): Promise<unknown> {\n const headers: Record<string, string> = {};\n const authHeader = await options.getAuthHeader?.();\n if (authHeader) headers.Authorization = authHeader;\n if (body !== undefined) headers[\"Content-Type\"] = \"application/json\";\n\n let response: Response;\n try {\n response = await fetch(`${baseUrl}/api${path}`, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n } catch (cause) {\n throw new CadmusApiError(\n `Request to \"${baseUrl}/api${path}\" failed`,\n 0,\n cause,\n );\n }\n\n const parsed = await parseBody(response);\n if (!response.ok) {\n throw new CadmusApiError(\n errorMessage(parsed, response.status),\n response.status,\n parsed,\n );\n }\n return parsed;\n }\n\n return {\n find(collection: string): Promise<unknown[]> {\n return request(\"GET\", `/${collection}`) as Promise<unknown[]>;\n },\n findByID(collection: string, id: number): Promise<unknown> {\n return request(\"GET\", `/${collection}/${id}`);\n },\n search(collection: string, query: string): Promise<unknown[]> {\n return request(\n \"GET\",\n `/${collection}/search?q=${encodeURIComponent(query)}`,\n ) as Promise<unknown[]>;\n },\n create(\n collection: string,\n data: Record<string, unknown>,\n ): Promise<unknown> {\n return request(\"POST\", `/${collection}`, data);\n },\n update(\n collection: string,\n id: number,\n data: Record<string, unknown>,\n ): Promise<unknown> {\n return request(\"PATCH\", `/${collection}/${id}`, data);\n },\n delete(collection: string, id: number): Promise<unknown> {\n return request(\"DELETE\", `/${collection}/${id}`);\n },\n };\n}\n\nexport type CmsApiClient = ReturnType<typeof createCmsApiClient>;\n","// Copyright (c) 2026 BowenLabs. All rights reserved.\n// Cadmus is MIT licensed. See LICENSE in the repo root.\n\nimport type { Context } from \"hono\";\nimport { Hono } from \"hono\";\nimport type { ClientErrorStatusCode } from \"hono/utils/http-status\";\nimport type { LocalApi } from \"../cms/index.js\";\nimport { CadmusAccessDeniedError, CadmusCmsError } from \"../errors.js\";\n\nexport interface CmsRoutesOptions<TContext> {\n // biome-ignore lint/suspicious/noExplicitAny: see above\n collections: Record<string, LocalApi<any, TContext>>;\n /**\n * Resolves the per-request access context passed as the first argument\n * to every Local API call below — called once per request, not once per\n * collection method, so e.g. a session lookup only happens once even\n * though a write touches `create` and its `afterChange` hooks. Cadmus\n * doesn't standardize the context shape (see LocalApi's `TContext`) —\n * the caller's `resolveContext` is the one place that decides it, the\n * same way Cadmea's server functions each call `requireAuthOrThrow()`\n * themselves today.\n */\n resolveContext: (c: Context) => Promise<TContext>;\n}\n\n// Coupled to the exact message strings localApi.ts's notFound() and\n// wrapWriteError() author — both files are Cadmus-internal, so this is\n// matching a contract we control, not arbitrary third-party text. The\n// honest long-term fix is a status/discriminated-code field on\n// CadmusCmsError; flagged as a follow-up, not built here (it would\n// ripple across every existing primitive error).\nfunction statusForError(error: CadmusCmsError): ClientErrorStatusCode {\n if (error instanceof CadmusAccessDeniedError) return 403;\n if (error.message.includes(\"document found with id\")) return 404;\n if (error.message.includes(\"Unique constraint violated\")) return 409;\n return 400;\n}\n\nfunction getApi<TContext>(\n collections: CmsRoutesOptions<TContext>[\"collections\"],\n slug: string,\n // biome-ignore lint/suspicious/noExplicitAny: see CmsRoutesOptions\n): LocalApi<any, TContext> {\n const api = collections[slug];\n if (!api) throw new CadmusCmsError(`Unknown collection \"${slug}\"`);\n return api;\n}\n\n// Mounts a Payload-equivalent REST surface at /api:\n// GET /api/:collection\n// GET /api/:collection/search?q=...\n// GET /api/:collection/:id\n// POST /api/:collection\n// PATCH /api/:collection/:id\n// DELETE /api/:collection/:id\nexport function mountCmsRoutes<TContext>(\n app: Hono,\n options: CmsRoutesOptions<TContext>,\n): Hono {\n const router = new Hono();\n\n router.onError((error, c) => {\n if (error instanceof CadmusCmsError) {\n return c.json({ error: error.message }, statusForError(error));\n }\n throw error;\n });\n\n // `resolveContext` runs once per request, before any Local API call —\n // every route below shares the one resolved context across its method\n // call and that method's own hooks (e.g. create()'s afterChange).\n router.get(\"/:collection\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.find(context));\n });\n\n // Registered before \"/:collection/:id\" — Hono's router prioritizes a\n // static path segment (\"search\") over a dynamic one (\":id\") regardless\n // of registration order, but the ordering here documents the intent\n // either way: a request for /api/pages/search must never be parsed as\n // findByID with id=\"search\".\n router.get(\"/:collection/search\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.search(context, c.req.query(\"q\") ?? \"\"));\n });\n\n router.get(\"/:collection/:id\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.findByID(context, Number(c.req.param(\"id\"))));\n });\n\n router.post(\"/:collection\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.create(context, await c.req.json()), 201);\n });\n\n router.patch(\"/:collection/:id\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n const id = Number(c.req.param(\"id\"));\n return c.json(await api.update(context, id, await c.req.json()));\n });\n\n router.delete(\"/:collection/:id\", async (c) => {\n const api = getApi(options.collections, c.req.param(\"collection\"));\n const context = await options.resolveContext(c);\n return c.json(await api.deleteByID(context, Number(c.req.param(\"id\"))));\n });\n\n app.route(\"/api\", router);\n return app;\n}\n"],"mappings":"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"}