kizlo 0.2.2 → 0.3.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.
@@ -1,3 +1,5 @@
1
+ import { hash, hmac } from "../shared/crypto.js";
2
+
1
3
  //#region src/adapters/captcha.ts
2
4
  /** Author a custom captcha adapter — types your verify function against the {@link CaptchaAdapter} contract. */
3
5
  function createCaptchaAdapter(adapter) {
@@ -72,14 +74,20 @@ function arkose(opts) {
72
74
  })).json()).session_details?.solved === true;
73
75
  };
74
76
  }
77
+ const ALTCHA_ALGORITHMS = {
78
+ "SHA-256": "SHA-256",
79
+ "SHA-384": "SHA-384",
80
+ "SHA-512": "SHA-512"
81
+ };
75
82
  function altcha(opts) {
76
83
  return async ({ token }) => {
77
84
  try {
78
- const payload = JSON.parse(Buffer.from(token, "base64").toString("utf-8"));
79
- const { createHmac, createHash } = await import("node:crypto");
80
- const algo = payload.algorithm.toLowerCase().replace("-", "");
81
- if (createHash(algo).update(`${payload.salt}${payload.number}`).digest("hex") !== payload.challenge) return false;
82
- return createHmac(algo, opts.hmacKey).update(payload.challenge).digest("hex") === payload.signature;
85
+ const json = new TextDecoder().decode(Uint8Array.from(atob(token), (c) => c.charCodeAt(0)));
86
+ const payload = JSON.parse(json);
87
+ const algo = ALTCHA_ALGORITHMS[payload.algorithm.toUpperCase()];
88
+ if (!algo) return false;
89
+ if (await hash(`${payload.salt}${payload.number}`, algo) !== payload.challenge) return false;
90
+ return await hmac(opts.hmacKey, payload.challenge, algo) === payload.signature;
83
91
  } catch {
84
92
  return false;
85
93
  }
@@ -7,10 +7,10 @@ import { bootstrapDev } from "../wp/dev.js";
7
7
  import { reapOrphans, registerSession, removeProjectContainers, spawnWatchdog, unregisterSession } from "../wp/session.js";
8
8
  import { syncSiteSettings } from "../wp/settings.js";
9
9
  import { devStack } from "../wp/stack.js";
10
- import { randomBytes } from "node:crypto";
11
10
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
11
  import { join } from "node:path";
13
12
  import { defineCommand } from "citty";
13
+ import { randomBytes } from "node:crypto";
14
14
  import { networkInterfaces } from "node:os";
15
15
 
16
16
  //#region src/cli/commands/dev.ts
@@ -10,11 +10,11 @@ import { syncSiteSettings } from "../wp/settings.js";
10
10
  import { devStack } from "../wp/stack.js";
11
11
  import { PRESETS, detectPreset, getPreset } from "../presets/index.js";
12
12
  import z from "zod/v4";
13
- import { randomBytes } from "node:crypto";
14
13
  import fs from "node:fs";
15
14
  import path from "node:path";
16
15
  import { defineCommand } from "citty";
17
16
  import * as p from "@clack/prompts";
17
+ import { randomBytes } from "node:crypto";
18
18
 
19
19
  //#region src/cli/commands/init.ts
20
20
  /** Production connection keys — what a real deploy needs, and what `.env.example` always lists. */
@@ -116,6 +116,35 @@ function orCancel(value) {
116
116
  }
117
117
  return value;
118
118
  }
119
+ /**
120
+ * The single overwrite policy for every scaffolded file: create it when absent, overwrite on
121
+ * `--force`, and otherwise ask before clobbering an existing file (keeping it when the user
122
+ * declines or when running non-interactively with `--yes`). Every scaffolded file routes through
123
+ * here rather than deciding for itself, so new files inherit the same behavior for free.
124
+ */
125
+ async function scaffoldFile(cwd, file, opts) {
126
+ const absPath = path.join(cwd, file.relPath);
127
+ const existed = fs.existsSync(absPath);
128
+ if (existed) {
129
+ let overwrite = opts.force;
130
+ if (!opts.force && !opts.yes) {
131
+ p.log.warn(`${file.label} already exists at ${file.relPath}`);
132
+ overwrite = orCancel(await p.confirm({
133
+ message: "Overwrite it?",
134
+ initialValue: true
135
+ }));
136
+ }
137
+ if (!overwrite) return "kept";
138
+ }
139
+ fs.mkdirSync(path.dirname(absPath), { recursive: true });
140
+ fs.writeFileSync(absPath, file.contents);
141
+ return existed ? "overwritten" : "created";
142
+ }
143
+ /** Report a {@link scaffoldFile} outcome in init's usual voice. */
144
+ function reportScaffold(file, result, yes) {
145
+ if (result === "kept") p.log.info(`Kept existing ${file.label} (${file.relPath})${yes ? " — pass --force to overwrite" : ""}`);
146
+ else p.log.success(`${result === "overwritten" ? "Overwrote" : "Created"} ${file.label} (${file.relPath})`);
147
+ }
119
148
  async function collectInteractively(ctx) {
120
149
  let siteUrl;
121
150
  let baseUrl;
@@ -193,7 +222,7 @@ async function collectInteractively(ctx) {
193
222
  validate: validate(dirPath)
194
223
  }));
195
224
  let alias = "";
196
- if (ctx.preset.routeHandler) {
225
+ if (ctx.preset.apiPath) {
197
226
  const serverDir = path.join(dir.replace(/^\.\//, "").replace(/\/+$/, ""), "server");
198
227
  const detected = detectImportAlias(ctx.cwd, serverDir)?.prefix;
199
228
  alias = orCancel(await p.text({
@@ -230,7 +259,7 @@ function collectFromEnv(ctx) {
230
259
  wpUsername: process.env.KIZLO_WORDPRESS_USERNAME?.trim() ?? "",
231
260
  wpPassword: process.env.KIZLO_WORDPRESS_APPLICATION_PASSWORD?.trim() ?? "",
232
261
  dir,
233
- alias: ctx.preset.routeHandler ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
262
+ alias: ctx.preset.apiPath ? aliasWithSlash(detectImportAlias(ctx.cwd, path.join(dir, "server"))?.prefix) : ""
234
263
  };
235
264
  }
236
265
  function readPkg(pkgPath) {
@@ -434,52 +463,38 @@ const init = defineCommand({
434
463
  const exampleBody = `${exampleEnv}\n# Point the app at a local dev stack (managed by \`kizlo dev\`) instead of the keys above:\n# KIZLO_TARGET=dev\n`;
435
464
  const exampleCreated = writeFileIfAbsent(path.join(cwd, ".env.example"), exampleBody);
436
465
  const dirRel = setup.dir.replace(/^\.\//, "").replace(/\/+$/, "");
437
- const configCreated = writeFileIfAbsent(path.join(cwd, "kizlo.config.ts"), kizloConfigTemplate(dirRel, setup.alias, setup.devPath));
438
466
  const serverDirRel = path.join(dirRel, "server");
439
- const serverEntryRel = path.join(serverDirRel, "index.ts");
440
- const serverPath = path.join(cwd, serverEntryRel);
441
- const serverExisted = fs.existsSync(serverPath);
442
- let wroteServer = !serverExisted || force;
443
- if (serverExisted && !force && !yes) {
444
- p.log.warn(`A Kizlo server instance already exists at ${serverEntryRel}`);
445
- wroteServer = orCancel(await p.confirm({
446
- message: "Overwrite it?",
447
- initialValue: true
448
- }));
449
- }
450
- if (wroteServer) {
451
- fs.mkdirSync(path.dirname(serverPath), { recursive: true });
452
- fs.writeFileSync(serverPath, preset.serverEntry());
453
- }
454
- const generatedDirRel = path.join(serverDirRel, "generated");
455
- writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
456
- writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
457
467
  const clientUrl = setup.siteUrl && !sameOrigin(setup.siteUrl, setup.baseUrl) ? setup.baseUrl : void 0;
458
468
  const scaffold = {
459
469
  serverDirName: path.basename(serverDirRel),
470
+ serverEntryPath: path.join(serverDirRel, "index.ts"),
471
+ clientPath: path.join(dirRel, "client.ts"),
460
472
  appDir: detectAppDir(cwd, hasSrcDir),
461
473
  serverImport: (fromDir) => resolveModuleImport(cwd, serverDirRel, fromDir, setup.alias),
462
474
  clientUrl
463
475
  };
464
- const clientRel = path.join(dirRel, "client.ts");
465
- const clientCreated = writeFileIfAbsent(path.join(cwd, clientRel), preset.clientEntry(scaffold));
466
- let routeRel;
467
- let routeCreated = false;
468
- if (preset.routeHandler) {
469
- const route = preset.routeHandler(scaffold);
470
- routeRel = route.path;
471
- routeCreated = writeFileIfAbsent(path.join(cwd, route.path), route.contents);
472
- }
476
+ const files = [{
477
+ label: "Kizlo config",
478
+ relPath: "kizlo.config.ts",
479
+ contents: kizloConfigTemplate(dirRel, setup.alias, setup.devPath)
480
+ }, ...preset.scaffolds(scaffold)];
481
+ const scaffolded = [];
482
+ for (const file of files) scaffolded.push({
483
+ file,
484
+ result: await scaffoldFile(cwd, file, {
485
+ force,
486
+ yes
487
+ })
488
+ });
489
+ const generatedDirRel = path.join(serverDirRel, "generated");
490
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "contract.json"), "{}\n");
491
+ writeFileIfAbsent(path.join(cwd, generatedDirRel, "index.ts"), CONTRACT_BARREL);
473
492
  const gitignore = ensureGitignored(cwd, ".env");
474
493
  if (!envExisted) p.log.success("Created .env");
475
494
  else if (merge.updated.length || merge.added.length) p.log.success("Updated .env");
476
495
  else p.log.info("Left .env unchanged");
477
496
  p.log.success(exampleCreated ? "Created .env.example" : "Skipped .env.example (exists)");
478
- p.log.success(configCreated ? "Created kizlo.config.ts" : "Skipped kizlo.config.ts (exists)");
479
- if (wroteServer) p.log.success(`${serverExisted ? "Overwrote" : "Created"} Kizlo server instance (${serverEntryRel})`);
480
- else p.log.info(`Kept existing Kizlo server instance (${serverEntryRel})${yes ? " — pass --force to overwrite" : ""}`);
481
- p.log.success(clientCreated ? `Created browser client (${clientRel})` : "Skipped browser client (exists)");
482
- if (routeRel) p.log.success(routeCreated ? `Created API route (${routeRel})` : "Skipped API route (exists)");
497
+ for (const { file, result } of scaffolded) reportScaffold(file, result, yes);
483
498
  if (gitignore !== "present") p.log.success(`${gitignore === "created" ? "Created" : "Updated"} .gitignore (ignoring .env)`);
484
499
  if (setup.mode === "local") {
485
500
  p.log.success(`Local WordPress configured (${setup.devPath}) — .env points at ${setup.wpUrl}`);
@@ -4,27 +4,35 @@ const base = {
4
4
  label: "Generic (no framework detected)",
5
5
  detect: () => 1,
6
6
  baseUrlEnvKey: "KIZLO_BACKEND_URL",
7
- serverEntry() {
8
- return `import { createKizlo } from "kizlo"
7
+ scaffolds(ctx) {
8
+ return [{
9
+ label: "Kizlo server instance",
10
+ relPath: ctx.serverEntryPath,
11
+ contents: `import { createKizlo } from "kizlo"
9
12
 
10
13
  export const { router, client, context, handler } = createKizlo()
11
- `;
12
- },
13
- clientEntry(ctx) {
14
- if (ctx.clientUrl) return `import { createKizloClient } from "kizlo"
14
+ `
15
+ }, {
16
+ label: "Browser client",
17
+ relPath: ctx.clientPath,
18
+ contents: clientEntry(ctx)
19
+ }];
20
+ }
21
+ };
22
+ function clientEntry(ctx) {
23
+ if (ctx.clientUrl) return `import { createKizloClient } from "kizlo"
15
24
  import { contract } from "./${ctx.serverDirName}/generated"
16
25
 
17
26
  // Your Kizlo backend runs on a different origin than this app. Swap in your bundler's
18
27
  // public env var (e.g. import.meta.env.VITE_KIZLO_BACKEND_URL) to set it per environment.
19
28
  export const client = createKizloClient(contract, { url: "${ctx.clientUrl}" })
20
29
  `;
21
- return `import { createKizloClient } from "kizlo"
30
+ return `import { createKizloClient } from "kizlo"
22
31
  import { contract } from "./${ctx.serverDirName}/generated"
23
32
 
24
33
  export const client = createKizloClient(contract)
25
34
  `;
26
- }
27
- };
35
+ }
28
36
 
29
37
  //#endregion
30
38
  export { base };
@@ -10,28 +10,75 @@ const nextjs = {
10
10
  },
11
11
  baseUrlEnvKey: "NEXT_PUBLIC_KIZLO_BACKEND_URL",
12
12
  apiPath: "/api/kizlo",
13
- serverEntry() {
14
- return `import { createKizlo } from "kizlo/nextjs/server"
13
+ scaffolds(ctx) {
14
+ const apiDir = `${ctx.appDir}/api/kizlo/[[...rest]]`;
15
+ const robotsDir = `${ctx.appDir}/robots.txt`;
16
+ const sitemapDir = `${ctx.appDir}/sitemaps/[sitemap]`;
17
+ return [
18
+ {
19
+ label: "Kizlo server instance",
20
+ relPath: ctx.serverEntryPath,
21
+ contents: `import { createKizlo } from "kizlo/nextjs/server"
15
22
 
16
23
  export const { router, client, context, handler } = createKizlo()
17
- `;
18
- },
19
- clientEntry(ctx) {
20
- return `import { createKizloClient } from "kizlo/nextjs"
24
+ `
25
+ },
26
+ {
27
+ label: "Browser client",
28
+ relPath: ctx.clientPath,
29
+ contents: `import { createKizloClient } from "kizlo/nextjs"
21
30
  import { contract } from "./${ctx.serverDirName}/generated"
22
31
 
23
32
  export const client = createKizloClient(contract)
24
- `;
25
- },
26
- routeHandler(ctx) {
27
- const dir = `${ctx.appDir}/api/kizlo/[[...rest]]`;
28
- return {
29
- path: `${dir}/route.ts`,
30
- contents: `import { handler } from "${ctx.serverImport(dir)}"
33
+ `
34
+ },
35
+ {
36
+ label: "API route",
37
+ relPath: `${apiDir}/route.ts`,
38
+ contents: `import { handler } from "${ctx.serverImport(apiDir)}"
31
39
 
32
40
  export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE, handler as OPTIONS }
33
41
  `
34
- };
42
+ },
43
+ {
44
+ label: "robots.txt route",
45
+ relPath: `${robotsDir}/route.ts`,
46
+ contents: `import { createRobotsRoute } from "kizlo/nextjs/server"
47
+ import { client } from "${ctx.serverImport(robotsDir)}"
48
+
49
+ // Run on the edge: cheaper and faster than Node for a response this small.
50
+ export const runtime = "edge"
51
+
52
+ // Vercel treats special metadata files like robots.txt and sitemap.xml as completely static
53
+ // files at build time, bypassing normal Incremental Static Regeneration (ISR) and on-demand
54
+ // revalidatePath rules.
55
+ //
56
+ // While your revalidation logic executes perfectly in a local Node.js server, Vercel uploads
57
+ // these routes directly to its Edge CDN as immutable assets. Subsequent on-demand revalidation
58
+ // triggers will successfully clear the Data Cache but fail to purge the CDN-level Edge cache
59
+ // for that file.
60
+ //
61
+ // force-dynamic opts out of that static treatment so CMS edits show up; the WordPress call is
62
+ // cached, so requests stay cheap.
63
+ export const dynamic = "force-dynamic"
64
+
65
+ export const GET = createRobotsRoute(client)
66
+ `
67
+ },
68
+ {
69
+ label: "sitemap route",
70
+ relPath: `${sitemapDir}/route.ts`,
71
+ contents: `import { createSitemapRoute } from "kizlo/nextjs/server"
72
+ import { client } from "${ctx.serverImport(sitemapDir)}"
73
+
74
+ // Unlike robots.txt, Vercel revalidates this route on demand, so it can stay static. It's
75
+ // served from the CDN at no per-request cost and still refreshes when content changes.
76
+ export const dynamic = "force-static"
77
+
78
+ export const GET = createSitemapRoute(client)
79
+ `
80
+ }
81
+ ];
35
82
  }
36
83
  };
37
84
 
@@ -4,9 +4,9 @@ import { DEFAULT_PLUGINS, TEST_ADMIN } from "./constants.js";
4
4
  import { ensurePlugins } from "./utils.js";
5
5
  import { createAdminAppPassword, seedUsers } from "./bootstrap.js";
6
6
  import { prepareByo } from "./byo.js";
7
- import { randomBytes } from "node:crypto";
8
7
  import { existsSync, rmSync } from "node:fs";
9
8
  import { join } from "node:path";
9
+ import { randomBytes } from "node:crypto";
10
10
 
11
11
  //#region src/cli/wp/dev.ts
12
12
  /** A strong random admin password (144 bits, URL/JSON/CLI-safe characters). */
package/dist/context.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { compare, hmac } from "./shared/crypto.js";
1
2
  import { noopAdapter } from "./adapters/logger.js";
2
3
  import { PreviewTokenData } from "./shared/schema.js";
3
4
  import { CookiesStorage } from "./cookie.js";
4
5
  import { Service } from "./service.js";
5
- import { compare, hmac } from "./shared/crypto.js";
6
6
  import { base64Decode, tryCatchSync } from "@kizlo/shared";
7
7
  import { serialize } from "cookie";
8
8
 
@@ -106,7 +106,7 @@ var Context = class {
106
106
  if (error) return null;
107
107
  if (Date.now() > data.payload.expires * 1e3) return null;
108
108
  const payloadStr = `${data.payload.id}.${data.payload.parent}.${data.payload.expires}`;
109
- if (!compare(hmac(this.config.siteSecret ?? "", payloadStr), data.hash)) return null;
109
+ if (!await compare(await hmac(this.config.siteSecret ?? "", payloadStr), data.hash)) return null;
110
110
  return data.payload;
111
111
  };
112
112
  }
package/dist/index.d.ts CHANGED
@@ -17,8 +17,8 @@ import "./wordpress/index.js";
17
17
  import { DevPluginSource, Fixture, FixtureCleanupFn, FixtureSeedFn, JsonValue, LocalPlugin, PluginSource, SeedContext, TestAdminUser, TestCredentials, TestUser, defineFixture, isLocalPlugin } from "./cli/wp/types.js";
18
18
  import { AuthAdapter, AuthGetUser, AuthUser, authMock, createAuthAdapter } from "./adapters/auth.js";
19
19
  import { CaptchaAdapter, altcha, arkose, createCaptchaAdapter, friendlyCaptcha, hcaptcha, recaptcha, turnstile } from "./adapters/captcha.js";
20
- import { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList } from "./seo/schema.js";
21
- import { WPK_ListSitemapUrlInput, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapUrl, WPK_SitemapUrlImage } from "./seo/types.js";
20
+ import { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList } from "./seo/schema.js";
21
+ import { WPK_ListSitemapUrlInput, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapIndex, WPK_SitemapUrl, WPK_SitemapUrlImage } from "./seo/types.js";
22
22
  import { SitemapIndexEntry } from "./seo/utils.js";
23
23
  import { CurrencyFormat, IdentifierInput, LIST_ORDER, ListMetadata, ListOrder, Media, PreviewTokenData, PreviewTokenPayload } from "./shared/schema.js";
24
24
  import { AnyContext, AnyNestedClient, CookiesAdapter, CookiesDeleteAll, CookiesGetAll, CookiesSetAll, HTTPMethod, List, MaybeOptionalOptions, RedirectInput, RedirectStatus } from "./shared/types.js";
@@ -39,8 +39,8 @@ import { AnyProcedure, AnyProcedureRouter, BaseHandlerOptions, ExtractParams, Ex
39
39
  import { KizloClient, KizloClientConfig, createKizloClient } from "./client.js";
40
40
  import { CreateSitemapRouteOptions, SITEMAP_BASE, SITEMAP_INDEX_SLUG, SitemapRouteExtra, createSitemapRoute } from "./seo/sitemap.js";
41
41
  import { Contract, ContractProcedure, generateContract, isContractProcedure, restoreContract } from "./shared/contract.js";
42
- import { compare, hash, hmac } from "./shared/crypto.js";
42
+ import { ShaAlgorithm, compare, hash, hmac } from "./shared/crypto.js";
43
43
  import { Identifier, parseIdentifier } from "./shared/identifier.js";
44
44
  import { createOrpcRouter } from "./shared/router.js";
45
45
  import { deserializeCurrencyFormat, deserializeListMetadata, emptyList, serializeCurrencyFormat } from "./shared/serialize.js";
46
- export { AnyContext, AnyEventSchema, AnyEvents, AnyExtension, AnyKizloConfig, AnyMiddleware, AnyNestedClient, AnyProcedure, AnyProcedureRouter, AuthAdapter, AuthGetUser, AuthUser, AuthUserFn, BaseHandlerOptions, CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CaptchaAdapter, CommentService, CommonErrorCode, CommonErrorLiteral, CommonErrorMap, ConnInfo, ConnInfoFn, ConsoleLogAdapterOptions, ContentType, Context, ContextConfig, Contract, ContractProcedure, CookiesAdapter, CookiesDeleteAll, CookiesGetAll, CookiesSetAll, CreateKizloOptions, CreateSitemapRouteOptions, CurrencyFormat, DefinedErrorMapLike, DevPluginSource, Environment, ErrorDefinition, EventHandler, EventHandlerFn, Extension, ExtensionInitFn, ExtensionInitOptions, ExtensionInitResult, ExtractExtensionRouter, ExtractMiddlewareContextOutput, ExtractParams, ExtractProcedureByScope, Fixture, FixtureCleanupFn, FixtureSeedFn, GeoAdapter, GeoGetConnInfo, HTTPMethod, Identifier, IdentifierInput, InferEventUnion, InferExtensionRouter, InferUses, InvocationScope, JsonValue, Kizlo, KizloAuthorsSettings, KizloAuthorsSettingsInput, KizloClient, KizloClientConfig, KizloConfig, KizloCrawlingSettings, KizloCrawlingSettingsInput, KizloError, KizloErrorOptions, KizloIdentitySettings, KizloIdentitySettingsInput, KizloMedia, KizloOrganizationFounder, KizloOrganizationSettings, KizloOrganizationSettingsInput, KizloPersonSettings, KizloPersonSettingsInput, KizloPostStatus, KizloPostTypeSettings, KizloPostTypeSettingsInput, KizloSettings, KizloSettingsService, KizloSiteSettings, KizloSiteSettingsInput, KizloSocialProfile, KizloTarget, KizloTaxonomySettings, KizloTaxonomySettingsInput, KizloWebhookSettings, KizloWebhookSettingsInput, LIST_ORDER, List, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, LocalPlugin, LogLevel, LogPayload, Logger, LoggerAdapter, MaybeOptionalOptions, Media, MenuItemService, MenuService, Middleware, MiddlewareHandler, MiddlewareNextFn, MiddlewareOptions, MiddlewareResult, OpenapiPart, PluginSource, PostService, PreviewTokenData, PreviewTokenPayload, Procedure, ProcedureContext, ProcedureErrors, ProcedureHandler, ProcedureOptions, RedirectInput, RedirectStatus, ResolveParamsPart, ResolvedSchemaInput, ResolvedSchemaOutput, Robots, RobotsRule, RootRouter, RouterDepthTuple, S2SClient, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, SchemaIO, SeedContext, Seo, SeoHead, SeoSchema, ServerContext, Service, ServiceAdapters, ServiceConfig, Sitemap, type SitemapIndexEntry, SitemapList, SitemapRouteExtra, SitemapUrl, SitemapUrlImage, SitemapUrlList, TestAdminUser, TestCredentials, TestUser, ThrowableErrorFn, ThrowableErrorMap, UNEXPECTED_BODY_SNIPPET_LENGTH, UserService, VerifyCaptchaFn, VerifyPreviewTokenFn, WC_CORE_BASE, WC_STORE_BASE, WPK_ListSitemapUrlInput, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapUrl, WPK_SitemapUrlImage, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CORE_BASE, WP_Comment, WP_CommentAvatarUrls, WP_CommentContent, WP_CommentContentInput, WP_CommentCreateErrorCode, WP_CommentCreateInput, WP_CommentDeleteErrorCode, WP_CommentDeleteInput, WP_CommentDeleteResponse, WP_CommentLinks, WP_CommentListErrorCode, WP_CommentListInput, WP_CommentOrderBy, WP_CommentRetrieveErrorCode, WP_CommentRetrieveInput, WP_CommentStatus, WP_CommentStatusFilter, WP_CommentStatusInput, WP_CommentType, WP_CommentUpdateErrorCode, WP_CommentUpdateInput, WP_CommonErrorCode, WP_Context, WP_CurrencyFormat, WP_Error, WP_ErrorData, WP_KIZLO_BASE, WP_Link, WP_List, WP_ListMetadata, WP_ListOrder, WP_Menu, WP_MenuCreateErrorCode, WP_MenuCreateInput, WP_MenuDeleteErrorCode, WP_MenuDeleteInput, WP_MenuDeleteResponse, WP_MenuItem, WP_MenuItemCreateErrorCode, WP_MenuItemCreateInput, WP_MenuItemDeleteErrorCode, WP_MenuItemDeleteInput, WP_MenuItemDeleteResponse, WP_MenuItemLinks, WP_MenuItemListErrorCode, WP_MenuItemListInput, WP_MenuItemOrderBy, WP_MenuItemRetrieveErrorCode, WP_MenuItemRetrieveInput, WP_MenuItemStatus, WP_MenuItemStatusFilter, WP_MenuItemTarget, WP_MenuItemTaxRelation, WP_MenuItemTitle, WP_MenuItemTitleInput, WP_MenuItemType, WP_MenuItemUpdateErrorCode, WP_MenuItemUpdateInput, WP_MenuLinks, WP_MenuListErrorCode, WP_MenuListInput, WP_MenuOrderBy, WP_MenuRetrieveErrorCode, WP_MenuRetrieveInput, WP_MenuUpdateErrorCode, WP_MenuUpdateInput, WP_Post, WP_PostCommentStatus, WP_PostContent, WP_PostContentInput, WP_PostCreateErrorCode, WP_PostCreateInput, WP_PostDeleteErrorCode, WP_PostDeleteInput, WP_PostDeleteResponse, WP_PostExcerpt, WP_PostExcerptInput, WP_PostFormat, WP_PostGuid, WP_PostLinks, WP_PostListErrorCode, WP_PostListInput, WP_PostOrderBy, WP_PostRetrieveErrorCode, WP_PostRetrieveInput, WP_PostStatus, WP_PostStatusFilter, WP_PostTaxRelation, WP_PostTitle, WP_PostTitleInput, WP_PostUpdateErrorCode, WP_PostUpdateInput, WP_RequestInput, WP_Result, WP_User, WP_UserAvatarUrls, WP_UserCapabilities, WP_UserCreateErrorCode, WP_UserCreateInput, WP_UserDeleteErrorCode, WP_UserDeleteInput, WP_UserDeleteMeErrorCode, WP_UserDeleteMeInput, WP_UserDeleteMeResponse, WP_UserDeleteResponse, WP_UserLinks, WP_UserListErrorCode, WP_UserListInput, WP_UserOrderBy, WP_UserRetrieveErrorCode, WP_UserRetrieveInput, WP_UserRetrieveMeErrorCode, WP_UserRetrieveMeInput, WP_UserUpdateErrorCode, WP_UserUpdateInput, WP_UserUpdateMeErrorCode, WP_UserUpdateMeInput, WP_UserWho, WebhookOptions, WithCommonErrorMap, WordPressCredentials, WordPressService, altcha, arkose, authMock, buildProcedure, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createMiddleware, createOrpcRouter, createProcedure, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, recaptcha, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, toKizloError, turnstile };
46
+ export { AnyContext, AnyEventSchema, AnyEvents, AnyExtension, AnyKizloConfig, AnyMiddleware, AnyNestedClient, AnyProcedure, AnyProcedureRouter, AuthAdapter, AuthGetUser, AuthUser, AuthUserFn, BaseHandlerOptions, CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CaptchaAdapter, CommentService, CommonErrorCode, CommonErrorLiteral, CommonErrorMap, ConnInfo, ConnInfoFn, ConsoleLogAdapterOptions, ContentType, Context, ContextConfig, Contract, ContractProcedure, CookiesAdapter, CookiesDeleteAll, CookiesGetAll, CookiesSetAll, CreateKizloOptions, CreateSitemapRouteOptions, CurrencyFormat, DefinedErrorMapLike, DevPluginSource, Environment, ErrorDefinition, EventHandler, EventHandlerFn, Extension, ExtensionInitFn, ExtensionInitOptions, ExtensionInitResult, ExtractExtensionRouter, ExtractMiddlewareContextOutput, ExtractParams, ExtractProcedureByScope, Fixture, FixtureCleanupFn, FixtureSeedFn, GeoAdapter, GeoGetConnInfo, HTTPMethod, Identifier, IdentifierInput, InferEventUnion, InferExtensionRouter, InferUses, InvocationScope, JsonValue, Kizlo, KizloAuthorsSettings, KizloAuthorsSettingsInput, KizloClient, KizloClientConfig, KizloConfig, KizloCrawlingSettings, KizloCrawlingSettingsInput, KizloError, KizloErrorOptions, KizloIdentitySettings, KizloIdentitySettingsInput, KizloMedia, KizloOrganizationFounder, KizloOrganizationSettings, KizloOrganizationSettingsInput, KizloPersonSettings, KizloPersonSettingsInput, KizloPostStatus, KizloPostTypeSettings, KizloPostTypeSettingsInput, KizloSettings, KizloSettingsService, KizloSiteSettings, KizloSiteSettingsInput, KizloSocialProfile, KizloTarget, KizloTaxonomySettings, KizloTaxonomySettingsInput, KizloWebhookSettings, KizloWebhookSettingsInput, LIST_ORDER, List, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, LocalPlugin, LogLevel, LogPayload, Logger, LoggerAdapter, MaybeOptionalOptions, Media, MenuItemService, MenuService, Middleware, MiddlewareHandler, MiddlewareNextFn, MiddlewareOptions, MiddlewareResult, OpenapiPart, PluginSource, PostService, PreviewTokenData, PreviewTokenPayload, Procedure, ProcedureContext, ProcedureErrors, ProcedureHandler, ProcedureOptions, RedirectInput, RedirectStatus, ResolveParamsPart, ResolvedSchemaInput, ResolvedSchemaOutput, Robots, RobotsRule, RootRouter, RouterDepthTuple, S2SClient, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, SchemaIO, SeedContext, Seo, SeoHead, SeoSchema, ServerContext, Service, ServiceAdapters, ServiceConfig, ShaAlgorithm, Sitemap, SitemapIndex, type SitemapIndexEntry, SitemapList, SitemapRouteExtra, SitemapUrl, SitemapUrlImage, SitemapUrlList, TestAdminUser, TestCredentials, TestUser, ThrowableErrorFn, ThrowableErrorMap, UNEXPECTED_BODY_SNIPPET_LENGTH, UserService, VerifyCaptchaFn, VerifyPreviewTokenFn, WC_CORE_BASE, WC_STORE_BASE, WPK_ListSitemapUrlInput, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapIndex, WPK_SitemapUrl, WPK_SitemapUrlImage, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CORE_BASE, WP_Comment, WP_CommentAvatarUrls, WP_CommentContent, WP_CommentContentInput, WP_CommentCreateErrorCode, WP_CommentCreateInput, WP_CommentDeleteErrorCode, WP_CommentDeleteInput, WP_CommentDeleteResponse, WP_CommentLinks, WP_CommentListErrorCode, WP_CommentListInput, WP_CommentOrderBy, WP_CommentRetrieveErrorCode, WP_CommentRetrieveInput, WP_CommentStatus, WP_CommentStatusFilter, WP_CommentStatusInput, WP_CommentType, WP_CommentUpdateErrorCode, WP_CommentUpdateInput, WP_CommonErrorCode, WP_Context, WP_CurrencyFormat, WP_Error, WP_ErrorData, WP_KIZLO_BASE, WP_Link, WP_List, WP_ListMetadata, WP_ListOrder, WP_Menu, WP_MenuCreateErrorCode, WP_MenuCreateInput, WP_MenuDeleteErrorCode, WP_MenuDeleteInput, WP_MenuDeleteResponse, WP_MenuItem, WP_MenuItemCreateErrorCode, WP_MenuItemCreateInput, WP_MenuItemDeleteErrorCode, WP_MenuItemDeleteInput, WP_MenuItemDeleteResponse, WP_MenuItemLinks, WP_MenuItemListErrorCode, WP_MenuItemListInput, WP_MenuItemOrderBy, WP_MenuItemRetrieveErrorCode, WP_MenuItemRetrieveInput, WP_MenuItemStatus, WP_MenuItemStatusFilter, WP_MenuItemTarget, WP_MenuItemTaxRelation, WP_MenuItemTitle, WP_MenuItemTitleInput, WP_MenuItemType, WP_MenuItemUpdateErrorCode, WP_MenuItemUpdateInput, WP_MenuLinks, WP_MenuListErrorCode, WP_MenuListInput, WP_MenuOrderBy, WP_MenuRetrieveErrorCode, WP_MenuRetrieveInput, WP_MenuUpdateErrorCode, WP_MenuUpdateInput, WP_Post, WP_PostCommentStatus, WP_PostContent, WP_PostContentInput, WP_PostCreateErrorCode, WP_PostCreateInput, WP_PostDeleteErrorCode, WP_PostDeleteInput, WP_PostDeleteResponse, WP_PostExcerpt, WP_PostExcerptInput, WP_PostFormat, WP_PostGuid, WP_PostLinks, WP_PostListErrorCode, WP_PostListInput, WP_PostOrderBy, WP_PostRetrieveErrorCode, WP_PostRetrieveInput, WP_PostStatus, WP_PostStatusFilter, WP_PostTaxRelation, WP_PostTitle, WP_PostTitleInput, WP_PostUpdateErrorCode, WP_PostUpdateInput, WP_RequestInput, WP_Result, WP_User, WP_UserAvatarUrls, WP_UserCapabilities, WP_UserCreateErrorCode, WP_UserCreateInput, WP_UserDeleteErrorCode, WP_UserDeleteInput, WP_UserDeleteMeErrorCode, WP_UserDeleteMeInput, WP_UserDeleteMeResponse, WP_UserDeleteResponse, WP_UserLinks, WP_UserListErrorCode, WP_UserListInput, WP_UserOrderBy, WP_UserRetrieveErrorCode, WP_UserRetrieveInput, WP_UserRetrieveMeErrorCode, WP_UserRetrieveMeInput, WP_UserUpdateErrorCode, WP_UserUpdateInput, WP_UserUpdateMeErrorCode, WP_UserUpdateMeInput, WP_UserWho, WebhookOptions, WithCommonErrorMap, WordPressCredentials, WordPressService, altcha, arkose, authMock, buildProcedure, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createMiddleware, createOrpcRouter, createProcedure, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, recaptcha, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, toKizloError, turnstile };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { authMock, createAuthAdapter } from "./adapters/auth.js";
2
+ import { compare, hash, hmac } from "./shared/crypto.js";
2
3
  import { altcha, arkose, createCaptchaAdapter, friendlyCaptcha, hcaptcha, recaptcha, turnstile } from "./adapters/captcha.js";
3
4
  import { createCookiesAdapter } from "./adapters/cookies.js";
4
5
  import { createGeoAdapter, geoMock } from "./adapters/geo.js";
@@ -22,15 +23,14 @@ import { isWordPressResourceError } from "./wordpress/utils.js";
22
23
  import { WordPressService } from "./wordpress/service.js";
23
24
  import { KizloSettingsService } from "./settings/service.js";
24
25
  import { Service } from "./service.js";
25
- import { compare, hash, hmac } from "./shared/crypto.js";
26
26
  import { Context } from "./context.js";
27
27
  import { deserializeCurrencyFormat, deserializeListMetadata, emptyList, serializeCurrencyFormat } from "./shared/serialize.js";
28
28
  import { parseIdentifier } from "./shared/identifier.js";
29
- import { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList } from "./seo/schema.js";
29
+ import { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList } from "./seo/schema.js";
30
30
  import { createEventHandler, createWebhookRouter } from "./webhook/index.js";
31
31
  import { Kizlo, createKizlo, resolveKizloConfig } from "./kizlo.js";
32
32
  import { SITEMAP_BASE, SITEMAP_INDEX_SLUG, createSitemapRoute } from "./seo/sitemap.js";
33
33
  import { createExtension } from "./shared/extension.js";
34
34
  import { createMiddleware } from "./shared/middleware.js";
35
35
 
36
- export { CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CommentService, ContentType, Context, CurrencyFormat, IdentifierInput, Kizlo, KizloClient, KizloError, KizloSettingsService, LIST_ORDER, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, Media, MenuItemService, MenuService, PostService, PreviewTokenData, PreviewTokenPayload, Robots, RobotsRule, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, Seo, SeoHead, SeoSchema, Service, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList, UNEXPECTED_BODY_SNIPPET_LENGTH, UserService, WC_CORE_BASE, WC_STORE_BASE, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CORE_BASE, WP_Error, WP_KIZLO_BASE, WordPressService, altcha, arkose, authMock, buildProcedure, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createMiddleware, createOrpcRouter, createProcedure, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, recaptcha, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, toKizloError, turnstile };
36
+ export { CART_HASH_HEADER_KEY, COMMON_ERRORS, CONTENT_TYPES, CommentService, ContentType, Context, CurrencyFormat, IdentifierInput, Kizlo, KizloClient, KizloError, KizloSettingsService, LIST_ORDER, ListMetadata, ListOrder, ListSitemapUrlInput, ListSitemapUrlInputBase, Media, MenuItemService, MenuService, PostService, PreviewTokenData, PreviewTokenPayload, Robots, RobotsRule, SAFE_REQUEST_TIMEOUT, SITEMAP_BASE, SITEMAP_INDEX_SLUG, Seo, SeoHead, SeoSchema, Service, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList, UNEXPECTED_BODY_SNIPPET_LENGTH, UserService, WC_CORE_BASE, WC_STORE_BASE, WP_AUTH_HEADER_KEY, WP_AUTH_TYPE, WP_CORE_BASE, WP_Error, WP_KIZLO_BASE, WordPressService, altcha, arkose, authMock, buildProcedure, combineDetailed, compare, consoleLog, createAuthAdapter, createCaptchaAdapter, createCookiesAdapter, createEventHandler, createExtension, createGeoAdapter, createKizlo, createKizloClient, createLoggerAdapter, createMiddleware, createOrpcRouter, createProcedure, createSitemapRoute, createThrowableErrorMap, createWebhookRouter, defineErrorMap, defineFixture, deserializeCurrencyFormat, deserializeListMetadata, emptyList, friendlyCaptcha, generateContract, geoMock, geoVercelNext, handleUnexpectedError, hash, hcaptcha, hmac, isContractProcedure, isKizloError, isLocalPlugin, isWordPressResourceError, noopAdapter, parseIdentifier, recaptcha, resolveKizloConfig, restoreContract, schemaType, serializeCurrencyFormat, toKizloError, turnstile };
@@ -4,20 +4,23 @@ import { Pathname } from "@kizlo/shared";
4
4
 
5
5
  //#region src/integrations/nextjs/revalidate.d.ts
6
6
  type RevalidatePathFn = (path: string, type?: "layout" | "page") => void;
7
- /** A path to revalidate, optionally with the Next `type` a dynamic route needs (e.g. `"page"`). */
7
+ type RevalidateTagFn = (tag: string, profile: string | {
8
+ expire?: number;
9
+ }) => void;
8
10
  type RevalidateTarget = Pathname | {
9
11
  path: Pathname;
10
12
  type?: "layout" | "page";
11
13
  };
12
14
  interface NextRevalidateOptions {
15
+ revalidateTag?: RevalidateTagFn;
13
16
  revalidatePath?: RevalidatePathFn;
14
17
  /** Return extra paths to revalidate for a given event, on top of the event's own URL. */
15
18
  paths?: (event: KizloEvent) => Pathname[] | Promise<Pathname[]>;
16
19
  /**
17
20
  * How to revalidate the sitemap. Defaults to the route `createSitemapRoute` serves
18
- * (`/sitemaps/[sitemap]`, revalidated as a dynamic page). Override this only if you
19
- * serve sitemaps from your own route(s) instead of `createSitemapRoute`; pass `false`
20
- * to skip sitemap revalidation.
21
+ * (`/sitemaps/[sitemap]`, revalidated as a `layout` so the whole on-demand subtree
22
+ * refreshes at once). Override this only if you serve sitemaps from your own route(s)
23
+ * instead of `createSitemapRoute`; pass `false` to skip sitemap revalidation.
21
24
  */
22
25
  sitemap?: false | RevalidateTarget | RevalidateTarget[];
23
26
  }
@@ -1,27 +1,30 @@
1
+ import { POST_EVENT_TYPES, TERM_EVENT_TYPES } from "../../webhook/schema.js";
1
2
  import { createEventHandler } from "../../webhook/index.js";
2
3
  import { createExtension } from "../../shared/extension.js";
4
+ import { ROBOTS_CACHE_TAG } from "./robots.js";
3
5
  import { SITEMAP_ROUTE } from "./sitemap.js";
4
6
 
5
7
  //#region src/integrations/nextjs/revalidate.ts
6
- const ROBOTS_PATH = "/robots.txt";
7
8
  const DEFAULT_SITEMAP_TARGET = {
8
9
  path: SITEMAP_ROUTE,
9
- type: "page"
10
+ type: "layout"
10
11
  };
12
+ const CONTENT_EVENT_TYPES = new Set([...POST_EVENT_TYPES, ...TERM_EVENT_TYPES]);
11
13
  function nextRevalidation(options) {
12
14
  return createExtension({
13
15
  id: "nextjs-revalidation",
14
16
  init: () => ({ events: [createEventHandler(async (event) => {
15
17
  if (!event) return;
16
- if (event.type.startsWith("settings")) return;
17
18
  const revalidatePath = options?.revalidatePath ?? (await import("next/cache")).revalidatePath;
19
+ const revalidateTag = options?.revalidateTag ?? (await import("next/cache")).revalidateTag;
18
20
  const paths = [
19
21
  ...await Promise.resolve(options?.paths?.(event) ?? []),
20
22
  ...event.data?.url ? [event.data.url] : [],
21
- ROBOTS_PATH
23
+ "/post"
22
24
  ];
23
25
  for (const path of normalizePaths(paths)) revalidatePath(path);
24
- for (const target of resolveSitemapTargets(options?.sitemap)) revalidatePath(normalizePath(target.path), target.type);
26
+ if (event.type === "settings.saved") revalidateTag(ROBOTS_CACHE_TAG, { expire: 0 });
27
+ if (CONTENT_EVENT_TYPES.has(event.type)) for (const target of resolveSitemapTargets(options?.sitemap)) revalidatePath(normalizePath(target.path), target.type);
25
28
  })] })
26
29
  });
27
30
  }
@@ -0,0 +1,7 @@
1
+ import { S2SClient } from "../../kizlo.js";
2
+
3
+ //#region src/integrations/nextjs/robots.d.ts
4
+
5
+ declare function createRobotsRoute(client: S2SClient<[]>): (request: Request) => Promise<Response>;
6
+ //#endregion
7
+ export { createRobotsRoute };
@@ -0,0 +1,23 @@
1
+ import { createRobotsRoute } from "../../seo/robots.js";
2
+
3
+ //#region src/integrations/nextjs/robots.ts
4
+ /**
5
+ * Cache tag for the cached robots.txt body. The Next revalidation extension calls
6
+ * `revalidateTag(ROBOTS_CACHE_TAG)` on every webhook event so the route refreshes
7
+ * on-demand instead of on every request.
8
+ */
9
+ const ROBOTS_CACHE_TAG = "kizlo:robots";
10
+ function createRobotsRoute$1(client) {
11
+ const handler = createRobotsRoute(client);
12
+ let getBody;
13
+ return async function GET(request) {
14
+ if (!getBody) {
15
+ const { unstable_cache } = await import("next/cache");
16
+ getBody = unstable_cache(async () => (await handler(request)).text(), [ROBOTS_CACHE_TAG], { tags: [ROBOTS_CACHE_TAG] });
17
+ }
18
+ return new Response(await getBody(), { headers: { "Content-Type": "text/plain; charset=utf-8" } });
19
+ };
20
+ }
21
+
22
+ //#endregion
23
+ export { ROBOTS_CACHE_TAG, createRobotsRoute$1 as createRobotsRoute };
@@ -6,8 +6,11 @@ import { CreateSitemapRouteOptions } from "../../seo/sitemap.js";
6
6
  /**
7
7
  * The Next route the sitemap handler is mounted at: a single dynamic segment named
8
8
  * `[sitemap]`. It is fixed (not configurable) so that one `revalidatePath(SITEMAP_ROUTE,
9
- * "page")` refreshes the index and every generated sitemap at once. `[sitemap]` is Next's
10
- * file-routing convention, which is why it lives in this integration and not in core.
9
+ * "layout")` refreshes the index and every generated sitemap at once. A `layout`
10
+ * revalidation (not `page`) is required because the route has no `generateStaticParams`,
11
+ * so its slugs are generated on-demand and only a subtree invalidation reaches them.
12
+ * `[sitemap]` is Next's file-routing convention, which is why it lives in this integration
13
+ * and not in core.
11
14
  */
12
15
  declare const SITEMAP_ROUTE: "/sitemaps/[sitemap]";
13
16
  /** Next's dynamic-route context. `params` is a promise in the App Router. */
@@ -22,7 +25,7 @@ interface NextRouteContext {
22
25
  * Next wrapper around the framework-agnostic {@link createCoreSitemapRoute}. It serves the
23
26
  * same sitemap index and collection pages, and additionally asserts the route is mounted at
24
27
  * {@link SITEMAP_ROUTE}: if the dynamic segment is not named `[sitemap]`, `params.sitemap`
25
- * is missing and a wrong mount would silently break `revalidatePath(SITEMAP_ROUTE, "page")`,
28
+ * is missing and a wrong mount would silently break `revalidatePath(SITEMAP_ROUTE, "layout")`,
26
29
  * so it throws loudly on the first request instead.
27
30
  */
28
31
  declare function createSitemapRoute(client: S2SClient<[]>, options?: CreateSitemapRouteOptions): (request: Request, ctx?: NextRouteContext) => Promise<Response>;
@@ -4,15 +4,18 @@ import { SITEMAP_BASE, createSitemapRoute } from "../../seo/sitemap.js";
4
4
  /**
5
5
  * The Next route the sitemap handler is mounted at: a single dynamic segment named
6
6
  * `[sitemap]`. It is fixed (not configurable) so that one `revalidatePath(SITEMAP_ROUTE,
7
- * "page")` refreshes the index and every generated sitemap at once. `[sitemap]` is Next's
8
- * file-routing convention, which is why it lives in this integration and not in core.
7
+ * "layout")` refreshes the index and every generated sitemap at once. A `layout`
8
+ * revalidation (not `page`) is required because the route has no `generateStaticParams`,
9
+ * so its slugs are generated on-demand and only a subtree invalidation reaches them.
10
+ * `[sitemap]` is Next's file-routing convention, which is why it lives in this integration
11
+ * and not in core.
9
12
  */
10
13
  const SITEMAP_ROUTE = `${SITEMAP_BASE}/[sitemap]`;
11
14
  /**
12
15
  * Next wrapper around the framework-agnostic {@link createCoreSitemapRoute}. It serves the
13
16
  * same sitemap index and collection pages, and additionally asserts the route is mounted at
14
17
  * {@link SITEMAP_ROUTE}: if the dynamic segment is not named `[sitemap]`, `params.sitemap`
15
- * is missing and a wrong mount would silently break `revalidatePath(SITEMAP_ROUTE, "page")`,
18
+ * is missing and a wrong mount would silently break `revalidatePath(SITEMAP_ROUTE, "layout")`,
16
19
  * so it throws loudly on the first request instead.
17
20
  */
18
21
  function createSitemapRoute$1(client, options) {
@@ -1,3 +1,4 @@
1
1
  import { KizloServerOptions, createKizlo } from "../integrations/nextjs/kizlo.server.js";
2
+ import { createRobotsRoute } from "../integrations/nextjs/robots.js";
2
3
  import { SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
3
- export { KizloServerOptions, SITEMAP_ROUTE, createKizlo, createSitemapRoute };
4
+ export { KizloServerOptions, SITEMAP_ROUTE, createKizlo, createRobotsRoute, createSitemapRoute };
@@ -1,4 +1,5 @@
1
+ import { createRobotsRoute } from "../integrations/nextjs/robots.js";
1
2
  import { SITEMAP_ROUTE, createSitemapRoute } from "../integrations/nextjs/sitemap.js";
2
3
  import { createKizlo } from "../integrations/nextjs/kizlo.server.js";
3
4
 
4
- export { SITEMAP_ROUTE, createKizlo, createSitemapRoute };
5
+ export { SITEMAP_ROUTE, createKizlo, createRobotsRoute, createSitemapRoute };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.2.2";
2
+ var version = "0.3.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -1,5 +1,5 @@
1
- import { createProcedure } from "../shared/procedure.js";
2
1
  import { compare } from "../shared/crypto.js";
2
+ import { createProcedure } from "../shared/procedure.js";
3
3
  import { deserializeListMetadata } from "../shared/serialize.js";
4
4
  import { getPostTypeService } from "../post-type/service.js";
5
5
  import { parseIdentifier } from "../shared/identifier.js";
@@ -59,7 +59,7 @@ const POST_ROUTER_MAP = {
59
59
  }
60
60
  const data = response.data;
61
61
  if (input.query?.password && data.password) {
62
- if (!compare(input.query.password, data.password)) throw errors.POST_PASSWORD_INVALID();
62
+ if (!await compare(input.query.password, data.password)) throw errors.POST_PASSWORD_INVALID();
63
63
  return deserializePost({
64
64
  ...data,
65
65
  password: ""
package/dist/router.d.ts CHANGED
@@ -346,12 +346,23 @@ declare const ROUTER_MAP: {
346
346
  "@graph": Record<string, unknown>[];
347
347
  };
348
348
  }, DefinedErrorMapLike>;
349
- sitemaps: Procedure<"internal", {}, {
350
- key: string;
351
- type: "author" | "post_type" | "taxonomy";
352
- pages: number;
353
- lastmod: string;
354
- }[], DefinedErrorMapLike>;
349
+ sitemaps: {
350
+ list: Procedure<"internal", {}, {
351
+ key: string;
352
+ type: "author" | "post_type" | "taxonomy";
353
+ pages: number;
354
+ lastmod: string;
355
+ }[], DefinedErrorMapLike>;
356
+ index: Procedure<"internal", {}, {
357
+ origin: string;
358
+ sitemaps: {
359
+ key: string;
360
+ type: "author" | "post_type" | "taxonomy";
361
+ pages: number;
362
+ lastmod: string;
363
+ }[];
364
+ }, DefinedErrorMapLike>;
365
+ };
355
366
  robots: Procedure<"internal", {}, {
356
367
  sitemaps: string[];
357
368
  rules: {
package/dist/seo/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createProcedure } from "../shared/procedure.js";
2
2
  import { WP_KIZLO_BASE } from "../wordpress/constants.js";
3
- import { ListSitemapUrlInput, Robots, Seo, SitemapList, SitemapUrlList } from "./schema.js";
3
+ import { ListSitemapUrlInput, Robots, Seo, SitemapIndex, SitemapList, SitemapUrlList } from "./schema.js";
4
4
 
5
5
  //#region src/seo/index.ts
6
6
  const SEO_ROUTER_MAP = {
@@ -53,17 +53,33 @@ const SEO_ROUTER_MAP = {
53
53
  schema: response.data.schema
54
54
  };
55
55
  }),
56
- sitemaps: createProcedure({
57
- scope: "internal",
58
- output: SitemapList
59
- }, async ({ context, errors }) => {
60
- const response = await context.service.wordpress.get("/seo/sitemaps", { base: WP_KIZLO_BASE });
61
- if (response.error) {
62
- context.logger.error("List sitemaps unhandled error", response.error);
63
- throw errors.INTERNAL_SERVER_ERROR();
64
- }
65
- return response.data;
66
- }),
56
+ sitemaps: {
57
+ list: createProcedure({
58
+ scope: "internal",
59
+ output: SitemapList
60
+ }, async ({ context, errors }) => {
61
+ const response = await context.service.wordpress.get("/seo/sitemaps", { base: WP_KIZLO_BASE });
62
+ if (response.error) {
63
+ context.logger.error("List sitemaps unhandled error", response.error);
64
+ throw errors.INTERNAL_SERVER_ERROR();
65
+ }
66
+ return response.data;
67
+ }),
68
+ index: createProcedure({
69
+ scope: "internal",
70
+ output: SitemapIndex
71
+ }, async ({ context, errors }) => {
72
+ const response = await context.service.wordpress.get("/seo/sitemaps/index", { base: WP_KIZLO_BASE });
73
+ if (response.error) {
74
+ context.logger.error("Get sitemap index unhandled error", response.error);
75
+ throw errors.INTERNAL_SERVER_ERROR();
76
+ }
77
+ return {
78
+ origin: response.data.origin,
79
+ sitemaps: response.data.sitemaps
80
+ };
81
+ })
82
+ },
67
83
  robots: createProcedure({
68
84
  scope: "internal",
69
85
  output: Robots
@@ -0,0 +1,18 @@
1
+ //#region src/seo/robots.ts
2
+ function createRobotsRoute(client) {
3
+ return async function GET(_request) {
4
+ const data = await client.seo.robots.call();
5
+ const groups = data.rules.map((rule) => {
6
+ const lines = [`User-agent: ${rule.userAgent}`];
7
+ for (const path of rule.allow) lines.push(`Allow: ${path}`);
8
+ for (const path of rule.disallow) lines.push(`Disallow: ${path}`);
9
+ return lines.join("\n");
10
+ });
11
+ const sitemaps = data.sitemaps.map((url) => `Sitemap: ${url}`).join("\n");
12
+ const blocks = [groups.join("\n\n"), sitemaps].filter(Boolean);
13
+ return new Response(`${blocks.join("\n\n")}\n`, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
14
+ };
15
+ }
16
+
17
+ //#endregion
18
+ export { createRobotsRoute };
@@ -131,6 +131,20 @@ declare const SitemapList: z.ZodArray<z.ZodObject<{
131
131
  lastmod: z.ZodString;
132
132
  }, z.core.$strip>>;
133
133
  type SitemapList = z.infer<typeof SitemapList>;
134
+ declare const SitemapIndex: z.ZodObject<{
135
+ origin: z.ZodString;
136
+ sitemaps: z.ZodArray<z.ZodObject<{
137
+ key: z.ZodString;
138
+ type: z.ZodEnum<{
139
+ author: "author";
140
+ post_type: "post_type";
141
+ taxonomy: "taxonomy";
142
+ }>;
143
+ pages: z.ZodNumber;
144
+ lastmod: z.ZodString;
145
+ }, z.core.$strip>>;
146
+ }, z.core.$strip>;
147
+ type SitemapIndex = z.infer<typeof SitemapIndex>;
134
148
  declare const SitemapUrlImage: z.ZodObject<{
135
149
  loc: z.ZodString;
136
150
  title: z.ZodString;
@@ -193,4 +207,4 @@ declare const Robots: z.ZodObject<{
193
207
  }, z.core.$strip>;
194
208
  type Robots = z.infer<typeof Robots>;
195
209
  //#endregion
196
- export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
210
+ export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
@@ -64,6 +64,10 @@ const Sitemap = z.object({
64
64
  lastmod: z.string()
65
65
  });
66
66
  const SitemapList = z.array(Sitemap);
67
+ const SitemapIndex = z.object({
68
+ origin: z.string(),
69
+ sitemaps: SitemapList
70
+ });
67
71
  const SitemapUrlImage = z.object({
68
72
  loc: z.string(),
69
73
  title: z.string()
@@ -93,4 +97,4 @@ const Robots = z.object({
93
97
  });
94
98
 
95
99
  //#endregion
96
- export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
100
+ export { CONTENT_TYPES, ContentType, ListSitemapUrlInput, ListSitemapUrlInputBase, Robots, RobotsRule, Seo, SeoHead, SeoSchema, Sitemap, SitemapIndex, SitemapList, SitemapUrl, SitemapUrlImage, SitemapUrlList };
@@ -25,19 +25,19 @@ function createSitemapRoute(client, options) {
25
25
  const url = new URL(request.url);
26
26
  const slug = lastPathSegment(url);
27
27
  if (!slug) return notFound();
28
- const { origin } = url;
29
28
  if (slug === SITEMAP_INDEX_SLUG) {
30
- const { data: data$1 } = await client.seo.sitemaps();
31
- return xmlResponse(renderSitemapIndex([...data$1 ?? [], ...extra.map((item) => item.entry)], origin));
29
+ const { data: data$1 } = await client.seo.sitemaps.index();
30
+ return xmlResponse(renderSitemapIndex([...data$1?.sitemaps ?? [], ...extra.map((item) => item.entry)], data$1?.origin ?? url.origin));
32
31
  }
33
32
  const parsed = parseSitemapSlug(slug);
34
33
  if (!parsed) return notFound();
35
34
  const custom = extra.find((item) => item.entry.key === parsed.key);
36
35
  if (custom) {
37
36
  if (parsed.page > custom.entry.pages) return notFound();
38
- return xmlResponse(renderUrlSet(await custom.urls(origin, parsed.page)));
37
+ const { data: data$1 } = await client.seo.sitemaps.index();
38
+ return xmlResponse(renderUrlSet(await custom.urls(data$1?.origin ?? url.origin, parsed.page)));
39
39
  }
40
- const { data: sitemaps } = await client.seo.sitemaps();
40
+ const { data: sitemaps } = await client.seo.sitemaps.list();
41
41
  if (!sitemaps) return notFound();
42
42
  const entry = sitemaps.find((item) => item.key === parsed.key);
43
43
  if (!entry || parsed.page > entry.pages) return notFound();
@@ -62,6 +62,10 @@ interface WPK_Sitemap {
62
62
  pages: number;
63
63
  lastmod: string;
64
64
  }
65
+ interface WPK_SitemapIndex {
66
+ origin: string;
67
+ sitemaps: WPK_Sitemap[];
68
+ }
65
69
  interface WPK_SitemapUrlImage {
66
70
  loc: string;
67
71
  title: string;
@@ -77,4 +81,4 @@ interface WPK_ListSitemapUrlInput {
77
81
  page?: number;
78
82
  }
79
83
  //#endregion
80
- export { WPK_ListSitemapUrlInput, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapUrl, WPK_SitemapUrlImage };
84
+ export { WPK_ListSitemapUrlInput, WPK_RobotRule, WPK_Robots, WPK_Seo, WPK_Sitemap, WPK_SitemapContentType, WPK_SitemapIndex, WPK_SitemapUrl, WPK_SitemapUrlImage };
package/dist/seo/utils.js CHANGED
@@ -69,10 +69,7 @@ function renderUrlSet(urls) {
69
69
  }).join("\n")}\n</urlset>\n`;
70
70
  }
71
71
  function xmlResponse(body) {
72
- return new Response(body, { headers: {
73
- "Content-Type": "application/xml; charset=utf-8",
74
- "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400"
75
- } });
72
+ return new Response(body, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
76
73
  }
77
74
 
78
75
  //#endregion
@@ -1,15 +1,18 @@
1
- import { BinaryLike } from "node:crypto";
2
-
3
1
  //#region src/shared/crypto.d.ts
4
-
2
+ /** SHA family accepted by the Web Crypto API. */
3
+ type ShaAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
5
4
  /**
6
5
  * Compares two strings in a timing-safe manner to prevent timing attacks.
7
6
  *
7
+ * Both inputs are hashed to fixed-length digests before comparison, so the
8
+ * check is constant-time regardless of input length and never throws on a
9
+ * length mismatch.
10
+ *
8
11
  * @param a - The first string to compare.
9
12
  * @param b - The second string to compare.
10
13
  * @returns `true` if the strings match, `false` otherwise.
11
14
  */
12
- declare function compare(a: string, b: string): boolean;
15
+ declare function compare(a: string, b: string): Promise<boolean>;
13
16
  /**
14
17
  * Hashes a secret API key with SHA-256 for secure database storage.
15
18
  * Safe to use in place of bcrypt/argon2 because API keys are high-entropy
@@ -19,10 +22,10 @@ declare function compare(a: string, b: string): boolean;
19
22
  * @param key - The raw secret API key to hash.
20
23
  * @returns A SHA-256 hex string suitable for database storage.
21
24
  */
22
- declare function hash(key: string): string;
25
+ declare function hash(key: string, algorithm?: ShaAlgorithm): Promise<string>;
23
26
  /**
24
- * Create hmac using node crypto lib.
27
+ * Create an HMAC hex digest using the Web Crypto API. Defaults to SHA-256.
25
28
  */
26
- declare function hmac(key: string, value: BinaryLike): string;
29
+ declare function hmac(key: string, value: string, algorithm?: ShaAlgorithm): Promise<string>;
27
30
  //#endregion
28
- export { compare, hash, hmac };
31
+ export { ShaAlgorithm, compare, hash, hmac };
@@ -1,15 +1,28 @@
1
- import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
-
3
1
  //#region src/shared/crypto.ts
2
+ const encoder = new TextEncoder();
3
+ function toHex(buffer) {
4
+ let hex = "";
5
+ for (const byte of new Uint8Array(buffer)) hex += byte.toString(16).padStart(2, "0");
6
+ return hex;
7
+ }
4
8
  /**
5
9
  * Compares two strings in a timing-safe manner to prevent timing attacks.
6
10
  *
11
+ * Both inputs are hashed to fixed-length digests before comparison, so the
12
+ * check is constant-time regardless of input length and never throws on a
13
+ * length mismatch.
14
+ *
7
15
  * @param a - The first string to compare.
8
16
  * @param b - The second string to compare.
9
17
  * @returns `true` if the strings match, `false` otherwise.
10
18
  */
11
- function compare(a, b) {
12
- return timingSafeEqual(Buffer.from(a), Buffer.from(b));
19
+ async function compare(a, b) {
20
+ const [ha, hb] = await Promise.all([crypto.subtle.digest("SHA-256", encoder.encode(a)), crypto.subtle.digest("SHA-256", encoder.encode(b))]);
21
+ const va = new Uint8Array(ha);
22
+ const vb = new Uint8Array(hb);
23
+ let diff = 0;
24
+ for (let i = 0; i < va.length; i++) diff |= (va[i] ?? 0) ^ (vb[i] ?? 0);
25
+ return diff === 0;
13
26
  }
14
27
  /**
15
28
  * Hashes a secret API key with SHA-256 for secure database storage.
@@ -20,14 +33,18 @@ function compare(a, b) {
20
33
  * @param key - The raw secret API key to hash.
21
34
  * @returns A SHA-256 hex string suitable for database storage.
22
35
  */
23
- function hash(key) {
24
- return createHash("sha256").update(key).digest("hex");
36
+ async function hash(key, algorithm = "SHA-256") {
37
+ return toHex(await crypto.subtle.digest(algorithm, encoder.encode(key)));
25
38
  }
26
39
  /**
27
- * Create hmac using node crypto lib.
40
+ * Create an HMAC hex digest using the Web Crypto API. Defaults to SHA-256.
28
41
  */
29
- function hmac(key, value) {
30
- return createHmac("sha256", key).update(value).digest("hex");
42
+ async function hmac(key, value, algorithm = "SHA-256") {
43
+ const cryptoKey = await crypto.subtle.importKey("raw", encoder.encode(key), {
44
+ name: "HMAC",
45
+ hash: algorithm
46
+ }, false, ["sign"]);
47
+ return toHex(await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(value)));
31
48
  }
32
49
 
33
50
  //#endregion
@@ -14,14 +14,11 @@ const POST_EVENT_TYPES = [
14
14
  "post.deleted"
15
15
  ];
16
16
  const PostEventType = z.enum(POST_EVENT_TYPES);
17
- const PAYMENT_EVENT_TYPES = ["payment.status_requested"];
18
- const PaymentEventType = z.enum(PAYMENT_EVENT_TYPES);
19
17
  const SETTINGS_EVENT_TYPES = ["settings.saved"];
20
18
  const SettingsEventType = z.enum(SETTINGS_EVENT_TYPES);
21
19
  const EVENT_TYPES = [
22
20
  ...TERM_EVENT_TYPES,
23
21
  ...POST_EVENT_TYPES,
24
- ...PAYMENT_EVENT_TYPES,
25
22
  ...SETTINGS_EVENT_TYPES
26
23
  ];
27
24
  const EventType = z.enum(EVENT_TYPES);
@@ -61,4 +58,4 @@ const AnyEvent = z.object({
61
58
  });
62
59
 
63
60
  //#endregion
64
- export { AnyEvent, KizloEvent };
61
+ export { AnyEvent, KizloEvent, POST_EVENT_TYPES, TERM_EVENT_TYPES };
@@ -17,7 +17,10 @@ var WordPressService = class {
17
17
  authHeader;
18
18
  defaultTimeout;
19
19
  constructor(context) {
20
- this.authHeader = `${WP_AUTH_TYPE} ${Buffer.from(`${context.credentials.username}:${context.credentials.password}`, "utf8").toString("base64")}`;
20
+ const credentialBytes = new TextEncoder().encode(`${context.credentials.username}:${context.credentials.password}`);
21
+ let binary = "";
22
+ for (const byte of credentialBytes) binary += String.fromCharCode(byte);
23
+ this.authHeader = `${WP_AUTH_TYPE} ${btoa(binary)}`;
21
24
  this.siteBase = context.credentials.url.endsWith("/") ? context.credentials.url : `${context.credentials.url}/`;
22
25
  this.defaultTimeout = context.timeout ?? SAFE_REQUEST_TIMEOUT;
23
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress toolkit for TypeScript.",
6
6
  "keywords": [