@wp-typia/project-tools 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ import wpEnvUtils from "./wp-env-utils.cjs";
4
+
5
+ const { runWpCli } = wpEnvUtils;
6
+
7
+ const baseUrl = process.argv[2] ?? "http://localhost:8889";
8
+ const timeoutMs = Number.parseInt(process.argv[3] ?? "180000", 10);
9
+ const wpEnvConfig = process.argv[4] ?? ".wp-env.test.json";
10
+ const intervalMs = 2000;
11
+
12
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
13
+ throw new Error(
14
+ `Invalid timeoutMs: ${process.argv[3] ?? "180000"}. Expected a positive integer.`,
15
+ );
16
+ }
17
+
18
+ const deadline = Date.now() + timeoutMs;
19
+ const loginUrl = new URL("/wp-login.php", baseUrl).toString();
20
+
21
+ function looksReady(html) {
22
+ return html.includes('id="loginform"') || html.includes('id="wpadminbar"');
23
+ }
24
+
25
+ function looksLikeInstall(html) {
26
+ return (
27
+ html.includes("install.php") ||
28
+ (html.includes("WordPress") && html.includes("Installation"))
29
+ );
30
+ }
31
+
32
+ function ensureWordPressInstalled(configPath) {
33
+ try {
34
+ runWpCli(["core", "is-installed"], { configPath });
35
+ return false;
36
+ } catch {
37
+ runWpCli(
38
+ [
39
+ "core",
40
+ "install",
41
+ `--url=${baseUrl}`,
42
+ `--title={{slug}}`,
43
+ "--admin_user=admin",
44
+ "--admin_password=password",
45
+ "--admin_email=admin@example.com",
46
+ "--skip-email",
47
+ ],
48
+ { configPath },
49
+ );
50
+ return true;
51
+ }
52
+ }
53
+
54
+ async function main() {
55
+ let lastStatus = "not-started";
56
+ let installAttempted = false;
57
+
58
+ while (Date.now() < deadline) {
59
+ try {
60
+ const response = await fetch(loginUrl, {
61
+ redirect: "follow",
62
+ headers: {
63
+ "cache-control": "no-cache",
64
+ },
65
+ });
66
+ const body = await response.text();
67
+
68
+ if (response.ok && looksReady(body)) {
69
+ console.log(`✅ wp-env ready at ${loginUrl}`);
70
+ return;
71
+ }
72
+
73
+ if (looksLikeInstall(body)) {
74
+ lastStatus = "install-screen";
75
+ if (!installAttempted) {
76
+ try {
77
+ const installed = ensureWordPressInstalled(wpEnvConfig);
78
+ installAttempted = installed;
79
+ lastStatus = installed ? "auto-installed" : "install-screen";
80
+ } catch (error) {
81
+ installAttempted = false;
82
+ lastStatus =
83
+ error instanceof Error ? error.message : "install-failed";
84
+ }
85
+ }
86
+ } else {
87
+ lastStatus = `http-${response.status}`;
88
+ }
89
+ } catch (error) {
90
+ lastStatus =
91
+ error instanceof Error ? error.message : "unknown-network-error";
92
+ }
93
+
94
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
95
+ }
96
+
97
+ throw new Error(
98
+ `Timed out waiting for wp-env login readiness at ${loginUrl} (last status: ${lastStatus})`,
99
+ );
100
+ }
101
+
102
+ await main();
@@ -0,0 +1,32 @@
1
+ const { execFileSync } = require("node:child_process");
2
+ const path = require("node:path");
3
+
4
+ const ROOT_DIR = path.resolve(__dirname, "..");
5
+ const TEST_WP_ENV_CONFIG = ".wp-env.test.json";
6
+
7
+ function getWpEnvCommand() {
8
+ return process.platform === "win32"
9
+ ? path.join(ROOT_DIR, "node_modules", ".bin", "wp-env.cmd")
10
+ : path.join(ROOT_DIR, "node_modules", ".bin", "wp-env");
11
+ }
12
+
13
+ function runWpEnv(args, { cwd = ROOT_DIR } = {}) {
14
+ return execFileSync(getWpEnvCommand(), args, {
15
+ cwd,
16
+ encoding: "utf8",
17
+ shell: process.platform === "win32",
18
+ stdio: "pipe",
19
+ });
20
+ }
21
+
22
+ function runWpCli(args, { configPath = TEST_WP_ENV_CONFIG, cwd = ROOT_DIR } = {}) {
23
+ return runWpEnv(["run", "cli", `--config=${configPath}`, "wp", ...args], { cwd });
24
+ }
25
+
26
+ module.exports = {
27
+ ROOT_DIR,
28
+ TEST_WP_ENV_CONFIG,
29
+ getWpEnvCommand,
30
+ runWpCli,
31
+ runWpEnv,
32
+ };
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ const BLOCK_NAME = "{{namespace}}/{{slugKebabCase}}";
4
+ const BLOCK_TITLE = "{{title}}";
5
+
6
+ test("registers the scaffolded block in the WordPress editor", async ({ page }) => {
7
+ await page.goto("/wp-login.php");
8
+ await page.getByLabel("Username or Email Address").fill("admin");
9
+ await page.getByLabel("Password").fill("password");
10
+ await page.getByRole("button", { name: "Log In" }).click();
11
+
12
+ await page.goto("/wp-admin/post-new.php");
13
+ await page.waitForLoadState("networkidle");
14
+
15
+ await page.waitForFunction(
16
+ (blockName) => Boolean(window.wp?.blocks?.getBlockType(blockName)),
17
+ BLOCK_NAME,
18
+ );
19
+
20
+ const registeredBlock = await page.evaluate((blockName) => {
21
+ const block = window.wp?.blocks?.getBlockType(blockName);
22
+ return block
23
+ ? {
24
+ name: block.name,
25
+ title: block.title,
26
+ }
27
+ : null;
28
+ }, BLOCK_NAME);
29
+
30
+ expect(registeredBlock).toEqual({
31
+ name: BLOCK_NAME,
32
+ title: BLOCK_TITLE,
33
+ });
34
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.wp.org/trunk/wp-env.json",
3
+ "core": null,
4
+ "port": 8888,
5
+ "testsEnvironment": false,
6
+ "plugins": [
7
+ "."
8
+ ],
9
+ "config": {
10
+ "WP_DEBUG": true,
11
+ "WP_DEBUG_LOG": true,
12
+ "WP_DEBUG_DISPLAY": false,
13
+ "SCRIPT_DEBUG": true,
14
+ "WP_ENVIRONMENT_TYPE": "local"
15
+ }
16
+ }
@@ -0,0 +1,37 @@
1
+ <?php
2
+
3
+ function {{phpPrefix}}_get_rest_nonce( WP_REST_Request $request ) {
4
+ $header_nonce = $request->get_header( 'X-WP-Nonce' );
5
+ if ( is_string( $header_nonce ) && '' !== $header_nonce ) {
6
+ return $header_nonce;
7
+ }
8
+
9
+ $query_nonce = $request->get_param( '_wpnonce' );
10
+ if ( is_string( $query_nonce ) && '' !== $query_nonce ) {
11
+ return $query_nonce;
12
+ }
13
+
14
+ return null;
15
+ }
16
+
17
+ // Customize authenticated write policy here when your project needs stricter capability checks.
18
+ function {{phpPrefix}}_can_write_authenticated( WP_REST_Request $request ) {
19
+ if ( ! is_user_logged_in() ) {
20
+ return new WP_Error(
21
+ 'rest_forbidden',
22
+ 'Authentication is required to update this counter.',
23
+ array( 'status' => rest_authorization_required_code() )
24
+ );
25
+ }
26
+
27
+ $nonce = {{phpPrefix}}_get_rest_nonce( $request );
28
+ if ( ! is_string( $nonce ) || '' === $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
29
+ return new WP_Error(
30
+ 'rest_forbidden',
31
+ 'The REST nonce is missing or invalid.',
32
+ array( 'status' => 403 )
33
+ );
34
+ }
35
+
36
+ return true;
37
+ }
@@ -0,0 +1,314 @@
1
+ <?php
2
+
3
+ function {{phpPrefix}}_base64url_encode( $value ) {
4
+ return rtrim( strtr( base64_encode( $value ), '+/', '-_' ), '=' );
5
+ }
6
+
7
+ function {{phpPrefix}}_base64url_decode( $value ) {
8
+ if ( ! is_string( $value ) || '' === $value ) {
9
+ return false;
10
+ }
11
+
12
+ $padding = strlen( $value ) % 4;
13
+ if ( $padding > 0 ) {
14
+ $value .= str_repeat( '=', 4 - $padding );
15
+ }
16
+
17
+ return base64_decode( strtr( $value, '-_', '+/' ), true );
18
+ }
19
+
20
+ function {{phpPrefix}}_get_public_write_action() {
21
+ return '{{namespace}}/{{slugKebabCase}}/state/write';
22
+ }
23
+
24
+ function {{phpPrefix}}_get_public_write_client_subject() {
25
+ $remote_addr = isset( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] )
26
+ ? wp_unslash( $_SERVER['REMOTE_ADDR'] )
27
+ : '';
28
+ $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) && is_string( $_SERVER['HTTP_USER_AGENT'] )
29
+ ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] )
30
+ : '';
31
+
32
+ return md5( $remote_addr . '|' . $user_agent );
33
+ }
34
+
35
+ function {{phpPrefix}}_get_public_write_rate_limit_key( $post_id, $resource_key ) {
36
+ return '{{phpPrefix}}_public_write_rl_' . (int) $post_id . '_' . md5(
37
+ (string) $resource_key . '|' . {{phpPrefix}}_get_public_write_client_subject()
38
+ );
39
+ }
40
+
41
+ function {{phpPrefix}}_get_public_write_request_replay_key( $post_id, $resource_key, $request_id ) {
42
+ return '{{phpPrefix}}_public_write_req_' . (int) $post_id . '_' . md5(
43
+ (string) $resource_key . '|' . (string) $request_id
44
+ );
45
+ }
46
+
47
+ function {{phpPrefix}}_get_public_write_lock_key( $post_id, $resource_key, $scope, $lock_subject = '' ) {
48
+ $lock_subject = is_string( $lock_subject ) && '' !== $lock_subject
49
+ ? $lock_subject
50
+ : {{phpPrefix}}_get_public_write_client_subject();
51
+
52
+ return 'wpt_pwl_' . md5(
53
+ '{{phpPrefix}}|' . (string) $scope . '|' . (int) $post_id . '|' . (string) $resource_key . '|' . $lock_subject
54
+ );
55
+ }
56
+
57
+ function {{phpPrefix}}_with_public_write_lock( $post_id, $resource_key, $scope, $callback, $lock_subject = '' ) {
58
+ global $wpdb;
59
+
60
+ $lock_key = {{phpPrefix}}_get_public_write_lock_key( $post_id, $resource_key, $scope, $lock_subject );
61
+ $acquired = (int) $wpdb->get_var(
62
+ $wpdb->prepare(
63
+ 'SELECT GET_LOCK(%s, 5)',
64
+ $lock_key
65
+ )
66
+ );
67
+
68
+ if ( 1 === $acquired ) {
69
+ try {
70
+ return $callback();
71
+ } finally {
72
+ $wpdb->get_var(
73
+ $wpdb->prepare(
74
+ 'SELECT RELEASE_LOCK(%s)',
75
+ $lock_key
76
+ )
77
+ );
78
+ }
79
+ }
80
+
81
+ return new WP_Error(
82
+ 'rest_temporarily_unavailable',
83
+ 'Could not acquire the public write lock.',
84
+ array( 'status' => 503 )
85
+ );
86
+ }
87
+
88
+ function {{phpPrefix}}_enforce_public_write_rate_limit( $post_id, $resource_key ) {
89
+ return {{phpPrefix}}_with_public_write_lock(
90
+ $post_id,
91
+ $resource_key,
92
+ 'rate_limit',
93
+ function () use ( $post_id, $resource_key ) {
94
+ $key = {{phpPrefix}}_get_public_write_rate_limit_key( $post_id, $resource_key );
95
+ $count = (int) get_transient( $key );
96
+
97
+ if ( $count >= (int) {{phpPrefixUpper}}_PUBLIC_WRITE_RATE_LIMIT_MAX ) {
98
+ return new WP_Error(
99
+ 'rest_rate_limited',
100
+ 'Too many public write attempts. Wait a minute and try again.',
101
+ array( 'status' => 429 )
102
+ );
103
+ }
104
+
105
+ set_transient(
106
+ $key,
107
+ $count + 1,
108
+ (int) {{phpPrefixUpper}}_PUBLIC_WRITE_RATE_LIMIT_WINDOW
109
+ );
110
+
111
+ return true;
112
+ }
113
+ );
114
+ }
115
+
116
+ function {{phpPrefix}}_consume_public_write_request_id( $post_id, $resource_key, $request_id ) {
117
+ if ( ! is_string( $request_id ) || '' === $request_id ) {
118
+ return new WP_Error(
119
+ 'rest_forbidden',
120
+ 'The public write request id is missing.',
121
+ array( 'status' => 403 )
122
+ );
123
+ }
124
+
125
+ return {{phpPrefix}}_with_public_write_lock(
126
+ $post_id,
127
+ $resource_key,
128
+ 'replay',
129
+ function () use ( $post_id, $resource_key, $request_id ) {
130
+ $key = {{phpPrefix}}_get_public_write_request_replay_key( $post_id, $resource_key, $request_id );
131
+ if ( false !== get_transient( $key ) ) {
132
+ return new WP_Error(
133
+ 'rest_conflict',
134
+ 'This public write request was already processed.',
135
+ array( 'status' => 409 )
136
+ );
137
+ }
138
+
139
+ set_transient( $key, 1, (int) {{phpPrefixUpper}}_PUBLIC_WRITE_TTL );
140
+
141
+ return true;
142
+ },
143
+ $request_id
144
+ );
145
+ }
146
+
147
+ function {{phpPrefix}}_release_public_write_request_id( $post_id, $resource_key, $request_id ) {
148
+ if ( ! is_string( $request_id ) || '' === $request_id ) {
149
+ return;
150
+ }
151
+
152
+ delete_transient( {{phpPrefix}}_get_public_write_request_replay_key( $post_id, $resource_key, $request_id ) );
153
+ }
154
+
155
+ // Public write tokens are scaffold-owned by default; customize this flow if you need a different anonymous write policy.
156
+ function {{phpPrefix}}_create_public_write_token( $post_id, $resource_key ) {
157
+ $expires_at = time() + (int) {{phpPrefixUpper}}_PUBLIC_WRITE_TTL;
158
+ $payload = array(
159
+ 'action' => {{phpPrefix}}_get_public_write_action(),
160
+ 'exp' => $expires_at,
161
+ 'postId' => (int) $post_id,
162
+ 'resourceKey' => (string) $resource_key,
163
+ );
164
+ $json = wp_json_encode( $payload );
165
+
166
+ if ( ! is_string( $json ) || '' === $json ) {
167
+ return array(
168
+ 'expiresAt' => $expires_at,
169
+ 'token' => '',
170
+ );
171
+ }
172
+
173
+ $payload_segment = {{phpPrefix}}_base64url_encode( $json );
174
+ $signature_segment = {{phpPrefix}}_base64url_encode(
175
+ hash_hmac( 'sha256', $payload_segment, wp_salt( 'nonce' ), true )
176
+ );
177
+
178
+ return array(
179
+ 'expiresAt' => $expires_at,
180
+ 'token' => $payload_segment . '.' . $signature_segment,
181
+ );
182
+ }
183
+
184
+ function {{phpPrefix}}_verify_public_write_token( $token, $post_id, $resource_key ) {
185
+ if ( ! is_string( $token ) || '' === $token ) {
186
+ return new WP_Error(
187
+ 'rest_forbidden',
188
+ 'The public write token is missing.',
189
+ array( 'status' => 403 )
190
+ );
191
+ }
192
+
193
+ $segments = explode( '.', $token );
194
+ if ( 2 !== count( $segments ) ) {
195
+ return new WP_Error(
196
+ 'rest_forbidden',
197
+ 'The public write token format is invalid.',
198
+ array( 'status' => 403 )
199
+ );
200
+ }
201
+
202
+ list( $payload_segment, $signature_segment ) = $segments;
203
+ $expected_signature = {{phpPrefix}}_base64url_encode(
204
+ hash_hmac( 'sha256', $payload_segment, wp_salt( 'nonce' ), true )
205
+ );
206
+
207
+ if ( ! hash_equals( $expected_signature, $signature_segment ) ) {
208
+ return new WP_Error(
209
+ 'rest_forbidden',
210
+ 'The public write token signature is invalid.',
211
+ array( 'status' => 403 )
212
+ );
213
+ }
214
+
215
+ $payload_json = {{phpPrefix}}_base64url_decode( $payload_segment );
216
+ if ( false === $payload_json ) {
217
+ return new WP_Error(
218
+ 'rest_forbidden',
219
+ 'The public write token payload is invalid.',
220
+ array( 'status' => 403 )
221
+ );
222
+ }
223
+
224
+ $payload = json_decode( $payload_json, true );
225
+ if ( ! is_array( $payload ) ) {
226
+ return new WP_Error(
227
+ 'rest_forbidden',
228
+ 'The public write token payload is invalid.',
229
+ array( 'status' => 403 )
230
+ );
231
+ }
232
+
233
+ $expires_at = isset( $payload['exp'] ) ? (int) $payload['exp'] : 0;
234
+ if ( time() > $expires_at ) {
235
+ return new WP_Error(
236
+ 'rest_forbidden',
237
+ 'The public write token has expired. Reload the page and try again.',
238
+ array( 'status' => 403 )
239
+ );
240
+ }
241
+
242
+ if ( {{phpPrefix}}_get_public_write_action() !== ( isset( $payload['action'] ) ? (string) $payload['action'] : '' ) ) {
243
+ return new WP_Error(
244
+ 'rest_forbidden',
245
+ 'The public write token action is invalid.',
246
+ array( 'status' => 403 )
247
+ );
248
+ }
249
+
250
+ if ( (int) ( isset( $payload['postId'] ) ? $payload['postId'] : 0 ) !== (int) $post_id ) {
251
+ return new WP_Error(
252
+ 'rest_forbidden',
253
+ 'The public write token is not valid for this post.',
254
+ array( 'status' => 403 )
255
+ );
256
+ }
257
+
258
+ if ( (string) ( isset( $payload['resourceKey'] ) ? $payload['resourceKey'] : '' ) !== (string) $resource_key ) {
259
+ return new WP_Error(
260
+ 'rest_forbidden',
261
+ 'The public write token is not valid for this resource key.',
262
+ array( 'status' => 403 )
263
+ );
264
+ }
265
+
266
+ return true;
267
+ }
268
+
269
+ // Customize the public write gate here when you need different anonymous request checks.
270
+ function {{phpPrefix}}_can_write_publicly( WP_REST_Request $request ) {
271
+ $payload = $request->get_json_params();
272
+ if ( ! is_array( $payload ) ) {
273
+ $payload = array();
274
+ }
275
+
276
+ $post_id = isset( $payload['postId'] ) ? (int) $payload['postId'] : 0;
277
+ $resource_key = isset( $payload['resourceKey'] ) ? (string) $payload['resourceKey'] : '';
278
+ $request_id = isset( $payload['publicWriteRequestId'] ) ? (string) $payload['publicWriteRequestId'] : '';
279
+ $token = isset( $payload['publicWriteToken'] ) ? (string) $payload['publicWriteToken'] : '';
280
+
281
+ if ( '' === $token ) {
282
+ $fallback = $request->get_param( 'publicWriteToken' );
283
+ $token = is_string( $fallback ) ? $fallback : '';
284
+ }
285
+
286
+ if ( '' === $request_id ) {
287
+ $fallback = $request->get_param( 'publicWriteRequestId' );
288
+ $request_id = is_string( $fallback ) ? $fallback : '';
289
+ }
290
+
291
+ if ( $post_id <= 0 || '' === $resource_key ) {
292
+ return new WP_Error(
293
+ 'rest_forbidden',
294
+ 'The public write request is missing its target identifiers.',
295
+ array( 'status' => 403 )
296
+ );
297
+ }
298
+
299
+ if ( '' === $request_id ) {
300
+ return new WP_Error(
301
+ 'rest_forbidden',
302
+ 'The public write request id is missing.',
303
+ array( 'status' => 403 )
304
+ );
305
+ }
306
+
307
+ $verification = {{phpPrefix}}_verify_public_write_token( $token, $post_id, $resource_key );
308
+ if ( is_wp_error( $verification ) ) {
309
+ return $verification;
310
+ }
311
+
312
+ // Customize this subject or limit strategy if your public write surface needs stronger abuse controls.
313
+ return {{phpPrefix}}_enforce_public_write_rate_limit( $post_id, $resource_key );
314
+ }
@@ -0,0 +1,58 @@
1
+ <?php
2
+
3
+ // Scaffold-owned schema plumbing. Extend request/response behavior in the route handlers instead.
4
+ function {{phpPrefix}}_load_schema_from_build_dir( $build_dir, $schema_name ) {
5
+ if ( ! is_string( $build_dir ) || '' === $build_dir ) {
6
+ return null;
7
+ }
8
+
9
+ $path = $build_dir . '/api-schemas/' . $schema_name . '.schema.json';
10
+ if ( ! is_readable( $path ) ) {
11
+ return null;
12
+ }
13
+
14
+ $contents = file_get_contents( $path );
15
+ if ( false === $contents ) {
16
+ return null;
17
+ }
18
+
19
+ $decoded = json_decode( $contents, true );
20
+ return is_array( $decoded ) ? $decoded : null;
21
+ }
22
+
23
+ // Keep schema sanitation aligned with generated JSON Schema unless you are debugging bridge behavior.
24
+ function {{phpPrefix}}_sanitize_rest_schema( $schema ) {
25
+ if ( ! is_array( $schema ) ) {
26
+ return $schema;
27
+ }
28
+
29
+ unset( $schema['$schema'], $schema['title'] );
30
+
31
+ if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
32
+ foreach ( $schema['properties'] as $key => $property_schema ) {
33
+ $schema['properties'][ $key ] = {{phpPrefix}}_sanitize_rest_schema( $property_schema );
34
+ }
35
+ }
36
+
37
+ if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
38
+ $schema['items'] = {{phpPrefix}}_sanitize_rest_schema( $schema['items'] );
39
+ }
40
+
41
+ return $schema;
42
+ }
43
+
44
+ // Route handlers call this bridge before product-specific logic runs.
45
+ function {{phpPrefix}}_validate_and_sanitize_request( $value, $build_dir, $schema_name, $param_name ) {
46
+ $schema = {{phpPrefix}}_load_schema_from_build_dir( $build_dir, $schema_name );
47
+ if ( ! is_array( $schema ) ) {
48
+ return new WP_Error( 'missing_schema', 'Missing REST schema.', array( 'status' => 500 ) );
49
+ }
50
+
51
+ $rest_schema = {{phpPrefix}}_sanitize_rest_schema( $schema );
52
+ $validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
53
+ if ( is_wp_error( $validation ) ) {
54
+ return $validation;
55
+ }
56
+
57
+ return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
58
+ }
@@ -0,0 +1,36 @@
1
+ <?php
2
+
3
+ function {{phpPrefix}}_get_rest_nonce( WP_REST_Request $request ) {
4
+ $header_nonce = $request->get_header( 'X-WP-Nonce' );
5
+ if ( is_string( $header_nonce ) && '' !== $header_nonce ) {
6
+ return $header_nonce;
7
+ }
8
+
9
+ $query_nonce = $request->get_param( '_wpnonce' );
10
+ if ( is_string( $query_nonce ) && '' !== $query_nonce ) {
11
+ return $query_nonce;
12
+ }
13
+
14
+ return null;
15
+ }
16
+
17
+ function {{phpPrefix}}_can_write_authenticated( WP_REST_Request $request ) {
18
+ if ( ! is_user_logged_in() ) {
19
+ return new WP_Error(
20
+ 'rest_forbidden',
21
+ 'Authentication is required to update this block state.',
22
+ array( 'status' => rest_authorization_required_code() )
23
+ );
24
+ }
25
+
26
+ $nonce = {{phpPrefix}}_get_rest_nonce( $request );
27
+ if ( ! is_string( $nonce ) || '' === $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
28
+ return new WP_Error(
29
+ 'rest_forbidden',
30
+ 'The REST nonce is missing or invalid.',
31
+ array( 'status' => 403 )
32
+ );
33
+ }
34
+
35
+ return true;
36
+ }
@@ -0,0 +1,55 @@
1
+ <?php
2
+
3
+ function {{phpPrefix}}_load_schema_from_build_dir( $build_dir, $schema_name ) {
4
+ if ( ! is_string( $build_dir ) || '' === $build_dir ) {
5
+ return null;
6
+ }
7
+
8
+ $path = $build_dir . '/api-schemas/' . $schema_name . '.schema.json';
9
+ if ( ! is_readable( $path ) ) {
10
+ return null;
11
+ }
12
+
13
+ $contents = file_get_contents( $path );
14
+ if ( false === $contents ) {
15
+ return null;
16
+ }
17
+
18
+ $decoded = json_decode( $contents, true );
19
+ return is_array( $decoded ) ? $decoded : null;
20
+ }
21
+
22
+ function {{phpPrefix}}_sanitize_rest_schema( $schema ) {
23
+ if ( ! is_array( $schema ) ) {
24
+ return $schema;
25
+ }
26
+
27
+ unset( $schema['$schema'], $schema['title'] );
28
+
29
+ if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
30
+ foreach ( $schema['properties'] as $key => $property_schema ) {
31
+ $schema['properties'][ $key ] = {{phpPrefix}}_sanitize_rest_schema( $property_schema );
32
+ }
33
+ }
34
+
35
+ if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
36
+ $schema['items'] = {{phpPrefix}}_sanitize_rest_schema( $schema['items'] );
37
+ }
38
+
39
+ return $schema;
40
+ }
41
+
42
+ function {{phpPrefix}}_validate_and_sanitize_request( $value, $build_dir, $schema_name, $param_name ) {
43
+ $schema = {{phpPrefix}}_load_schema_from_build_dir( $build_dir, $schema_name );
44
+ if ( ! is_array( $schema ) ) {
45
+ return new WP_Error( 'missing_schema', 'Missing REST schema.', array( 'status' => 500 ) );
46
+ }
47
+
48
+ $rest_schema = {{phpPrefix}}_sanitize_rest_schema( $schema );
49
+ $validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
50
+ if ( is_wp_error( $validation ) ) {
51
+ return $validation;
52
+ }
53
+
54
+ return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
55
+ }