npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@marvalt/wadapter

v2.3.48

Published

WordPress and Gravity Forms integration package for React applications with static data generation

Readme

@marvalt/wadapter

Static-first WordPress and Gravity Forms integration package for React applications. Provides build-time static data generation, runtime static data access, and secure form submission capabilities with Cloudflare Turnstile bot protection.

Table of Contents

Overview

@marvalt/wadapter provides a complete solution for integrating WordPress and Gravity Forms into React applications with a static-first architecture:

  • Build-time generators: Fetch WordPress content and Gravity Forms schemas at build time, outputting static JSON files
  • Runtime static access: Read-only access to pre-generated static data (no runtime API calls)
  • Secure form submissions: Server-side proxy with multiple security layers including Turnstile bot protection
  • React components: Ready-to-use components for rendering WordPress content and Gravity Forms
  • TypeScript support: Full type definitions for all data structures

Package Structure

The package has three distinct entry points to separate browser-safe code from Node.js-only code:

  1. @marvalt/wadapter (Main browser bundle)

    • React components (GravityForm, WordPressContent, TurnstileWidget)
    • React hooks (useWordPress, useGravityForms)
    • API clients (WordPressClient, GravityFormsClient)
    • Static data loaders and selectors
    • Types and utilities
    • No Node.js dependencies (browser-safe)
  2. @marvalt/wadapter/generators (Node.js-only bundle)

    • generateWordPressData() - Build-time WordPress data generation
    • generateGravityFormsData() - Build-time Gravity Forms schema generation
    • generateFormProtectionData() - Build-time form protection data
    • Uses fs, path, and other Node.js modules
    • Only for build scripts and SSG
  3. @marvalt/wadapter/server (Cloudflare Pages Functions)

    • handleGravityFormsProxy() - Server-side proxy handler
    • verifyTurnstile() - Server-side Turnstile verification
    • Only for serverless functions

Installation

npm install @marvalt/wadapter

Peer Dependencies

The package requires React (16.8.0+) and React DOM (16.8.0+). These should already be installed in your React project.

WordPress Plugin Requirements

Required Plugins

  1. Gravity Forms (Premium or Basic)

    • Must be installed and activated
    • Required for form functionality
  2. Gravity Forms API Endpoint (Custom Plugin)

    • Required for proper form submissions with notification triggering
    • Provides enhanced REST API endpoints at /wp-json/gf-api/v1/
    • Solves the critical issue where standard Gravity Forms REST API submissions don't trigger email notifications
    • Installation:
      # Clone or download the plugin
      git clone https://github.com/ViBuNe-Pty-Ltd/gravity-forms-api-endpoint.git wp-content/plugins/gravity-forms-api-endpoint
    • Activate the plugin in WordPress admin: Plugins → Installed Plugins
    • Verify installation: Visit https://your-wordpress-site.com/wp-json/gf-api/v1/health

Why the Custom Plugin is Required

The standard Gravity Forms REST API (/wp-json/gf/v2/forms/{id}/submissions) creates entries but does not trigger notifications. The custom plugin provides /wp-json/gf-api/v1/forms/{id}/submit which:

  • ✅ Creates entries AND triggers notifications
  • ✅ Fires all Gravity Forms hooks
  • ✅ Returns proper confirmation messages
  • ✅ Integrates with webhook automation for static data regeneration

Environment Configuration

Build-Time Variables (.env or .env.local)

These variables are used during build-time data generation and may be exposed to the browser (use .env for non-secrets, .env.local for secrets):

# Authentication Mode
VITE_AUTH_MODE=direct  # or 'cloudflare_proxy'

# WordPress Configuration
VITE_WORDPRESS_API_URL=https://your-wordpress-site.com

# WordPress Credentials (for build-time generation only - never exposed to browser)
VITE_WP_API_USERNAME=your-username
VITE_WP_APP_PASSWORD=your-app-password

# Gravity Forms Configuration
VITE_GRAVITY_FORMS_API_URL=https://your-wordpress-site.com/wp-json/gf-api/v1
# Or leave empty to auto-construct from VITE_WORDPRESS_API_URL

# Cloudflare Access (if WordPress is behind Cloudflare Access)
VITE_CF_ACCESS_CLIENT_ID=your-client-id
VITE_CF_ACCESS_CLIENT_SECRET=your-client-secret

# Turnstile Bot Protection (optional)
VITE_TURNSTILE_SITE_KEY=your-site-key

# WordPress Data Generation Options
VITE_ENABLED_POST_TYPES=posts,pages,chapter_member  # Comma-separated list
VITE_DEFAULT_MAX_ITEMS=100  # Max items per post type

Cloudflare Pages Environment Variables

These are set in the Cloudflare Pages dashboard (Settings → Environment Variables) and are server-side only:

# WordPress Credentials (for Pages Function proxy)
VITE_WORDPRESS_API_URL=https://your-wordpress-site.com
VITE_WP_API_USERNAME=your-username
VITE_WP_APP_PASSWORD=your-app-password

# Origin Validation (REQUIRED for production)
ALLOWED_ORIGINS=https://yourdomain.com,https://preview.yourdomain.com

# Turnstile Verification (optional but recommended)
TURNSTILE_SECRET_KEY=your-secret-key

# Cloudflare Access (if WordPress is behind Cloudflare Access)
VITE_CF_ACCESS_CLIENT_ID=your-client-id
VITE_CF_ACCESS_CLIENT_SECRET=your-client-secret

Important Notes:

  • ALLOWED_ORIGINS is required for the proxy to work in production. If not set, only localhost:8080 and localhost:5173 are allowed.
  • TURNSTILE_SECRET_KEY is optional. If set, Turnstile verification will be enforced for form submissions.
  • VITE_TURNSTILE_SITE_KEY must be set in both build-time and runtime environments for Turnstile to work.

Build-Time Data Generation

Setup Generation Scripts

Create scripts to generate static data at build time. These scripts use the /generators export which requires Node.js.

Example: scripts/generateWordPressData.ts

import dotenv from 'dotenv';
import { generateWordPressData } from '@marvalt/wadapter/generators';

dotenv.config({ path: '.env' });
dotenv.config({ path: '.env.local' });

async function run() {
  const config = {
    authMode: 'direct' as const,
    apiUrl: process.env.VITE_WORDPRESS_API_URL,
    username: process.env.VITE_WP_API_USERNAME,
    password: process.env.VITE_WP_APP_PASSWORD,
    cfAccessClientId: process.env.VITE_CF_ACCESS_CLIENT_ID,
    cfAccessClientSecret: process.env.VITE_CF_ACCESS_CLIENT_SECRET,
  };

  if (!config.apiUrl || !config.username || !config.password) {
    console.error('❌ Missing required environment variables');
    process.exit(1);
  }

  const postTypes = process.env.VITE_ENABLED_POST_TYPES
    ? process.env.VITE_ENABLED_POST_TYPES.split(',').map(t => t.trim())
    : ['posts', 'pages'];

  const maxItems = parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '100', 10);

  await generateWordPressData({
    ...config,
    outputPath: './public/wordpress-data.json',
    postTypes,
    includeEmbedded: true, // Always true for full functionality
    maxItems,
  });

  console.log('✅ WordPress data generation completed');
}

run();

Example: scripts/generateGravityFormsData.ts

import dotenv from 'dotenv';
import { generateGravityFormsData } from '@marvalt/wadapter/generators';

dotenv.config({ path: '.env' });
dotenv.config({ path: '.env.local' });

async function run() {
  const config = {
    authMode: 'direct' as const,
    apiUrl: process.env.VITE_GRAVITY_FORMS_API_URL || 
            `${process.env.VITE_WORDPRESS_API_URL}/wp-json/gf-api/v1`,
    username: process.env.VITE_WP_API_USERNAME || '',
    password: process.env.VITE_WP_APP_PASSWORD || '',
    useCustomEndpoint: true, // Force use of gf-api/v1
    cfAccessClientId: process.env.VITE_CF_ACCESS_CLIENT_ID,
    cfAccessClientSecret: process.env.VITE_CF_ACCESS_CLIENT_SECRET,
  };

  if (!config.apiUrl || !config.username || !config.password) {
    console.error('❌ Missing required environment variables');
    process.exit(1);
  }

  await generateGravityFormsData({
    ...config,
    outputPath: './public/gravityForms.json',
    includeInactive: false,
  });

  console.log('✅ Gravity Forms data generation completed');
}

run();

Integration with Build Process

Add to your package.json:

{
  "scripts": {
    "generate:wp": "tsx scripts/generateWordPressData.ts",
    "generate:gf": "tsx scripts/generateGravityFormsData.ts",
    "generate:all": "npm run generate:wp && npm run generate:gf",
    "build": "npm run generate:all && vite build"
  }
}

Generated Files

The generators create static JSON files in your public directory:

  • public/wordpress-data.json - WordPress posts, pages, media, categories, tags, and custom post types
  • public/gravityForms.json - Gravity Forms schemas with fields, notifications, and confirmations

WordPress Data Structure:

  • Pages include parsed Gutenberg blocks (content.raw is parsed and stored as blocks)
  • Posts use rendered HTML (content.rendered)
  • Embedded data (featured media, terms, etc.) is included when includeEmbedded: true

Runtime Usage

Static Data Access

Load and query static data at runtime (no API calls):

import { 
  loadWordPressData, 
  getWordPressPosts, 
  getWordPressPages,
  getWordPressMedia 
} from '@marvalt/wadapter';

import {
  loadGravityFormsData,
  getActiveForms,
  getFormById
} from '@marvalt/wadapter';

// Load data (typically done once at app startup)
await loadWordPressData();
await loadGravityFormsData();

// Query data
const posts = getWordPressPosts();
const pages = getWordPressPages();
const media = getWordPressMedia();
const forms = getActiveForms();
const form = getFormById(1);

React Hooks

Use React hooks for convenient data access:

import { useWordPress, useGravityForms } from '@marvalt/wadapter';

function MyComponent() {
  const { posts, pages, media, loading } = useWordPress();
  const { form, loading: formLoading, submitForm } = useGravityForms(1, {
    apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
    authMode: 'direct',
  });

  // Use the data...
}

React Components

WordPressContent Component

Render WordPress content with automatic HTML sanitization and responsive images:

import { WordPressContent } from '@marvalt/wadapter';

function PostPage({ post }) {
  return (
    <WordPressContent
      content={post.content.rendered}
      className="prose prose-lg"
    />
  );
}

GravityForm Component

Render and handle Gravity Forms with automatic Turnstile integration:

import { GravityForm } from '@marvalt/wadapter';

function ContactForm() {
  return (
    <GravityForm
      formId={1}
      config={{
        apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
        authMode: 'direct', // Uses Cloudflare Pages Function proxy
      }}
      onSubmit={(result) => {
        console.log('Success:', result);
        // result.confirmation contains confirmation message
      }}
      onError={(error) => {
        console.error('Error:', error);
      }}
    />
  );
}

Features:

  • Automatic field rendering based on form schema
  • Client-side validation
  • Conditional logic support
  • Turnstile bot protection (automatic if VITE_TURNSTILE_SITE_KEY is set)
  • Submit button disabled until Turnstile verification completes (if enabled)

TurnstileWidget Component

Standalone Turnstile widget for custom implementations:

import { TurnstileWidget } from '@marvalt/wadapter';

function CustomForm() {
  const [token, setToken] = useState<string | null>(null);

  return (
    <>
      <TurnstileWidget
        onVerify={(token) => setToken(token)}
        onError={() => setToken(null)}
      />
      <button disabled={!token}>Submit</button>
    </>
  );
}

API Clients

For programmatic access without React:

import { GravityFormsClient } from '@marvalt/wadapter';

const client = new GravityFormsClient({
  apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
  authMode: 'direct',
});

const result = await client.submitForm({
  form_id: 1,
  field_values: {
    '1': 'John Doe',
    '2': '[email protected]',
  },
});

Cloudflare Setup

Cloudflare Pages Function

The package automatically installs a Cloudflare Pages Function template during npm install via a postinstall script. The function is located at:

functions/api/gravity-forms-submit.ts

If the file doesn't exist, create it manually:

import { handleGravityFormsProxy } from '@marvalt/wadapter/server';

export const onRequest = handleGravityFormsProxy;

How the Proxy Works

┌─────────┐
│ Browser │ (Untrusted)
└────┬────┘
     │ HTTPS + Turnstile Token (optional)
     ▼
┌─────────────────────┐
│ Pages Function      │ ← Security Checkpoint
│ /api/gf-submit      │   • Origin validation
│                     │   • Turnstile verification (optional)
│                     │   • Endpoint whitelist
│                     │   • Basic Auth injection
│                     │   • CF Access injection (optional)
└────┬────────────────┘
     │ Authenticated Request
     ▼
┌─────────────────────┐
│ WordPress           │ (Trusted)
│ /wp-json/gf-api/v1  │ • No auth needed from client
│ (Custom Plugin)     │ • Processes submission
│                     │ • Triggers notifications
│                     │ • Returns confirmation
└─────────────────────┘

Security Layers

The proxy implements five security layers:

  1. Origin Validation - Only authorized domains can submit (via ALLOWED_ORIGINS)
  2. Endpoint Whitelisting - Only /forms/{id}/submit endpoints allowed
  3. Turnstile Verification - Bot protection (optional, requires TURNSTILE_SECRET_KEY)
  4. Server-Side Auth - WordPress credentials never exposed to browser
  5. CF Access Support - Optional additional security layer for WordPress behind Cloudflare Access

Cloudflare Access (Optional)

If your WordPress site is behind Cloudflare Access (Zero Trust), you need to:

  1. Create a Cloudflare Access Service Token
  2. Set VITE_CF_ACCESS_CLIENT_ID and VITE_CF_ACCESS_CLIENT_SECRET in both:
    • Build-time environment (for data generation)
    • Cloudflare Pages environment (for proxy)

The proxy will automatically inject CF Access headers when these credentials are present.

Security

Authentication Modes

Direct Mode (VITE_AUTH_MODE=direct)

  • Build-time: Uses WordPress Basic Auth directly (credentials in .env.local)
  • Runtime: Uses Cloudflare Pages Function proxy (credentials in Cloudflare Pages environment)
  • WordPress: Uses standard REST API endpoints
  • Gravity Forms: Uses custom gf-api/v1 endpoints (requires custom plugin)

Cloudflare Proxy Mode (VITE_AUTH_MODE=cloudflare_proxy)

  • Build-time: Uses WordPress Basic Auth directly
  • Runtime: Uses Cloudflare Worker with ?endpoint= query parameter
  • WordPress: Worker injects CF Access credentials
  • Gravity Forms: Worker routes to gf-api/v1 endpoints

Note: Most implementations use direct mode with Cloudflare Pages Functions, which provides the same security benefits without requiring a separate Worker.

Best Practices

  1. Never expose credentials to the browser: All WordPress credentials should be in .env.local (not committed) or Cloudflare Pages environment variables
  2. Always use the proxy for form submissions: Never call WordPress API directly from the browser
  3. Enable Turnstile: Set VITE_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY for bot protection
  4. Set ALLOWED_ORIGINS: Required for production deployments
  5. Use HTTPS: Always use HTTPS in production

API Reference

Generators (@marvalt/wadapter/generators)

generateWordPressData(config)

Generates static WordPress data.

Parameters:

  • config.apiUrl (string, required) - WordPress API URL
  • config.username (string, required) - WordPress username
  • config.password (string, required) - WordPress app password
  • config.outputPath (string, required) - Output file path
  • config.postTypes (string[], optional) - Post types to fetch (default: ['posts', 'pages'])
  • config.includeEmbedded (boolean, optional) - Include embedded data (default: true)
  • config.maxItems (number, optional) - Max items per post type (default: 100)
  • config.cfAccessClientId (string, optional) - CF Access client ID
  • config.cfAccessClientSecret (string, optional) - CF Access client secret

Returns: Promise<WordPressStaticData>

generateGravityFormsData(config)

Generates static Gravity Forms data.

Parameters:

  • config.apiUrl (string, required) - Gravity Forms API URL (should use gf-api/v1)
  • config.username (string, required) - WordPress username
  • config.password (string, required) - WordPress app password
  • config.outputPath (string, required) - Output file path
  • config.useCustomEndpoint (boolean, optional) - Use custom gf-api/v1 endpoint (default: true)
  • config.includeInactive (boolean, optional) - Include inactive forms (default: false)
  • config.cfAccessClientId (string, optional) - CF Access client ID
  • config.cfAccessClientSecret (string, optional) - CF Access client secret

Returns: Promise<GravityFormsStaticBundle>

Server Functions (@marvalt/wadapter/server)

handleGravityFormsProxy(context)

Cloudflare Pages Function handler for Gravity Forms proxy.

Usage:

import { handleGravityFormsProxy } from '@marvalt/wadapter/server';

export const onRequest = handleGravityFormsProxy;

Required Environment Variables:

  • VITE_WORDPRESS_API_URL or WORDPRESS_API_URL
  • VITE_WP_API_USERNAME or WP_API_USERNAME
  • VITE_WP_APP_PASSWORD or WP_APP_PASSWORD
  • ALLOWED_ORIGINS (required for production)

Optional Environment Variables:

  • TURNSTILE_SECRET_KEY or VITE_TURNSTILE_SECRET_KEY
  • VITE_CF_ACCESS_CLIENT_ID or CF_ACCESS_CLIENT_ID
  • VITE_CF_ACCESS_CLIENT_SECRET or CF_ACCESS_CLIENT_SECRET

React Components

<GravityForm />

Props:

  • formId (number, required) - Gravity Forms form ID
  • config (GravityFormsConfig, required) - Configuration object
  • className (string, optional) - Additional CSS classes
  • onSubmit (function, optional) - Success callback
  • onError (function, optional) - Error callback

<WordPressContent />

Props:

  • content (string, required) - WordPress HTML content
  • className (string, optional) - Additional CSS classes

<TurnstileWidget />

Props:

  • onVerify (function, required) - Called with token when verified
  • onError (function, optional) - Called on verification error

React Hooks

useWordPress()

Returns WordPress static data.

Returns:

{
  posts: WordPressPost[];
  pages: WordPressPage[];
  media: WordPressMedia[];
  categories: WordPressCategory[];
  tags: WordPressTag[];
  loading: boolean;
  error: Error | null;
}

useGravityForms(formId, config)

Returns Gravity Forms data and submission function.

Parameters:

  • formId (number, required) - Form ID
  • config (GravityFormsConfig, required) - Configuration

Returns:

{
  form: GravityForm | null;
  loading: boolean;
  error: Error | null;
  submitting: boolean;
  result: any | null;
  submitForm: (data: GravityFormSubmission) => Promise<any>;
}

Examples

Complete Setup Example

See the bnibrilliance-website implementation for a complete, production-ready example:

  • Build scripts: scripts/generateWordPressData.ts, scripts/generateGravityFormsData.ts
  • Cloudflare Pages Function: functions/api/gravity-forms-submit.ts
  • React components using the package
  • Environment variable configuration

Basic Form Implementation

import { GravityForm } from '@marvalt/wadapter';

function ContactPage() {
  return (
    <div>
      <h1>Contact Us</h1>
      <GravityForm
        formId={1}
        config={{
          apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
          authMode: 'direct',
        }}
        onSubmit={(result) => {
          alert(result.confirmation?.message || 'Thank you!');
        }}
        onError={(error) => {
          alert('Error: ' + error.message);
        }}
      />
    </div>
  );
}

Custom Form with Manual Submission

import { useGravityForms } from '@marvalt/wadapter';

function CustomForm() {
  const { form, loading, submitForm } = useGravityForms(1, {
    apiUrl: import.meta.env.VITE_WORDPRESS_API_URL,
    authMode: 'direct',
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.target as HTMLFormElement);
    
    const fieldValues: Record<string, string> = {};
    formData.forEach((value, key) => {
      fieldValues[key] = value.toString();
    });

    try {
      const result = await submitForm({
        form_id: 1,
        field_values: fieldValues,
      });
      console.log('Success:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  if (loading) return <div>Loading form...</div>;
  if (!form) return <div>Form not found</div>;

  return (
    <form onSubmit={handleSubmit}>
      {/* Render form fields based on form.fields */}
      <button type="submit">Submit</button>
    </form>
  );
}

Troubleshooting

Build Errors

Error: Module "fs" has been externalized for browser compatibility

Solution: You're importing generators from the main package. Use the /generators export instead:

// ❌ Wrong
import { generateWordPressData } from '@marvalt/wadapter';

// ✅ Correct
import { generateWordPressData } from '@marvalt/wadapter/generators';

Form Submission Errors

Error: 403 Forbidden

Possible causes:

  1. ALLOWED_ORIGINS not set in Cloudflare Pages environment
  2. Turnstile token missing or invalid (if Turnstile is enabled)
  3. Endpoint pattern mismatch

Solutions:

  1. Set ALLOWED_ORIGINS in Cloudflare Pages dashboard: Settings → Environment Variables
  2. Verify VITE_TURNSTILE_SITE_KEY is set and Turnstile widget is rendering
  3. Check that form ID matches the endpoint pattern

Error: 404 Not Found

Possible causes:

  1. Gravity Forms API Endpoint plugin not installed/activated
  2. Incorrect API URL

Solutions:

  1. Verify plugin is installed: Visit https://your-site.com/wp-json/gf-api/v1/health
  2. Check VITE_WORDPRESS_API_URL is correct
  3. Verify WordPress REST API is enabled (Settings → Permalinks)

Data Generation Errors

Error: 401 Unauthorized

Possible causes:

  1. Incorrect WordPress credentials
  2. Cloudflare Access credentials missing (if WordPress is behind CF Access)

Solutions:

  1. Verify VITE_WP_API_USERNAME and VITE_WP_APP_PASSWORD are correct
  2. If using Cloudflare Access, set VITE_CF_ACCESS_CLIENT_ID and VITE_CF_ACCESS_CLIENT_SECRET

Error: Empty or incomplete data

Possible causes:

  1. includeEmbedded: false (should always be true for full functionality)
  2. maxItems too low
  3. Post types not enabled

Solutions:

  1. Always set includeEmbedded: true in generator config
  2. Increase VITE_DEFAULT_MAX_ITEMS if needed
  3. Verify VITE_ENABLED_POST_TYPES includes all needed post types

Turnstile Issues

Widget not rendering

Possible causes:

  1. VITE_TURNSTILE_SITE_KEY not set
  2. Turnstile script not loaded

Solutions:

  1. Set VITE_TURNSTILE_SITE_KEY in environment variables
  2. The widget automatically loads the Turnstile script - verify no CSP blocks it

Verification always fails

Possible causes:

  1. TURNSTILE_SECRET_KEY incorrect
  2. Token expired (tokens expire after 5 minutes)

Solutions:

  1. Verify TURNSTILE_SECRET_KEY matches your Cloudflare Turnstile configuration
  2. Ensure form is submitted within 5 minutes of Turnstile verification

License

GPL-3.0-or-later


Need help? Check the implementation example in bnibrilliance-website or review the source code in the src directory.