@cancia/astro 0.4.1 → 0.5.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.
@@ -0,0 +1,20 @@
1
+ /** Prefix for every Cancia route cache tag. */
2
+ declare const CANCIA_CACHE_TAG_PREFIX = "cancia:route:";
3
+ /**
4
+ * Build the cache tag for a route. Normalises the input so the same page always
5
+ * yields the same tag regardless of how the route string was captured:
6
+ * • strips a query string and hash fragment
7
+ * • ensures a single leading "/"
8
+ * • collapses to "/" for the root and strips the trailing slash otherwise
9
+ * then prefixes `cancia:route:`.
10
+ *
11
+ * Examples:
12
+ * canciaCacheTag("/") -> "cancia:route:/"
13
+ * canciaCacheTag("/about/") -> "cancia:route:/about"
14
+ * canciaCacheTag("about") -> "cancia:route:/about"
15
+ * canciaCacheTag("/blog?page=2") -> "cancia:route:/blog"
16
+ * canciaCacheTag("/x#y") -> "cancia:route:/x"
17
+ */
18
+ declare function canciaCacheTag(route: string): string;
19
+
20
+ export { CANCIA_CACHE_TAG_PREFIX, canciaCacheTag };
package/dist/cache.js ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ CANCIA_CACHE_TAG_PREFIX,
3
+ canciaCacheTag
4
+ } from "./chunk-X6ZFFGIA.js";
5
+ export {
6
+ CANCIA_CACHE_TAG_PREFIX,
7
+ canciaCacheTag
8
+ };
@@ -0,0 +1,97 @@
1
+ import {
2
+ createGitHubClient
3
+ } from "./chunk-U7V53JX7.js";
4
+
5
+ // src/publish-hook.ts
6
+ async function firePublish(hook, opts = {}) {
7
+ if (!hook) return { ok: false, kind: "no-hook" };
8
+ const headers = { ...opts.headers ?? {} };
9
+ if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
10
+ const init = { method: opts.method ?? "POST" };
11
+ if (Object.keys(headers).length > 0) init.headers = headers;
12
+ try {
13
+ const res = await fetch(hook, init);
14
+ if (!res.ok) return { ok: false, kind: "bad-response", status: res.status };
15
+ return { ok: true };
16
+ } catch {
17
+ return { ok: false, kind: "unreachable" };
18
+ }
19
+ }
20
+
21
+ // src/publish-dispatch.ts
22
+ var CANCIA_PUBLISH_EVENT = "cancia-publish";
23
+ async function fireDispatch(opts) {
24
+ if (!opts.repo || !opts.token) return { ok: false, kind: "not-configured" };
25
+ const client = createGitHubClient({
26
+ repo: opts.repo,
27
+ // branch is irrelevant for dispatch (repo-level event) but required by the
28
+ // client's options — a harmless placeholder.
29
+ branch: "main",
30
+ token: opts.token,
31
+ fetch: opts.fetch,
32
+ apiBase: opts.apiBase
33
+ });
34
+ try {
35
+ await client.dispatch(opts.eventType ?? CANCIA_PUBLISH_EVENT, opts.clientPayload);
36
+ return { ok: true };
37
+ } catch {
38
+ return { ok: false, kind: "failed" };
39
+ }
40
+ }
41
+
42
+ // src/publish-modes.ts
43
+ function json(body, status = 200) {
44
+ return new Response(JSON.stringify(body), {
45
+ status,
46
+ headers: { "Content-Type": "application/json" }
47
+ });
48
+ }
49
+ async function runPublish(cfg) {
50
+ const mode = cfg.mode;
51
+ if (mode === "live" || mode === "invalidate") {
52
+ return json({ ok: true, mode });
53
+ }
54
+ if (mode === "redeploy") {
55
+ return runRedeploy(cfg.hook, cfg.hookOptions);
56
+ }
57
+ if (mode === "dispatch") {
58
+ return runDispatch(cfg.repo, cfg.githubToken);
59
+ }
60
+ if (cfg.repo && cfg.githubToken) {
61
+ return runDispatch(cfg.repo, cfg.githubToken);
62
+ }
63
+ return runRedeploy(cfg.hook, cfg.hookOptions);
64
+ }
65
+ async function runRedeploy(hook, hookOptions) {
66
+ const result = await firePublish(hook, hookOptions);
67
+ if (result.ok) return json({ ok: true });
68
+ if (result.kind === "no-hook") {
69
+ return json(
70
+ {
71
+ error: "No publish mechanism configured. Set a publish repo + CANCIA_GITHUB_TOKEN, or CANCIA_DEPLOY_HOOK."
72
+ },
73
+ 503
74
+ );
75
+ }
76
+ if (result.kind === "bad-response") {
77
+ return json({ error: `Deploy hook responded with ${result.status}` }, 502);
78
+ }
79
+ return json({ error: "Failed to reach deploy hook" }, 502);
80
+ }
81
+ async function runDispatch(repo, token) {
82
+ if (!repo || !token) {
83
+ return json(
84
+ {
85
+ error: "No publish mechanism configured. Set a publish repo + CANCIA_GITHUB_TOKEN, or CANCIA_DEPLOY_HOOK."
86
+ },
87
+ 503
88
+ );
89
+ }
90
+ const dispatched = await fireDispatch({ repo, token });
91
+ if (dispatched.ok) return json({ ok: true });
92
+ return json({ error: "GitHub repository_dispatch failed" }, 502);
93
+ }
94
+
95
+ export {
96
+ runPublish
97
+ };
@@ -1,9 +1,12 @@
1
1
  // src/storage/sqlite.ts
2
2
  import { createRequire } from "module";
3
+ import { mkdirSync } from "fs";
4
+ import { dirname } from "path";
3
5
  var require2 = createRequire(import.meta.url);
4
6
  var _db = null;
5
7
  function createDB(dbPath) {
6
8
  const Database = require2("better-sqlite3");
9
+ if (dbPath !== ":memory:") mkdirSync(dirname(dbPath), { recursive: true });
7
10
  const sqlite = new Database(dbPath);
8
11
  sqlite.pragma("journal_mode = WAL");
9
12
  sqlite.exec(`
@@ -11,9 +11,12 @@ import {
11
11
  // src/storage/sqlite-v2.ts
12
12
  import { createRequire } from "module";
13
13
  import { randomUUID } from "crypto";
14
+ import { mkdirSync } from "fs";
15
+ import { dirname } from "path";
14
16
  var require2 = createRequire(import.meta.url);
15
17
  function openDB(dbPath) {
16
18
  const Database = require2("better-sqlite3");
19
+ if (dbPath !== ":memory:") mkdirSync(dirname(dbPath), { recursive: true });
17
20
  const db = new Database(dbPath);
18
21
  db.pragma("journal_mode = WAL");
19
22
  db.pragma("foreign_keys = ON");
@@ -0,0 +1,31 @@
1
+ import {
2
+ canciaCacheTag
3
+ } from "./chunk-X6ZFFGIA.js";
4
+
5
+ // src/cache-invalidate.ts
6
+ async function invalidateOnSave(cache, publishMode, route) {
7
+ if (publishMode !== "invalidate") return;
8
+ if (!cache || !cache.enabled) return;
9
+ if (!route || typeof route !== "string") return;
10
+ try {
11
+ await cache.invalidate({ tags: [canciaCacheTag(route)] });
12
+ } catch {
13
+ }
14
+ }
15
+ function extractRoute(body, requestUrl) {
16
+ if (body && typeof body === "object" && "route" in body) {
17
+ const r = body.route;
18
+ if (typeof r === "string" && r.length > 0) return r;
19
+ }
20
+ try {
21
+ const q = new URL(requestUrl).searchParams.get("route");
22
+ if (q) return q;
23
+ } catch {
24
+ }
25
+ return void 0;
26
+ }
27
+
28
+ export {
29
+ invalidateOnSave,
30
+ extractRoute
31
+ };
@@ -0,0 +1,20 @@
1
+ // src/cache.ts
2
+ var CANCIA_CACHE_TAG_PREFIX = "cancia:route:";
3
+ function canciaCacheTag(route) {
4
+ return CANCIA_CACHE_TAG_PREFIX + normalizeRoute(route);
5
+ }
6
+ function normalizeRoute(route) {
7
+ let r = typeof route === "string" ? route : "";
8
+ const q = r.indexOf("?");
9
+ if (q !== -1) r = r.slice(0, q);
10
+ const h = r.indexOf("#");
11
+ if (h !== -1) r = r.slice(0, h);
12
+ r = "/" + r.replace(/^\/+/, "");
13
+ if (r.length > 1) r = r.replace(/\/+$/, "");
14
+ return r === "" ? "/" : r;
15
+ }
16
+
17
+ export {
18
+ CANCIA_CACHE_TAG_PREFIX,
19
+ canciaCacheTag
20
+ };
@@ -1,15 +1,15 @@
1
+ import {
2
+ detectImageType,
3
+ isValidSite
4
+ } from "./chunk-5IPHDIC6.js";
1
5
  import {
2
6
  createGitBackedAdapter,
3
7
  createSqliteAdapterV2
4
- } from "./chunk-5AUIPW2I.js";
8
+ } from "./chunk-GNWD7EL2.js";
5
9
  import {
6
10
  createJsonFileAdapter,
7
11
  createJsonFileAdapterV2
8
12
  } from "./chunk-L2VKQJPY.js";
9
- import {
10
- detectImageType,
11
- isValidSite
12
- } from "./chunk-5IPHDIC6.js";
13
13
 
14
14
  // src/routes/upload.ts
15
15
  import { writeFile, mkdir } from "fs/promises";
@@ -170,7 +170,8 @@ function buildRuntimeFromBaked(baked) {
170
170
  const projectRoot = baked.projectRoot || process.cwd();
171
171
  const token = process.env.CANCIA_TOKEN?.trim() || "";
172
172
  const secret = baked.public ? void 0 : token || void 0;
173
- const deployHook = process.env.CANCIA_DEPLOY_HOOK || void 0;
173
+ const redeployHook = baked.redeployHook || void 0;
174
+ const deployHook = redeployHook || process.env.CANCIA_DEPLOY_HOOK || void 0;
174
175
  const deployHookToken = process.env.CANCIA_DEPLOY_HOOK_TOKEN || void 0;
175
176
  const publishRepo = baked.publishRepo || void 0;
176
177
  const publishGithubToken = process.env.CANCIA_GITHUB_TOKEN?.trim() || void 0;
@@ -192,6 +193,8 @@ function buildRuntimeFromBaked(baked) {
192
193
  deployHookHeaders: baked.deployHookHeaders,
193
194
  publishRepo,
194
195
  publishGithubToken,
196
+ publishMode: baked.publishMode,
197
+ redeployHook,
195
198
  maxUploadMB: baked.maxUploadMB
196
199
  };
197
200
  }
@@ -1,9 +1,9 @@
1
+ import { APIContext } from 'astro';
2
+
1
3
  declare function GET({ request }: {
2
4
  request: Request;
3
5
  }): Promise<Response>;
4
- declare function POST({ request }: {
5
- request: Request;
6
- }): Promise<Response>;
6
+ declare function POST(context: APIContext): Promise<Response>;
7
7
  declare function DELETE({ request }: {
8
8
  request: Request;
9
9
  }): Promise<Response>;
@@ -1,3 +1,9 @@
1
+ import {
2
+ extractRoute,
3
+ invalidateOnSave
4
+ } from "../chunk-VGRG5DN7.js";
5
+ import "../chunk-X6ZFFGIA.js";
6
+
1
7
  // src/endpoints/content.ts
2
8
  import { getCanciaRuntime } from "virtual:cancia/runtime";
3
9
  function checkAuth(req, secret) {
@@ -19,8 +25,9 @@ async function GET({ request }) {
19
25
  headers: { "Content-Type": "application/json" }
20
26
  });
21
27
  }
22
- async function POST({ request }) {
23
- const { storage, secret } = getCanciaRuntime();
28
+ async function POST(context) {
29
+ const { request } = context;
30
+ const { storage, secret, publishMode } = getCanciaRuntime();
24
31
  const deny = checkAuth(request, secret);
25
32
  if (deny) return deny;
26
33
  const body = await request.json().catch(() => null);
@@ -30,6 +37,7 @@ async function POST({ request }) {
30
37
  { status: 400 }
31
38
  );
32
39
  await storage.set(body.site, body.key, body.lang, body.value);
40
+ await invalidateOnSave(context.cache, publishMode, extractRoute(body, request.url));
33
41
  return new Response(JSON.stringify({ ok: true }), {
34
42
  headers: { "Content-Type": "application/json" }
35
43
  });
@@ -1,14 +1,10 @@
1
+ import { APIContext } from 'astro';
2
+
1
3
  declare function GET({ request }: {
2
4
  request: Request;
3
5
  }): Promise<Response>;
4
- declare function POST({ request }: {
5
- request: Request;
6
- }): Promise<Response>;
7
- declare function PATCH({ request }: {
8
- request: Request;
9
- }): Promise<Response>;
10
- declare function DELETE({ request }: {
11
- request: Request;
12
- }): Promise<Response>;
6
+ declare function POST(context: APIContext): Promise<Response>;
7
+ declare function PATCH(context: APIContext): Promise<Response>;
8
+ declare function DELETE(context: APIContext): Promise<Response>;
13
9
 
14
10
  export { DELETE, GET, PATCH, POST };
@@ -3,6 +3,11 @@ import {
3
3
  } from "../chunk-22DJVJBR.js";
4
4
  import "../chunk-NG5GJME5.js";
5
5
  import "../chunk-7IA5B5CF.js";
6
+ import {
7
+ extractRoute,
8
+ invalidateOnSave
9
+ } from "../chunk-VGRG5DN7.js";
10
+ import "../chunk-X6ZFFGIA.js";
6
11
 
7
12
  // src/endpoints/lists.ts
8
13
  import { getCanciaRuntime } from "virtual:cancia/runtime";
@@ -16,17 +21,26 @@ function getRoutes() {
16
21
  defaultLocale: rt.defaultLocale
17
22
  });
18
23
  }
24
+ async function handleMutation(context, method) {
25
+ const { request } = context;
26
+ const response = await getRoutes().handle(request, method);
27
+ if (response.ok) {
28
+ const { publishMode } = getCanciaRuntime();
29
+ await invalidateOnSave(context.cache, publishMode, extractRoute(void 0, request.url));
30
+ }
31
+ return response;
32
+ }
19
33
  async function GET({ request }) {
20
34
  return getRoutes().handle(request, "GET");
21
35
  }
22
- async function POST({ request }) {
23
- return getRoutes().handle(request, "POST");
36
+ async function POST(context) {
37
+ return handleMutation(context, "POST");
24
38
  }
25
- async function PATCH({ request }) {
26
- return getRoutes().handle(request, "PATCH");
39
+ async function PATCH(context) {
40
+ return handleMutation(context, "PATCH");
27
41
  }
28
- async function DELETE({ request }) {
29
- return getRoutes().handle(request, "DELETE");
42
+ async function DELETE(context) {
43
+ return handleMutation(context, "DELETE");
30
44
  }
31
45
  export {
32
46
  DELETE,
@@ -1,7 +1,6 @@
1
1
  import {
2
- fireDispatch,
3
- firePublish
4
- } from "../chunk-BMCI2F2Y.js";
2
+ runPublish
3
+ } from "../chunk-5RCLBWRH.js";
5
4
  import "../chunk-U7V53JX7.js";
6
5
 
7
6
  // src/endpoints/publish.ts
@@ -14,6 +13,7 @@ async function POST({ request }) {
14
13
  deployHookHeaders,
15
14
  publishRepo,
16
15
  publishGithubToken,
16
+ publishMode,
17
17
  secret
18
18
  } = getCanciaRuntime();
19
19
  if (secret) {
@@ -21,42 +21,18 @@ async function POST({ request }) {
21
21
  if (token !== secret)
22
22
  return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
23
23
  }
24
- const ghToken = publishGithubToken ?? process.env.CANCIA_GITHUB_TOKEN?.trim();
25
- if (publishRepo && ghToken) {
26
- const dispatched = await fireDispatch({ repo: publishRepo, token: ghToken });
27
- if (dispatched.ok)
28
- return new Response(JSON.stringify({ ok: true }), {
29
- headers: { "Content-Type": "application/json" }
30
- });
31
- return new Response(
32
- JSON.stringify({ error: "GitHub repository_dispatch failed" }),
33
- { status: 502 }
34
- );
35
- }
36
24
  const hook = deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
37
- const result = await firePublish(hook, {
38
- token: deployHookToken,
39
- method: deployHookMethod,
40
- headers: deployHookHeaders
41
- });
42
- if (result.ok)
43
- return new Response(JSON.stringify({ ok: true }), {
44
- headers: { "Content-Type": "application/json" }
45
- });
46
- if (result.kind === "no-hook")
47
- return new Response(
48
- JSON.stringify({
49
- error: "No publish mechanism configured. Set a publish repo + CANCIA_GITHUB_TOKEN, or CANCIA_DEPLOY_HOOK."
50
- }),
51
- { status: 503 }
52
- );
53
- if (result.kind === "bad-response")
54
- return new Response(
55
- JSON.stringify({ error: `Deploy hook responded with ${result.status}` }),
56
- { status: 502 }
57
- );
58
- return new Response(JSON.stringify({ error: "Failed to reach deploy hook" }), {
59
- status: 502
25
+ const ghToken = publishGithubToken ?? process.env.CANCIA_GITHUB_TOKEN?.trim();
26
+ return runPublish({
27
+ mode: publishMode,
28
+ hook,
29
+ hookOptions: {
30
+ token: deployHookToken,
31
+ method: deployHookMethod,
32
+ headers: deployHookHeaders
33
+ },
34
+ repo: publishRepo,
35
+ githubToken: ghToken
60
36
  });
61
37
  }
62
38
  export {
package/dist/index.d.ts CHANGED
@@ -60,21 +60,51 @@ interface CanciaIntegrationOptions {
60
60
  /** Optional extra headers merged into the deploy-hook request. */
61
61
  deployHookHeaders?: Record<string, string>;
62
62
  /**
63
- * The host-independent standard publish path (031). When set, the Publish
64
- * button fires a GitHub `repository_dispatch` (event `cancia-publish`) on
65
- * this repo instead of the raw deploy hook — a workflow in the repo
66
- * (templates/cancia-publish.yml) makes an empty commit → the host's normal
67
- * git integration rebuilds. Identical for every host; no host API / IP
68
- * allowlist. The token is NEVER baked — it is read from
69
- * `process.env.CANCIA_GITHUB_TOKEN` at runtime (a fine-grained PAT with
70
- * `contents:write`, which covers repository_dispatch). Precedence:
71
- * publish-dispatch (if repo + token) → deploy hook (030) → 503.
63
+ * Publish mode (034) how an edit becomes live, adapted to the site's render
64
+ * mode. Cancia has ONE publish method with THREE modes plus the legacy
65
+ * dispatch path:
72
66
  *
73
- * If a git-backed `git.repo` is already declared, that repo is reused as the
74
- * publish target unless overridden here.
67
+ * `{ mode: "live" }` SSR, uncached (`prerender=false`). Save IS publish:
68
+ * `getCMS()` reads live per request, so there is NO trigger and NO Publish
69
+ * button. Correct for a pure-SSR site.
70
+ * • `{ mode: "invalidate" }` — SSR + a `cache` provider (ISR). On save the
71
+ * server tag-invalidates the edited page
72
+ * (`cache.invalidate({ tags: [canciaCacheTag(route)] })`). Instant + cached.
73
+ * No Publish button (save is publish).
74
+ * • `{ mode: "redeploy", hook? }` — static (`prerender=true`). On PUBLISH the
75
+ * host's deploy hook is fired (no commit, no junk). This is the recommended
76
+ * static default. Hook precedence: `redeploy.hook` (baked) →
77
+ * `CANCIA_DEPLOY_HOOK` (env). Token always `CANCIA_DEPLOY_HOOK_TOKEN` (env).
78
+ * • `{ mode: "dispatch", github: { repo } }` — static, GitHub
79
+ * `repository_dispatch` (031, legacy). A workflow in the repo makes an empty
80
+ * commit → the host's git integration rebuilds. Kept as the git-commit
81
+ * alternative; NO LONGER the recommended default.
82
+ * • `{ github: { repo } }` — BACK-COMPAT: a bare `github` (what 031 configs
83
+ * use) is treated as `{ mode: "dispatch", github }`. No existing config
84
+ * breaks.
85
+ *
86
+ * Omitted entirely, the mode is resolved from context: a deploy hook (config
87
+ * or CANCIA_DEPLOY_HOOK env) → `redeploy`; else a publish repo (or a reused
88
+ * git-backed `git.repo`) → `dispatch`; else → `live`.
89
+ *
90
+ * Secrets are NEVER baked — the GitHub PAT (`CANCIA_GITHUB_TOKEN`) and the
91
+ * deploy-hook token (`CANCIA_DEPLOY_HOOK_TOKEN`) are read from env at runtime.
75
92
  */
76
93
  publish?: {
77
- github?: {
94
+ mode: "live";
95
+ } | {
96
+ mode: "invalidate";
97
+ } | {
98
+ mode: "redeploy";
99
+ hook?: string;
100
+ } | {
101
+ mode: "dispatch";
102
+ github: {
103
+ repo: string;
104
+ };
105
+ } | {
106
+ /** BACK-COMPAT (031): a bare `github` is treated as dispatch. */
107
+ github: {
78
108
  /** "owner/name" of the repo whose `cancia-publish` workflow rebuilds. */
79
109
  repo: string;
80
110
  };
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import {
2
- fireDispatch,
3
- firePublish
4
- } from "./chunk-BMCI2F2Y.js";
2
+ runPublish
3
+ } from "./chunk-5RCLBWRH.js";
5
4
  import {
6
5
  makeListsRoutes
7
6
  } from "./chunk-22DJVJBR.js";
@@ -14,7 +13,8 @@ import {
14
13
  makeR2UploadHandler,
15
14
  makeUploadRoute,
16
15
  setCanciaRuntime
17
- } from "./chunk-U46HCJN3.js";
16
+ } from "./chunk-YZH7QWZV.js";
17
+ import "./chunk-5IPHDIC6.js";
18
18
  import {
19
19
  defineField,
20
20
  defineList,
@@ -26,12 +26,12 @@ import {
26
26
  } from "./chunk-UR5WC3RA.js";
27
27
  import {
28
28
  createSQLiteAdapter
29
- } from "./chunk-AE4SIY24.js";
29
+ } from "./chunk-CJDIVWO3.js";
30
30
  import {
31
31
  closeSqliteAdapterV2,
32
32
  createGitBackedAdapter,
33
33
  createSqliteAdapterV2
34
- } from "./chunk-5AUIPW2I.js";
34
+ } from "./chunk-GNWD7EL2.js";
35
35
  import "./chunk-U7V53JX7.js";
36
36
  import {
37
37
  createJsonFileAdapter,
@@ -41,7 +41,6 @@ import {
41
41
  RevConflictError
42
42
  } from "./chunk-7IA5B5CF.js";
43
43
  import "./chunk-BOIQNZAO.js";
44
- import "./chunk-5IPHDIC6.js";
45
44
 
46
45
  // src/integration.ts
47
46
  import { loadEnv } from "vite";
@@ -95,7 +94,7 @@ function makeContentRoute(storage, secret) {
95
94
  }
96
95
 
97
96
  // src/routes/publish.ts
98
- function makePublishRoute(deployHook, secret, hookOptions, dispatch) {
97
+ function makePublishRoute(deployHook, secret, hookOptions, dispatch, mode) {
99
98
  return async ({ request }) => {
100
99
  if (secret) {
101
100
  const token = request.headers.get("Authorization")?.replace("Bearer ", "").trim();
@@ -103,40 +102,14 @@ function makePublishRoute(deployHook, secret, hookOptions, dispatch) {
103
102
  return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
104
103
  }
105
104
  }
106
- if (dispatch?.repo && dispatch?.token) {
107
- const dispatched = await fireDispatch({ repo: dispatch.repo, token: dispatch.token });
108
- if (dispatched.ok) {
109
- return new Response(JSON.stringify({ ok: true }), {
110
- headers: { "Content-Type": "application/json" }
111
- });
112
- }
113
- return new Response(
114
- JSON.stringify({ error: "GitHub repository_dispatch failed" }),
115
- { status: 502 }
116
- );
117
- }
118
105
  const hook = deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
119
- const result = await firePublish(hook, hookOptions);
120
- if (result.ok) {
121
- return new Response(JSON.stringify({ ok: true }), {
122
- headers: { "Content-Type": "application/json" }
123
- });
124
- }
125
- if (result.kind === "no-hook") {
126
- return new Response(
127
- JSON.stringify({
128
- error: "No publish mechanism configured. Set a publish repo + CANCIA_GITHUB_TOKEN, or CANCIA_DEPLOY_HOOK."
129
- }),
130
- { status: 503 }
131
- );
132
- }
133
- if (result.kind === "bad-response") {
134
- return new Response(
135
- JSON.stringify({ error: `Deploy hook responded with ${result.status}` }),
136
- { status: 502 }
137
- );
138
- }
139
- return new Response(JSON.stringify({ error: "Failed to reach deploy hook" }), { status: 502 });
106
+ return runPublish({
107
+ mode,
108
+ hook,
109
+ hookOptions,
110
+ repo: dispatch?.repo,
111
+ githubToken: dispatch?.token
112
+ });
140
113
  };
141
114
  }
142
115
 
@@ -226,6 +199,27 @@ CANCIA_TOKEN=${token}
226
199
  }
227
200
 
228
201
  // src/integration.ts
202
+ function resolvePublish(opts, hasDeployHookEnv) {
203
+ const publish = opts.publish;
204
+ const configRedeployHook = publish && "mode" in publish && publish.mode === "redeploy" ? publish.hook : void 0;
205
+ const hasDeployHook = !!(opts.deployHook || configRedeployHook || hasDeployHookEnv);
206
+ const explicitRepo = publish && "github" in publish ? publish.github.repo : void 0;
207
+ const publishRepo = explicitRepo ?? opts.git?.repo;
208
+ let publishMode;
209
+ if (publish && "mode" in publish) {
210
+ publishMode = publish.mode;
211
+ } else if (publish && "github" in publish) {
212
+ publishMode = "dispatch";
213
+ } else {
214
+ if (hasDeployHook) publishMode = "redeploy";
215
+ else if (publishRepo) publishMode = "dispatch";
216
+ else publishMode = "live";
217
+ }
218
+ let canPublish = false;
219
+ if (publishMode === "redeploy") canPublish = hasDeployHook;
220
+ else if (publishMode === "dispatch") canPublish = !!publishRepo;
221
+ return { publishMode, redeployHook: configRedeployHook, publishRepo, canPublish };
222
+ }
229
223
  function buildDefaultV2Store(opts, projectRoot) {
230
224
  if (opts.db?.kind === "sqlite") {
231
225
  const path = opts.db.path ? isAbsolute(opts.db.path) ? opts.db.path : join2(projectRoot, opts.db.path) : join2(projectRoot, "cancia.db");
@@ -241,6 +235,8 @@ function canciaIntegration(opts = {}) {
241
235
  let resolvedUploadHandler;
242
236
  let resolvedDeployHook;
243
237
  let resolvedPublishRepo;
238
+ let resolvedPublishMode = "live";
239
+ let resolvedRedeployHook;
244
240
  let resolvedLocales = ["en"];
245
241
  let bakedConfig = null;
246
242
  return {
@@ -271,9 +267,12 @@ function canciaIntegration(opts = {}) {
271
267
  console.log(`
272
268
  \x1B[32mcancia\x1B[0m No CANCIA_TOKEN found \u2014 generated one for you.`);
273
269
  }
274
- const hasDeployHook = !!(opts.deployHook ?? env.CANCIA_DEPLOY_HOOK ?? process.env.CANCIA_DEPLOY_HOOK);
275
- resolvedPublishRepo = opts.publish?.github?.repo ?? opts.git?.repo;
276
- const canPublish = hasDeployHook || !!resolvedPublishRepo;
270
+ const hasDeployHookEnv = !!(env.CANCIA_DEPLOY_HOOK ?? process.env.CANCIA_DEPLOY_HOOK);
271
+ const resolved = resolvePublish(opts, hasDeployHookEnv);
272
+ resolvedPublishMode = resolved.publishMode;
273
+ resolvedRedeployHook = resolved.redeployHook;
274
+ resolvedPublishRepo = resolved.publishRepo;
275
+ const canPublish = resolved.canPublish;
277
276
  let storageDescriptor;
278
277
  if (opts.git) {
279
278
  storageDescriptor = {
@@ -312,6 +311,8 @@ function canciaIntegration(opts = {}) {
312
311
  deployHookMethod: opts.deployHookMethod,
313
312
  deployHookHeaders: opts.deployHookHeaders,
314
313
  publishRepo: resolvedPublishRepo,
314
+ publishMode: resolvedPublishMode,
315
+ redeployHook: resolvedRedeployHook,
315
316
  storage: storageDescriptor,
316
317
  r2: r2Baked
317
318
  };
@@ -370,7 +371,7 @@ function canciaIntegration(opts = {}) {
370
371
  "astro:server:setup": ({ server }) => {
371
372
  storage = opts.storage ?? createJsonFileAdapter(opts.dbPath);
372
373
  const secret = resolvedToken;
373
- const deployHook = opts.deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
374
+ const deployHook = resolvedRedeployHook ?? opts.deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
374
375
  const deployHookToken = process.env.CANCIA_DEPLOY_HOOK_TOKEN || opts.deployHookToken;
375
376
  const publishGithubToken = process.env.CANCIA_GITHUB_TOKEN?.trim() || void 0;
376
377
  let uploadHandler;
@@ -408,6 +409,8 @@ function canciaIntegration(opts = {}) {
408
409
  deployHookHeaders: opts.deployHookHeaders,
409
410
  publishRepo: resolvedPublishRepo,
410
411
  publishGithubToken,
412
+ publishMode: resolvedPublishMode,
413
+ redeployHook: resolvedRedeployHook,
411
414
  maxUploadMB: opts.maxUploadMB ?? 10
412
415
  });
413
416
  const contentRoutes = makeContentRoute(storage, routeSecret);
@@ -420,7 +423,8 @@ function canciaIntegration(opts = {}) {
420
423
  method: opts.deployHookMethod,
421
424
  headers: opts.deployHookHeaders
422
425
  },
423
- { repo: resolvedPublishRepo, token: publishGithubToken }
426
+ { repo: resolvedPublishRepo, token: publishGithubToken },
427
+ resolvedPublishMode
424
428
  );
425
429
  const authRoute = makeAuthRoute(secret);
426
430
  const listsRoutes = makeListsRoutes({
@@ -552,7 +556,7 @@ function canciaIntegration(opts = {}) {
552
556
  } else {
553
557
  uploadHandler = opts.uploadHandler ?? makeLocalUploadHandler({ maxMB: opts.maxUploadMB });
554
558
  }
555
- const deployHook = opts.deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
559
+ const deployHook = resolvedRedeployHook ?? opts.deployHook ?? process.env.CANCIA_DEPLOY_HOOK;
556
560
  const deployHookToken = process.env.CANCIA_DEPLOY_HOOK_TOKEN || opts.deployHookToken;
557
561
  const publishGithubToken = process.env.CANCIA_GITHUB_TOKEN?.trim() || void 0;
558
562
  const storageV2 = opts.storageV2 === null ? void 0 : opts.storageV2 ?? buildDefaultV2Store(opts, resolvedRootPath);
@@ -571,6 +575,8 @@ function canciaIntegration(opts = {}) {
571
575
  deployHookHeaders: opts.deployHookHeaders,
572
576
  publishRepo: resolvedPublishRepo,
573
577
  publishGithubToken,
578
+ publishMode: resolvedPublishMode,
579
+ redeployHook: resolvedRedeployHook,
574
580
  maxUploadMB: opts.maxUploadMB ?? 10
575
581
  });
576
582
  }
package/dist/runtime.d.ts CHANGED
@@ -2,6 +2,16 @@ import { C as CanciaStorage, a as CanciaStorageV2 } from './types-BMlLS-OS.js';
2
2
  import { U as UploadHandler } from './upload-DwCGjXbz.js';
3
3
  import { G as GitHubCommitter } from './github-client-Db0pIrqE.js';
4
4
 
5
+ /**
6
+ * The resolved publish mode (034). One of:
7
+ * • "live" — SSR, uncached; save IS publish (no trigger).
8
+ * • "invalidate" — SSR + cache provider (ISR); a save tag-invalidates the page.
9
+ * • "redeploy" — static; publish fires the host's deploy hook (no commit).
10
+ * • "dispatch" — static; publish fires a GitHub repository_dispatch (031).
11
+ * `live`/`invalidate` have no Publish button (canPublish false); `redeploy`/
12
+ * `dispatch` do (when their trigger is resolvable).
13
+ */
14
+ type PublishMode = "live" | "invalidate" | "redeploy" | "dispatch";
5
15
  interface CanciaRuntime {
6
16
  /** v1 KV-only storage. Kept for the existing /content endpoint. */
7
17
  storage: CanciaStorage;
@@ -49,6 +59,18 @@ interface CanciaRuntime {
49
59
  * read from CANCIA_GITHUB_TOKEN at runtime, never baked into dist/.
50
60
  */
51
61
  publishGithubToken?: string | undefined;
62
+ /**
63
+ * The resolved publish mode (034). Drives the publish endpoint's dispatch and
64
+ * the save-time invalidate. Optional so eager dev/build callers that predate
65
+ * 034 still typecheck; absent is treated as the legacy dispatch/hook path.
66
+ */
67
+ publishMode?: PublishMode | undefined;
68
+ /**
69
+ * Deploy-hook URL baked from `publish.redeploy.hook` (non-secret-ish). The
70
+ * runtime falls back to CANCIA_DEPLOY_HOOK / deployHook when this is absent.
71
+ * Non-secret, baked.
72
+ */
73
+ redeployHook?: string | undefined;
52
74
  maxUploadMB: number;
53
75
  }
54
76
  /** Describes which storage adapter to build lazily in the server process. */
@@ -97,6 +119,17 @@ interface BakedConfig {
97
119
  * runtime. Absent = no dispatch path (falls through to the deploy hook).
98
120
  */
99
121
  publishRepo?: string | undefined;
122
+ /**
123
+ * The resolved publish mode (034). Optional so baked configs from before 034
124
+ * still typecheck (undefined → runtime treats as the legacy dispatch/hook
125
+ * path). Baked, non-secret.
126
+ */
127
+ publishMode?: PublishMode | undefined;
128
+ /**
129
+ * Deploy-hook URL from `publish.redeploy.hook`, baked when the operator gave it
130
+ * in config. Absent → the runtime reads CANCIA_DEPLOY_HOOK from env. Non-secret.
131
+ */
132
+ redeployHook?: string | undefined;
100
133
  /** How to build the v2 storage adapter at runtime. Absent = no v2 storage. */
101
134
  storage: BakedStorageDescriptor | undefined;
102
135
  /** When set, build an R2 upload handler; secrets come from env at runtime. */
@@ -115,4 +148,4 @@ declare function setBakedConfig(config: BakedConfig): void;
115
148
  declare function setCanciaRuntime(runtime: CanciaRuntime): void;
116
149
  declare function getCanciaRuntime(): CanciaRuntime;
117
150
 
118
- export { type BakedConfig, type BakedR2Config, type BakedStorageDescriptor, type CanciaRuntime, getCanciaRuntime, setBakedConfig, setCanciaRuntime };
151
+ export { type BakedConfig, type BakedR2Config, type BakedStorageDescriptor, type CanciaRuntime, type PublishMode, getCanciaRuntime, setBakedConfig, setCanciaRuntime };
package/dist/runtime.js CHANGED
@@ -2,12 +2,12 @@ import {
2
2
  getCanciaRuntime,
3
3
  setBakedConfig,
4
4
  setCanciaRuntime
5
- } from "./chunk-U46HCJN3.js";
6
- import "./chunk-5AUIPW2I.js";
5
+ } from "./chunk-YZH7QWZV.js";
6
+ import "./chunk-5IPHDIC6.js";
7
+ import "./chunk-GNWD7EL2.js";
7
8
  import "./chunk-U7V53JX7.js";
8
9
  import "./chunk-L2VKQJPY.js";
9
10
  import "./chunk-7IA5B5CF.js";
10
- import "./chunk-5IPHDIC6.js";
11
11
  export {
12
12
  getCanciaRuntime,
13
13
  setBakedConfig,
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createSQLiteAdapter
3
- } from "../chunk-AE4SIY24.js";
3
+ } from "../chunk-CJDIVWO3.js";
4
4
  import {
5
5
  closeSqliteAdapterV2,
6
6
  createGitBackedAdapter,
7
7
  createSqliteAdapterV2
8
- } from "../chunk-5AUIPW2I.js";
8
+ } from "../chunk-GNWD7EL2.js";
9
9
  import {
10
10
  createGitHubClient
11
11
  } from "../chunk-U7V53JX7.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cancia/astro",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Astro integration for Cancia CMS — inline editing with zero separate server",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/storage/index.d.ts",
29
29
  "import": "./dist/storage/index.js"
30
30
  },
31
+ "./cache": {
32
+ "types": "./dist/cache.d.ts",
33
+ "import": "./dist/cache.js"
34
+ },
31
35
  "./richtext": {
32
36
  "types": "./dist/richtext/index.d.ts",
33
37
  "import": "./dist/richtext/index.js"
@@ -1,45 +0,0 @@
1
- import {
2
- createGitHubClient
3
- } from "./chunk-U7V53JX7.js";
4
-
5
- // src/publish-hook.ts
6
- async function firePublish(hook, opts = {}) {
7
- if (!hook) return { ok: false, kind: "no-hook" };
8
- const headers = { ...opts.headers ?? {} };
9
- if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
10
- const init = { method: opts.method ?? "POST" };
11
- if (Object.keys(headers).length > 0) init.headers = headers;
12
- try {
13
- const res = await fetch(hook, init);
14
- if (!res.ok) return { ok: false, kind: "bad-response", status: res.status };
15
- return { ok: true };
16
- } catch {
17
- return { ok: false, kind: "unreachable" };
18
- }
19
- }
20
-
21
- // src/publish-dispatch.ts
22
- var CANCIA_PUBLISH_EVENT = "cancia-publish";
23
- async function fireDispatch(opts) {
24
- if (!opts.repo || !opts.token) return { ok: false, kind: "not-configured" };
25
- const client = createGitHubClient({
26
- repo: opts.repo,
27
- // branch is irrelevant for dispatch (repo-level event) but required by the
28
- // client's options — a harmless placeholder.
29
- branch: "main",
30
- token: opts.token,
31
- fetch: opts.fetch,
32
- apiBase: opts.apiBase
33
- });
34
- try {
35
- await client.dispatch(opts.eventType ?? CANCIA_PUBLISH_EVENT, opts.clientPayload);
36
- return { ok: true };
37
- } catch {
38
- return { ok: false, kind: "failed" };
39
- }
40
- }
41
-
42
- export {
43
- firePublish,
44
- fireDispatch
45
- };