John's Technical Blog

How To Use Vite Build Artifacts in Plain PHP Pages

2025-02-28

Introduction

As a developer, I have had the opportunity to migrate several ordinary procedural PHP page websites into Laravel, a popular PHP framework. One of the key benefits of using Laravel is its seamless integration with Vite, a modern development server and build tool. Vite helps to build and manage CSS and JavaScript artifacts, making it easier to develop and maintain large web applications.

However, when migrating a website, it's often desirable to do so in stages, which can lead to a dilemma about how to manage CSS and JS artifacts between the new and old code. This can be further complicated by the fact that most older code runs JavaScript in the global scope, whereas Vite artifacts are generally used in the module scope. In this blog post, we'll explore how to bridge the gap between these two different approaches and leverage the advantages of using NPM to manage JavaScript versions and Vite to process CSS and JavaScript.

Understanding Global and Module Scopes in JavaScript

When including JavaScript on a page, it's important to understand the difference between loading a script normally versus as an ES module, since each behaves differently in terms of scope and import/export capabilities. Let's take a look at an example. In the global scope, variables and functions are attached to the global object (window in browsers):

/* Global scope */
var globalVariable = 'Hello, world!';
console.log(globalVariable); /* Output: Hello, world! */

In contrast, variables and functions defined in the module scope are not attached to the global object:

/* Module scope */
const moduleVariable = 'Hello, world!';
console.log(moduleVariable); /* Output: Hello, world! */
console.log(window.moduleVariable); /* Output: undefined */

The module scope provides a way to encapsulate variables and functions and prevent them from polluting the global namespace.

Considerations:

  • Global scope (classic scripts): Scripts loaded without type="module" run in the global scope. Any top-level variables or functions they define become properties of the window object.
  • Module scope (ES modules): Scripts loaded with type="module" use the JavaScript ES module system. Each module has its own scope, so top-level variables and functions do not automatically become global.
  • Load and execution order: Classic scripts execute in the order they appear. Module scripts are deferred and execute after HTML parsing.
  • Mixing modules and globals: If you include both, module scripts will not automatically see variables from global scripts unless explicitly referenced.

Using Vite Artifacts in Plain PHP Pages

So, how can we use Vite artifacts in plain PHP pages, while still retaining the ability to run JavaScript in the global scope? The answer lies in using the viteStaticCopy plugin to copy the raw JavaScript file managed by NPM into the folder of our choice with the name of our choice.

Install the static copy plugin using NPM:

npm install vite-plugin-static-copy

Here's a complete working Vite configuration, including the viteStaticCopy plugin in our vite.config.js file:

import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import laravel from "laravel-vite-plugin";
import {glob} from "glob";
import basicSsl from "@vitejs/plugin-basic-ssl";
import inject from "@rollup/plugin-inject";
import {viteStaticCopy} from 'vite-plugin-static-copy';

/* Have Vite process all these resources */
let inputs = glob.sync(`resources/css/**/*.css`, {nodir: true});
inputs.push(...glob.sync(`resources/scss/**/*.scss`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.js`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.jsx`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.ts`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.tsx`, {nodir: true}));

export default defineConfig({
    plugins: [
        basicSsl(),
        inject({
            $: "jquery",
            jQuery: "jquery",
        }),
        react(),
        laravel({
            input: inputs,
            refresh: true,
        }),
        viteStaticCopy({
            targets: [
                {
                    src: 'node_modules/jquery/dist/jquery.js',
                    dest: 'assets/vendor',
                    rename: 'jquery.js'
                }
            ]
        })
    ],
    resolve: {
        alias: {
            "@": "/resources/js",
        },
        extensions: [".js", ".ts", ".jsx", ".tsx"],
    },
    build: {
        sourcemap: true,
        rollupOptions: {
            output: {
                entryFileNames: "assets/[name].[hash].js",
                chunkFileNames: "assets/[name].[hash].js",
                assetFileNames: (assetInfo) => {
                    /* Preload fonts only if they are woff or woff2 */
                    if (assetInfo.extname === ".woff2" || assetInfo.extname === ".woff") {
                        return "fonts/[name].[hash][extname]";
                    }
                    return "assets/[name].[hash][extname]";
                },
            },
        },
    },
});

The viteStaticCopy(targets) configuration will copy the jQuery library from the node_modules directory into the assets/vendor directory and rename it to jquery.js. Note that you can choose any destination folder and file name you like.

Run npm run build to generate assets.

We can then use this library in our plain PHP pages, while still retaining the ability to run JavaScript in the global scope:

<?php
const BUILD_PATH = '/build/';

$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);

$appCss = BUILD_PATH . $manifest['resources/scss/app.scss']['file'];
$appJs = BUILD_PATH . $manifest['resources/js/app.js']['file'];
$jqueryJs = BUILD_PATH . '/assets/vendor/jquery.js' ;
?>

<!-- The processed CSS file can be linked like any other CSS file -->
<link href="<?= $appCss ?>" rel="stylesheet" type="text/css"/>
<!-- The module type is required if you have used any imports in app.js, which will usually be the case -->
<script type="module" src="<?= $appJs ?>"></script>
<!-- With any un-processed JavaScript files, you can use the normal global scope -->
<script type="text/javascript" src="<?= $jqueryJs ?>"></script>

<script type="module">
    <!-- This uses the jQuery include in the app.js file processed by Vite -->
    $('body').prepend("<h1>Hi!</h1>");
</script>
<script type="text/javascript">
    <!-- This uses the jQuery from the the raw jquery.js file loaded into the global scope -->
    $(function() {
        $('body').append("<h1>Bye!</h1>");
    });
</script>

In this example, we're using the viteStaticCopy plugin to copy the jQuery library into the assets/vendor directory, and then using it in our plain PHP page, while still using Vite to build and manage our CSS and JavaScript artifacts.

Conclusion

In conclusion, using Vite artifacts in plain PHP pages can be a challenge, especially when it comes to managing CSS and JavaScript artifacts between old and new code. However, by leveraging the advantages of using NPM to manage JavaScript versions and Vite to process CSS and JavaScript, we can bridge the gap between these two different approaches.

By understanding the difference between global and module scopes in JavaScript, we can use Vite artifacts in plain PHP pages, while still retaining the ability to run JavaScript in the global scope. The viteStaticCopy plugin provides a way to copy raw JavaScript files managed by NPM into the folder of our choice with the name of our choice, making it easier to use Vite artifacts in plain PHP pages.

I hope this blog post has provided a helpful guide on how to use Vite artifacts in plain PHP pages. By following these steps, you can leverage the advantages of using Vite and NPM to manage your CSS and JavaScript artifacts, while still supporting older code that runs in the global scope.

How To Use Vite Build Artifacts in Plain PHP Pages

2025-02-28

Introduction

As a developer, I have had the opportunity to migrate several ordinary procedural PHP page websites into Laravel, a popular PHP framework. One of the key benefits of using Laravel is its seamless integration with Vite, a modern development server and build tool. Vite helps to build and manage CSS and JavaScript artifacts, making it easier to develop and maintain large web applications.

However, when migrating a website, it's often desirable to do so in stages, which can lead to a dilemma about how to manage CSS and JS artifacts between the new and old code. This can be further complicated by the fact that most older code runs JavaScript in the global scope, whereas Vite artifacts are generally used in the module scope. In this blog post, we'll explore how to bridge the gap between these two different approaches and leverage the advantages of using NPM to manage JavaScript versions and Vite to process CSS and JavaScript.

Understanding Global and Module Scopes in JavaScript

When including JavaScript on a page, it's important to understand the difference between loading a script normally versus as an ES module, since each behaves differently in terms of scope and import/export capabilities. Let's take a look at an example. In the global scope, variables and functions are attached to the global object (window in browsers):

/* Global scope */
var globalVariable = 'Hello, world!';
console.log(globalVariable); /* Output: Hello, world! */

In contrast, variables and functions defined in the module scope are not attached to the global object:

/* Module scope */
const moduleVariable = 'Hello, world!';
console.log(moduleVariable); /* Output: Hello, world! */
console.log(window.moduleVariable); /* Output: undefined */

The module scope provides a way to encapsulate variables and functions and prevent them from polluting the global namespace.

Considerations:

  • Global scope (classic scripts): Scripts loaded without type="module" run in the global scope. Any top-level variables or functions they define become properties of the window object.
  • Module scope (ES modules): Scripts loaded with type="module" use the JavaScript ES module system. Each module has its own scope, so top-level variables and functions do not automatically become global.
  • Load and execution order: Classic scripts execute in the order they appear. Module scripts are deferred and execute after HTML parsing.
  • Mixing modules and globals: If you include both, module scripts will not automatically see variables from global scripts unless explicitly referenced.

Using Vite Artifacts in Plain PHP Pages

So, how can we use Vite artifacts in plain PHP pages, while still retaining the ability to run JavaScript in the global scope? The answer lies in using the viteStaticCopy plugin to copy the raw JavaScript file managed by NPM into the folder of our choice with the name of our choice.

Install the static copy plugin using NPM:

npm install vite-plugin-static-copy

Here's a complete working Vite configuration, including the viteStaticCopy plugin in our vite.config.js file:

import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import laravel from "laravel-vite-plugin";
import {glob} from "glob";
import basicSsl from "@vitejs/plugin-basic-ssl";
import inject from "@rollup/plugin-inject";
import {viteStaticCopy} from 'vite-plugin-static-copy';

/* Have Vite process all these resources */
let inputs = glob.sync(`resources/css/**/*.css`, {nodir: true});
inputs.push(...glob.sync(`resources/scss/**/*.scss`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.js`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.jsx`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.ts`, {nodir: true}));
inputs.push(...glob.sync(`resources/js/**/*.tsx`, {nodir: true}));

export default defineConfig({
    plugins: [
        basicSsl(),
        inject({
            $: "jquery",
            jQuery: "jquery",
        }),
        react(),
        laravel({
            input: inputs,
            refresh: true,
        }),
        viteStaticCopy({
            targets: [
                {
                    src: 'node_modules/jquery/dist/jquery.js',
                    dest: 'assets/vendor',
                    rename: 'jquery.js'
                }
            ]
        })
    ],
    resolve: {
        alias: {
            "@": "/resources/js",
        },
        extensions: [".js", ".ts", ".jsx", ".tsx"],
    },
    build: {
        sourcemap: true,
        rollupOptions: {
            output: {
                entryFileNames: "assets/[name].[hash].js",
                chunkFileNames: "assets/[name].[hash].js",
                assetFileNames: (assetInfo) => {
                    /* Preload fonts only if they are woff or woff2 */
                    if (assetInfo.extname === ".woff2" || assetInfo.extname === ".woff") {
                        return "fonts/[name].[hash][extname]";
                    }
                    return "assets/[name].[hash][extname]";
                },
            },
        },
    },
});

The viteStaticCopy(targets) configuration will copy the jQuery library from the node_modules directory into the assets/vendor directory and rename it to jquery.js. Note that you can choose any destination folder and file name you like.

Run npm run build to generate assets.

We can then use this library in our plain PHP pages, while still retaining the ability to run JavaScript in the global scope:

<?php
const BUILD_PATH = '/build/';

$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);

$appCss = BUILD_PATH . $manifest['resources/scss/app.scss']['file'];
$appJs = BUILD_PATH . $manifest['resources/js/app.js']['file'];
$jqueryJs = BUILD_PATH . '/assets/vendor/jquery.js' ;
?>

<!-- The processed CSS file can be linked like any other CSS file -->
<link href="<?= $appCss ?>" rel="stylesheet" type="text/css"/>
<!-- The module type is required if you have used any imports in app.js, which will usually be the case -->
<script type="module" src="<?= $appJs ?>"></script>
<!-- With any un-processed JavaScript files, you can use the normal global scope -->
<script type="text/javascript" src="<?= $jqueryJs ?>"></script>

<script type="module">
    <!-- This uses the jQuery include in the app.js file processed by Vite -->
    $('body').prepend("<h1>Hi!</h1>");
</script>
<script type="text/javascript">
    <!-- This uses the jQuery from the the raw jquery.js file loaded into the global scope -->
    $(function() {
        $('body').append("<h1>Bye!</h1>");
    });
</script>

In this example, we're using the viteStaticCopy plugin to copy the jQuery library into the assets/vendor directory, and then using it in our plain PHP page, while still using Vite to build and manage our CSS and JavaScript artifacts.

Conclusion

In conclusion, using Vite artifacts in plain PHP pages can be a challenge, especially when it comes to managing CSS and JavaScript artifacts between old and new code. However, by leveraging the advantages of using NPM to manage JavaScript versions and Vite to process CSS and JavaScript, we can bridge the gap between these two different approaches.

By understanding the difference between global and module scopes in JavaScript, we can use Vite artifacts in plain PHP pages, while still retaining the ability to run JavaScript in the global scope. The viteStaticCopy plugin provides a way to copy raw JavaScript files managed by NPM into the folder of our choice with the name of our choice, making it easier to use Vite artifacts in plain PHP pages.

I hope this blog post has provided a helpful guide on how to use Vite artifacts in plain PHP pages. By following these steps, you can leverage the advantages of using Vite and NPM to manage your CSS and JavaScript artifacts, while still supporting older code that runs in the global scope.

Creating Deterministic UUIDs with UUIDv5 and X.500

2024-12-21


Introduction

When you need consistent, repeatable identifiers in your projects, UUIDv5 can be your go-to solution. Unlike UUIDv4, which is purely random, or other versions that are less random, but still unpredictable, UUIDv5 combines a namespace with a name (or set of attributes) to produce the same UUID for the same input pair regardless of the platform or programming language. This is highly useful when you have data that should map to a fixed identifier—like a country code or an organizational unit—where you always want to get back the exact same UUID for that data.

In the RFC 4122, Appendix C , there are four primary standard namespaces for UUIDv5:

  1. DNS (Domain Name System)
  2. URL (Uniform Resource Locator)
  3. OID (Object Identifier)
  4. X.500 (Directory Services)

In this post, we’ll:

  1. Introduce the four standard UUIDv5 namespaces: DNS, URL, OID, and X.500.
  2. Show simple examples using DNS, URL, and OID, including their full names and brief definitions.
  3. Dive deeper into the X.500 namespace.
  4. Discuss how to create UUIDv5-based database keys from existing uniqueness constraints or compound keys using the X.500 namespace.

We’ll use code samples from Symfony’s Symfony\Component\Uid\Uuid component, but UUIDv5 is available in several libraries across different platforms, which will produce the exact same deterministic UUIDs in all cases.


Quick Examples: DNS, URL, and OID Namespaces

Before diving into X.500, here are three simple examples demonstrating how to generate deterministic UUIDs using the DNS, URL, and OID namespaces.

1. DNS Example

  • DNS stands for Domain Name System, a hierarchical naming system used to resolve domain names into IP addresses.
  • A DNS name typically looks like threeleaf.com or subdomain.example.org.

use Symfony\Component\Uid\Uuid;

/**
 * Generate a deterministic UUID using the DNS namespace.
 *
 * @param string $hostname The hostname to convert.
 * @return string The deterministic UUID for the given hostname.
 */
function generateDnsUuid(string $hostname): string
{
    /* Generate a deterministic UUIDv5: */
    $dnsUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_DNS),
        $hostname
    );

    return (string) $dnsUuid;
}

/* Example usage: */
$hostname = 'threeleaf.com';
echo generateDnsUuid($hostname);

2. URL Example

  • URL stands for Uniform Resource Locator, a reference (an address) to a resource on the internet.
  • A URL typically looks like https://threeleaf.com/blog.

/**
 * Generate a deterministic UUID using the URL namespace.
 *
 * @param string $url The URL to convert.
 * @return string The deterministic UUID for the given URL.
 */
function generateUrlUuid(string $url): string
{
    /* Generate a deterministic UUIDv5: */
    $urlUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_URL),
        $url
    );

    return (string) $urlUuid;
}

/* Example usage: */
$url = 'https://threeleaf.com/blog';
echo generateUrlUuid($url);

3. OID Example

  • OID stands for Object Identifier, a globally unique identifier used in various standards (e.g., SNMP, LDAP) to name an object or concept.
  • An OID typically looks like 1.3.6.1.4.1..., where each number identifies a node in a hierarchy.

/**
 * Generate a deterministic UUID using the OID namespace.
 *
 * @param string $oid The OID string to convert.
 * @return string The deterministic UUID for the given OID.
 */
function generateOidUuid(string $oid): string
{
    /* Generate a deterministic UUIDv5: */
    $oidUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_OID),
        $oid
    );

    return (string) $oidUuid;
}

/* Example usage: */
$oid = '1.3.6.1.4.1';
echo generateOidUuid($oid);

Deep Dive: X.500 Namespaces and Distinguished Names

X.500 is a suite of standards for directory services. It defines a structure for Distinguished Names (DNs), which act like the “full path” for an entry in a directory (similar in concept to a file path in a filesystem). Each DN is a concatenation of attribute-value pairs that uniquely identify an entry. Common attribute keys include:

  • CN (Common Name, Full Name)
  • OU (Organizational Unit)
  • O (Organization Name)
  • L (Locality)
  • ST (State or Province)
  • C (ISO 3166 2-Letter Country Code)
  • UID (User ID)
  • SN (Surname, Last Name)
  • GivenName (First Name)
  • Mail (Email Address)

For a thorough list of attributes, check out:

Because these attributes are hierarchical and fairly stable (e.g., CN=John A. Marsh), the X.500 namespace is perfectly suited for generating deterministic UUIDs when referencing directory-like entities.

Generating UUIDs with X.500

To generate a UUIDv5 based on a Distinguished Name, you can use the NAMESPACE_X500 constant:


/**
 * Generate a UUIDv5 based on a Distinguished Name (DN) using the X.500 namespace.
 *
 * @param string $distinguishedName The distinguished name to convert.
 * @return string The deterministic UUID for the given distinguished name.
 */
function generateX500Uuid(string $distinguishedName): string
{
    /* Generate a deterministic UUIDv5: */
    $uuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string) $uuid;
}

/* Example usage: */
$distinguishedName = 'CN=John A. Marsh';
echo generateX500Uuid($distinguishedName);

$complexDistinguishedName = 'CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US';
echo generateX500Uuid($complexDistinguishedName);

Important note about distinguished name volatility!
Depending on the specific use case, keep in mind that you may want to focus on attributes that are stable and unique for your application. If you are independently calculating deterministic UUIDs in several places over a long period of time, you will want to avoid attributes that might change in that timespan. For example, an application-assigned user ID or government-assigned ID might be considered permanent, whereas a user’s personal email address or phone number might change.


Creating UUID Database Keys

In many applications, you already have unique identifiers—like an internal employee ID or a composite key based on a combination of columns. By converting these existing keys into UUIDv5 values (using NAMESPACE_X500 in this case), you ensure you still get deterministic UUIDs while leveraging your current uniqueness constraints.

1. Converting a Non-UUID Employee ID into a UUID

Suppose you have a table employee with a unique, non-UUID primary key employee_id, such as 'EMP12345'. Since employeeNumber is a standard X.500 attribute for an employee’s unique ID, you could do:


/**
 * Convert a non-UUID employee ID into a UUID.
 *
 * @param string $employeeId The employee ID to convert.
 * @return string The deterministic UUID for the given employee ID.
 */
function generateEmployeeUuid(string $employeeId): string
{
    /* Construct an X.500 "distinguished name" style string: */
    $distinguishedName = 'employeeNumber=' . $employeeId;

    /* Generate a deterministic UUIDv5: */
    $employeeUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string) $employeeUuid;
}

/* Example usage: */
$employeeId = 'EMP12345';
echo generateEmployeeUuid($employeeId);

Here, every time you use "employeeNumber=EMP12345" with NAMESPACE_X500, you’ll get the same UUID, which can be stored or used in foreign keys.

2. Generating UUIDs from a Compound Key (Phone + Email)

Sometimes, uniqueness is enforced by a combination of columns—for example, phone_number and email. Two relevant LDAP attributes here are telephoneNumber and mail:


/**
 * Generate a UUID from a compound key (phone + email).
 *
 * @param string $phoneNumber The customer phone number.
 * @param string $email The customer email address.
 * @return string The deterministic UUID for the given phone number and email.
 */
function generateCustomerUuid(string $phoneNumber, string $email): string
{
    /* Create an X.500-style DN with multiple attributes: */
    $compoundDn = 'telephoneNumber=' . $phoneNumber . ',mail=' . $email;

    /* Generate the UUID using X.500 namespace: */
    $userUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $compoundDn
    );

    return (string) $userUuid;
}

/* Example usage: */
$phoneNumber = '+1234567890';
$email = 'john.marsh@example.com';
echo generateCustomerUuid($phoneNumber, $email);

Conclusion

Deterministic UUIDs (UUIDv5) can simplify your data by ensuring the same input always yields the same output. They’re particularly handy for X.500-style distinguished names, where hierarchical attributes remain stable over time. By combining a robust namespace like NAMESPACE_X500 with well-structured DNs, you’ll produce consistent identifiers throughout your applications.

Whether you’re generating UUIDs for DNS, URL, OID, or X.500, a Uuid v5 function makes it straightforward. Just remember to:

  1. Pick the right namespace (DNS, URL, OID, or X.500).
  2. Use suitable and stable attributes when generating UUIDs based on X.500 Distinguished Names.
  3. Leverage UUIDv5 to ensure deterministic results.

Finally, converting existing IDs or compound keys into UUIDv5 can unify how you manage references throughout your database and company. Once you adopt deterministic UUIDs, you’ll never have to worry about conflicting keys for the same underlying data.

Happy coding, and enjoy your deterministic UUIDs!



Appendix

Complete PHP Example (uuidv5-test.php)


require __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\Uid\Uuid;

/**
 * Generate a deterministic UUID using the DNS namespace.
 *
 * @param string $hostname The hostname to convert.
 *
 * @return string The deterministic UUID for the given hostname.
 */
function generateDnsUuid(string $hostname): string
{
    /* Generate a deterministic UUIDv5: */
    $dnsUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_DNS),
        $hostname
    );

    return (string)$dnsUuid;
}

/**
 * Generate a deterministic UUID using the URL namespace.
 *
 * @param string $url The URL to convert.
 *
 * @return string The deterministic UUID for the given URL.
 */
function generateUrlUuid(string $url): string
{
    /* Generate a deterministic UUIDv5: */
    $urlUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_URL),
        $url
    );

    return (string)$urlUuid;
}

/**
 * Generate a deterministic UUID using the OID namespace.
 *
 * @param string $oid The OID string to convert.
 *
 * @return string The deterministic UUID for the given OID.
 */
function generateOidUuid(string $oid): string
{
    /* Generate a deterministic UUIDv5: */
    $oidUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_OID),
        $oid
    );

    return (string)$oidUuid;
}

/**
 * Generate a UUIDv5 based on a Distinguished Name (DN) using the X.500 namespace.
 *
 * @param string $distinguishedName The distinguished name to convert.
 *
 * @return string The deterministic UUID for the given distinguished name.
 */
function generateX500Uuid(string $distinguishedName): string
{
    /* Generate a deterministic UUIDv5: */
    $uuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string)$uuid;
}

/**
 * Convert a non-UUID employee ID into a UUID.
 *
 * @param string $employeeId The employee ID to convert.
 *
 * @return string The deterministic UUID for the given employee ID.
 */
function generateEmployeeUuid(string $employeeId): string
{
    /* Construct an X.500 "distinguished name" style string: */
    $distinguishedName = 'employeeNumber=' . $employeeId;

    /* Generate a deterministic UUIDv5: */
    $employeeUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string)$employeeUuid;
}

/**
 * Generate a UUID from a compound key (phone + email).
 *
 * @param string $phoneNumber The customer phone number.
 * @param string $email       The customer email address.
 *
 * @return string The deterministic UUID for the given phone number and email.
 */
function generateCustomerUuid(string $phoneNumber, string $email): string
{
    /* Create an X.500-style DN with multiple attributes: */
    $compoundDn = 'telephoneNumber=' . $phoneNumber . ',mail=' . $email;

    /* Generate the UUID using X.500 namespace: */
    $userUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $compoundDn
    );

    return (string)$userUuid;
}

// Example usage section with formatted output:
echo '

Example Usage of UUID Generation Functions

'; echo "
"; // DNS Example $hostname = 'threeleaf.com'; $dnsResult = generateDnsUuid($hostname); echo "
"; echo "generateDnsUuid('$hostname')
"; echo "→ $dnsResult"; echo '
'; // URL Example $url = 'https://threeleaf.com/blog'; $urlResult = generateUrlUuid($url); echo "
"; echo "generateUrlUuid('$url')
"; echo "→ $urlResult"; echo '
'; // OID Example $oid = '1.3.6.1.4.1'; $oidResult = generateOidUuid($oid); echo "
"; echo "generateOidUuid('$oid')
"; echo "→ $oidResult"; echo '
'; // X500 Simple Example $distinguishedName = 'CN=John A. Marsh'; $x500Result = generateX500Uuid($distinguishedName); echo "
"; echo "generateX500Uuid('$distinguishedName')
"; echo "→ $x500Result"; echo '
'; // X500 Complex Example $complexDistinguishedName = 'CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US'; $complexX500Result = generateX500Uuid($complexDistinguishedName); echo "
"; echo "generateX500Uuid('$complexDistinguishedName')
"; echo "→ $complexX500Result"; echo '
'; // Employee Example $employeeId = 'EMP12345'; $employeeResult = generateEmployeeUuid($employeeId); echo "
"; echo "generateEmployeeUuid('$employeeId')
"; echo "→ $employeeResult"; echo '
'; // Customer Compound Key Example $phoneNumber = '+1234567890'; $email = 'john.marsh@example.com'; $customerResult = generateCustomerUuid($phoneNumber, $email); echo "
"; echo "generateCustomerUuid('$phoneNumber', '$email')
"; echo "→ $customerResult"; echo '
'; echo '
';

Complete Python Example (uuidv5_test.py)


import uuid
from typing import Optional

def generate_dns_uuid(hostname: str) -> str:
    """Generate a deterministic UUID using the DNS namespace.

    Args:
        hostname: The hostname to convert.
    Returns:
        The deterministic UUID for the given hostname.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_DNS, hostname))

def generate_url_uuid(url: str) -> str:
    """Generate a deterministic UUID using the URL namespace.

    Args:
        url: The URL to convert.
    Returns:
        The deterministic UUID for the given URL.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_URL, url))

def generate_oid_uuid(oid: str) -> str:
    """Generate a deterministic UUID using the OID namespace.

    Args:
        oid: The OID string to convert.
    Returns:
        The deterministic UUID for the given OID.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_OID, oid))

def generate_x500_uuid(distinguished_name: str) -> str:
    """Generate a UUIDv5 based on a Distinguished Name (DN) using the X.500 namespace.

    Args:
        distinguished_name: The distinguished name to convert.
    Returns:
        The deterministic UUID for the given distinguished name.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_X500, distinguished_name))

def generate_employee_uuid(employee_id: str) -> str:
    """Convert a non-UUID employee ID into a UUID.

    Args:
        employee_id: The employee ID to convert.
    Returns:
        The deterministic UUID for the given employee ID.
    """
    distinguished_name = f'employeeNumber={employee_id}'
    return str(uuid.uuid5(uuid.NAMESPACE_X500, distinguished_name))

def generate_customer_uuid(phone_number: str, email: str) -> str:
    """Generate a UUID from a compound key (phone + email).

    Args:
        phone_number: The customer phone number.
        email: The customer email address.
    Returns:
        The deterministic UUID for the given phone number and email.
    """
    compound_dn = f'telephoneNumber={phone_number},mail={email}'
    return str(uuid.uuid5(uuid.NAMESPACE_X500, compound_dn))

def print_example_header():
    print("

Example Usage of UUID Generation Functions

") print("
") def print_example_result(function_name: str, args: str, result: str): print("
") print(f"{function_name}({args})
") print(f"→ {result}") print("
") def main(): print_example_header() # DNS Example hostname = 'threeleaf.com' dns_result = generate_dns_uuid(hostname) print_example_result('generate_dns_uuid', f"'{hostname}'", dns_result) # URL Example url = 'https://threeleaf.com/blog' url_result = generate_url_uuid(url) print_example_result('generate_url_uuid', f"'{url}'", url_result) # OID Example oid = '1.3.6.1.4.1' oid_result = generate_oid_uuid(oid) print_example_result('generate_oid_uuid', f"'{oid}'", oid_result) # X500 Simple Example distinguished_name = 'CN=John A. Marsh' x500_result = generate_x500_uuid(distinguished_name) print_example_result('generate_x500_uuid', f"'{distinguished_name}'", x500_result) # X500 Complex Example complex_dn = 'CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US' complex_result = generate_x500_uuid(complex_dn) print_example_result('generate_x500_uuid', f"'{complex_dn}'", complex_result) # Employee Example employee_id = 'EMP12345' employee_result = generate_employee_uuid(employee_id) print_example_result('generate_employee_uuid', f"'{employee_id}'", employee_result) # Customer Compound Key Example phone_number = '+1234567890' email = 'john.marsh@example.com' customer_result = generate_customer_uuid(phone_number, email) print_example_result('generate_customer_uuid', f"'{phone_number}', '{email}'", customer_result) print("
") if __name__ == "__main__": main()

Output (matches between PHP runnin on Linux and Python running on Macbook)


Example Usage of UUID Generation Functions

generate_dns_uuid('threeleaf.com')
→ d4a08aa5-9661-57ab-bf61-8f28be9b1f00
generate_url_uuid('https://threeleaf.com/blog')
→ b0277088-caf5-5a15-aa5d-5115112640c3
generate_oid_uuid('1.3.6.1.4.1')
→ 106dd502-8b3e-50db-80ed-1134f5c18eae
generate_x500_uuid('CN=John A. Marsh')
→ a953bc33-f538-5bb3-baa3-aaf081b5df93
generate_x500_uuid('CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US')
→ b837ef27-6f4f-5a48-9419-badb502fb581
generate_employee_uuid('EMP12345')
→ 7aed46df-9f8e-5252-b340-34a0f15d33dd
generate_customer_uuid('+1234567890', 'john.marsh@example.com')
→ db23fe5d-ec32-5522-b563-f10847432d04

Listen to the podcast generated by NotebookLM

Creating Deterministic UUIDs with UUIDv5 and X.500

2024-12-21


Introduction

When you need consistent, repeatable identifiers in your projects, UUIDv5 can be your go-to solution. Unlike UUIDv4, which is purely random, or other versions that are less random, but still unpredictable, UUIDv5 combines a namespace with a name (or set of attributes) to produce the same UUID for the same input pair regardless of the platform or programming language. This is highly useful when you have data that should map to a fixed identifier—like a country code or an organizational unit—where you always want to get back the exact same UUID for that data.

In the RFC 4122, Appendix C , there are four primary standard namespaces for UUIDv5:

  1. DNS (Domain Name System)
  2. URL (Uniform Resource Locator)
  3. OID (Object Identifier)
  4. X.500 (Directory Services)

In this post, we’ll:

  1. Introduce the four standard UUIDv5 namespaces: DNS, URL, OID, and X.500.
  2. Show simple examples using DNS, URL, and OID, including their full names and brief definitions.
  3. Dive deeper into the X.500 namespace.
  4. Discuss how to create UUIDv5-based database keys from existing uniqueness constraints or compound keys using the X.500 namespace.

We’ll use code samples from Symfony’s Symfony\Component\Uid\Uuid component, but UUIDv5 is available in several libraries across different platforms, which will produce the exact same deterministic UUIDs in all cases.


Quick Examples: DNS, URL, and OID Namespaces

Before diving into X.500, here are three simple examples demonstrating how to generate deterministic UUIDs using the DNS, URL, and OID namespaces.

1. DNS Example

  • DNS stands for Domain Name System, a hierarchical naming system used to resolve domain names into IP addresses.
  • A DNS name typically looks like threeleaf.com or subdomain.example.org.

use Symfony\Component\Uid\Uuid;

/**
 * Generate a deterministic UUID using the DNS namespace.
 *
 * @param string $hostname The hostname to convert.
 * @return string The deterministic UUID for the given hostname.
 */
function generateDnsUuid(string $hostname): string
{
    /* Generate a deterministic UUIDv5: */
    $dnsUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_DNS),
        $hostname
    );

    return (string) $dnsUuid;
}

/* Example usage: */
$hostname = 'threeleaf.com';
echo generateDnsUuid($hostname);

2. URL Example

  • URL stands for Uniform Resource Locator, a reference (an address) to a resource on the internet.
  • A URL typically looks like https://threeleaf.com/blog.

/**
 * Generate a deterministic UUID using the URL namespace.
 *
 * @param string $url The URL to convert.
 * @return string The deterministic UUID for the given URL.
 */
function generateUrlUuid(string $url): string
{
    /* Generate a deterministic UUIDv5: */
    $urlUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_URL),
        $url
    );

    return (string) $urlUuid;
}

/* Example usage: */
$url = 'https://threeleaf.com/blog';
echo generateUrlUuid($url);

3. OID Example

  • OID stands for Object Identifier, a globally unique identifier used in various standards (e.g., SNMP, LDAP) to name an object or concept.
  • An OID typically looks like 1.3.6.1.4.1..., where each number identifies a node in a hierarchy.

/**
 * Generate a deterministic UUID using the OID namespace.
 *
 * @param string $oid The OID string to convert.
 * @return string The deterministic UUID for the given OID.
 */
function generateOidUuid(string $oid): string
{
    /* Generate a deterministic UUIDv5: */
    $oidUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_OID),
        $oid
    );

    return (string) $oidUuid;
}

/* Example usage: */
$oid = '1.3.6.1.4.1';
echo generateOidUuid($oid);

Deep Dive: X.500 Namespaces and Distinguished Names

X.500 is a suite of standards for directory services. It defines a structure for Distinguished Names (DNs), which act like the “full path” for an entry in a directory (similar in concept to a file path in a filesystem). Each DN is a concatenation of attribute-value pairs that uniquely identify an entry. Common attribute keys include:

  • CN (Common Name, Full Name)
  • OU (Organizational Unit)
  • O (Organization Name)
  • L (Locality)
  • ST (State or Province)
  • C (ISO 3166 2-Letter Country Code)
  • UID (User ID)
  • SN (Surname, Last Name)
  • GivenName (First Name)
  • Mail (Email Address)

For a thorough list of attributes, check out:

Because these attributes are hierarchical and fairly stable (e.g., CN=John A. Marsh), the X.500 namespace is perfectly suited for generating deterministic UUIDs when referencing directory-like entities.

Generating UUIDs with X.500

To generate a UUIDv5 based on a Distinguished Name, you can use the NAMESPACE_X500 constant:


/**
 * Generate a UUIDv5 based on a Distinguished Name (DN) using the X.500 namespace.
 *
 * @param string $distinguishedName The distinguished name to convert.
 * @return string The deterministic UUID for the given distinguished name.
 */
function generateX500Uuid(string $distinguishedName): string
{
    /* Generate a deterministic UUIDv5: */
    $uuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string) $uuid;
}

/* Example usage: */
$distinguishedName = 'CN=John A. Marsh';
echo generateX500Uuid($distinguishedName);

$complexDistinguishedName = 'CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US';
echo generateX500Uuid($complexDistinguishedName);

Important note about distinguished name volatility!
Depending on the specific use case, keep in mind that you may want to focus on attributes that are stable and unique for your application. If you are independently calculating deterministic UUIDs in several places over a long period of time, you will want to avoid attributes that might change in that timespan. For example, an application-assigned user ID or government-assigned ID might be considered permanent, whereas a user’s personal email address or phone number might change.


Creating UUID Database Keys

In many applications, you already have unique identifiers—like an internal employee ID or a composite key based on a combination of columns. By converting these existing keys into UUIDv5 values (using NAMESPACE_X500 in this case), you ensure you still get deterministic UUIDs while leveraging your current uniqueness constraints.

1. Converting a Non-UUID Employee ID into a UUID

Suppose you have a table employee with a unique, non-UUID primary key employee_id, such as 'EMP12345'. Since employeeNumber is a standard X.500 attribute for an employee’s unique ID, you could do:


/**
 * Convert a non-UUID employee ID into a UUID.
 *
 * @param string $employeeId The employee ID to convert.
 * @return string The deterministic UUID for the given employee ID.
 */
function generateEmployeeUuid(string $employeeId): string
{
    /* Construct an X.500 "distinguished name" style string: */
    $distinguishedName = 'employeeNumber=' . $employeeId;

    /* Generate a deterministic UUIDv5: */
    $employeeUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string) $employeeUuid;
}

/* Example usage: */
$employeeId = 'EMP12345';
echo generateEmployeeUuid($employeeId);

Here, every time you use "employeeNumber=EMP12345" with NAMESPACE_X500, you’ll get the same UUID, which can be stored or used in foreign keys.

2. Generating UUIDs from a Compound Key (Phone + Email)

Sometimes, uniqueness is enforced by a combination of columns—for example, phone_number and email. Two relevant LDAP attributes here are telephoneNumber and mail:


/**
 * Generate a UUID from a compound key (phone + email).
 *
 * @param string $phoneNumber The customer phone number.
 * @param string $email The customer email address.
 * @return string The deterministic UUID for the given phone number and email.
 */
function generateCustomerUuid(string $phoneNumber, string $email): string
{
    /* Create an X.500-style DN with multiple attributes: */
    $compoundDn = 'telephoneNumber=' . $phoneNumber . ',mail=' . $email;

    /* Generate the UUID using X.500 namespace: */
    $userUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $compoundDn
    );

    return (string) $userUuid;
}

/* Example usage: */
$phoneNumber = '+1234567890';
$email = 'john.marsh@example.com';
echo generateCustomerUuid($phoneNumber, $email);

Conclusion

Deterministic UUIDs (UUIDv5) can simplify your data by ensuring the same input always yields the same output. They’re particularly handy for X.500-style distinguished names, where hierarchical attributes remain stable over time. By combining a robust namespace like NAMESPACE_X500 with well-structured DNs, you’ll produce consistent identifiers throughout your applications.

Whether you’re generating UUIDs for DNS, URL, OID, or X.500, a Uuid v5 function makes it straightforward. Just remember to:

  1. Pick the right namespace (DNS, URL, OID, or X.500).
  2. Use suitable and stable attributes when generating UUIDs based on X.500 Distinguished Names.
  3. Leverage UUIDv5 to ensure deterministic results.

Finally, converting existing IDs or compound keys into UUIDv5 can unify how you manage references throughout your database and company. Once you adopt deterministic UUIDs, you’ll never have to worry about conflicting keys for the same underlying data.

Happy coding, and enjoy your deterministic UUIDs!



Appendix

Complete PHP Example (uuidv5-test.php)


require __DIR__ . '/../vendor/autoload.php';

use Symfony\Component\Uid\Uuid;

/**
 * Generate a deterministic UUID using the DNS namespace.
 *
 * @param string $hostname The hostname to convert.
 *
 * @return string The deterministic UUID for the given hostname.
 */
function generateDnsUuid(string $hostname): string
{
    /* Generate a deterministic UUIDv5: */
    $dnsUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_DNS),
        $hostname
    );

    return (string)$dnsUuid;
}

/**
 * Generate a deterministic UUID using the URL namespace.
 *
 * @param string $url The URL to convert.
 *
 * @return string The deterministic UUID for the given URL.
 */
function generateUrlUuid(string $url): string
{
    /* Generate a deterministic UUIDv5: */
    $urlUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_URL),
        $url
    );

    return (string)$urlUuid;
}

/**
 * Generate a deterministic UUID using the OID namespace.
 *
 * @param string $oid The OID string to convert.
 *
 * @return string The deterministic UUID for the given OID.
 */
function generateOidUuid(string $oid): string
{
    /* Generate a deterministic UUIDv5: */
    $oidUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_OID),
        $oid
    );

    return (string)$oidUuid;
}

/**
 * Generate a UUIDv5 based on a Distinguished Name (DN) using the X.500 namespace.
 *
 * @param string $distinguishedName The distinguished name to convert.
 *
 * @return string The deterministic UUID for the given distinguished name.
 */
function generateX500Uuid(string $distinguishedName): string
{
    /* Generate a deterministic UUIDv5: */
    $uuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string)$uuid;
}

/**
 * Convert a non-UUID employee ID into a UUID.
 *
 * @param string $employeeId The employee ID to convert.
 *
 * @return string The deterministic UUID for the given employee ID.
 */
function generateEmployeeUuid(string $employeeId): string
{
    /* Construct an X.500 "distinguished name" style string: */
    $distinguishedName = 'employeeNumber=' . $employeeId;

    /* Generate a deterministic UUIDv5: */
    $employeeUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $distinguishedName
    );

    return (string)$employeeUuid;
}

/**
 * Generate a UUID from a compound key (phone + email).
 *
 * @param string $phoneNumber The customer phone number.
 * @param string $email       The customer email address.
 *
 * @return string The deterministic UUID for the given phone number and email.
 */
function generateCustomerUuid(string $phoneNumber, string $email): string
{
    /* Create an X.500-style DN with multiple attributes: */
    $compoundDn = 'telephoneNumber=' . $phoneNumber . ',mail=' . $email;

    /* Generate the UUID using X.500 namespace: */
    $userUuid = Uuid::v5(
        Uuid::fromString(Uuid::NAMESPACE_X500),
        $compoundDn
    );

    return (string)$userUuid;
}

// Example usage section with formatted output:
echo '

Example Usage of UUID Generation Functions

'; echo "
"; // DNS Example $hostname = 'threeleaf.com'; $dnsResult = generateDnsUuid($hostname); echo "
"; echo "generateDnsUuid('$hostname')
"; echo "→ $dnsResult"; echo '
'; // URL Example $url = 'https://threeleaf.com/blog'; $urlResult = generateUrlUuid($url); echo "
"; echo "generateUrlUuid('$url')
"; echo "→ $urlResult"; echo '
'; // OID Example $oid = '1.3.6.1.4.1'; $oidResult = generateOidUuid($oid); echo "
"; echo "generateOidUuid('$oid')
"; echo "→ $oidResult"; echo '
'; // X500 Simple Example $distinguishedName = 'CN=John A. Marsh'; $x500Result = generateX500Uuid($distinguishedName); echo "
"; echo "generateX500Uuid('$distinguishedName')
"; echo "→ $x500Result"; echo '
'; // X500 Complex Example $complexDistinguishedName = 'CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US'; $complexX500Result = generateX500Uuid($complexDistinguishedName); echo "
"; echo "generateX500Uuid('$complexDistinguishedName')
"; echo "→ $complexX500Result"; echo '
'; // Employee Example $employeeId = 'EMP12345'; $employeeResult = generateEmployeeUuid($employeeId); echo "
"; echo "generateEmployeeUuid('$employeeId')
"; echo "→ $employeeResult"; echo '
'; // Customer Compound Key Example $phoneNumber = '+1234567890'; $email = 'john.marsh@example.com'; $customerResult = generateCustomerUuid($phoneNumber, $email); echo "
"; echo "generateCustomerUuid('$phoneNumber', '$email')
"; echo "→ $customerResult"; echo '
'; echo '
';

Complete Python Example (uuidv5_test.py)


import uuid
from typing import Optional

def generate_dns_uuid(hostname: str) -> str:
    """Generate a deterministic UUID using the DNS namespace.

    Args:
        hostname: The hostname to convert.
    Returns:
        The deterministic UUID for the given hostname.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_DNS, hostname))

def generate_url_uuid(url: str) -> str:
    """Generate a deterministic UUID using the URL namespace.

    Args:
        url: The URL to convert.
    Returns:
        The deterministic UUID for the given URL.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_URL, url))

def generate_oid_uuid(oid: str) -> str:
    """Generate a deterministic UUID using the OID namespace.

    Args:
        oid: The OID string to convert.
    Returns:
        The deterministic UUID for the given OID.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_OID, oid))

def generate_x500_uuid(distinguished_name: str) -> str:
    """Generate a UUIDv5 based on a Distinguished Name (DN) using the X.500 namespace.

    Args:
        distinguished_name: The distinguished name to convert.
    Returns:
        The deterministic UUID for the given distinguished name.
    """
    return str(uuid.uuid5(uuid.NAMESPACE_X500, distinguished_name))

def generate_employee_uuid(employee_id: str) -> str:
    """Convert a non-UUID employee ID into a UUID.

    Args:
        employee_id: The employee ID to convert.
    Returns:
        The deterministic UUID for the given employee ID.
    """
    distinguished_name = f'employeeNumber={employee_id}'
    return str(uuid.uuid5(uuid.NAMESPACE_X500, distinguished_name))

def generate_customer_uuid(phone_number: str, email: str) -> str:
    """Generate a UUID from a compound key (phone + email).

    Args:
        phone_number: The customer phone number.
        email: The customer email address.
    Returns:
        The deterministic UUID for the given phone number and email.
    """
    compound_dn = f'telephoneNumber={phone_number},mail={email}'
    return str(uuid.uuid5(uuid.NAMESPACE_X500, compound_dn))

def print_example_header():
    print("

Example Usage of UUID Generation Functions

") print("
") def print_example_result(function_name: str, args: str, result: str): print("
") print(f"{function_name}({args})
") print(f"→ {result}") print("
") def main(): print_example_header() # DNS Example hostname = 'threeleaf.com' dns_result = generate_dns_uuid(hostname) print_example_result('generate_dns_uuid', f"'{hostname}'", dns_result) # URL Example url = 'https://threeleaf.com/blog' url_result = generate_url_uuid(url) print_example_result('generate_url_uuid', f"'{url}'", url_result) # OID Example oid = '1.3.6.1.4.1' oid_result = generate_oid_uuid(oid) print_example_result('generate_oid_uuid', f"'{oid}'", oid_result) # X500 Simple Example distinguished_name = 'CN=John A. Marsh' x500_result = generate_x500_uuid(distinguished_name) print_example_result('generate_x500_uuid', f"'{distinguished_name}'", x500_result) # X500 Complex Example complex_dn = 'CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US' complex_result = generate_x500_uuid(complex_dn) print_example_result('generate_x500_uuid', f"'{complex_dn}'", complex_result) # Employee Example employee_id = 'EMP12345' employee_result = generate_employee_uuid(employee_id) print_example_result('generate_employee_uuid', f"'{employee_id}'", employee_result) # Customer Compound Key Example phone_number = '+1234567890' email = 'john.marsh@example.com' customer_result = generate_customer_uuid(phone_number, email) print_example_result('generate_customer_uuid', f"'{phone_number}', '{email}'", customer_result) print("
") if __name__ == "__main__": main()

Output (matches between PHP runnin on Linux and Python running on Macbook)


Example Usage of UUID Generation Functions

generate_dns_uuid('threeleaf.com')
→ d4a08aa5-9661-57ab-bf61-8f28be9b1f00
generate_url_uuid('https://threeleaf.com/blog')
→ b0277088-caf5-5a15-aa5d-5115112640c3
generate_oid_uuid('1.3.6.1.4.1')
→ 106dd502-8b3e-50db-80ed-1134f5c18eae
generate_x500_uuid('CN=John A. Marsh')
→ a953bc33-f538-5bb3-baa3-aaf081b5df93
generate_x500_uuid('CN=Madison Stutts,OU=Engineering,O=Example Corp,C=US')
→ b837ef27-6f4f-5a48-9419-badb502fb581
generate_employee_uuid('EMP12345')
→ 7aed46df-9f8e-5252-b340-34a0f15d33dd
generate_customer_uuid('+1234567890', 'john.marsh@example.com')
→ db23fe5d-ec32-5522-b563-f10847432d04

Listen to the podcast generated by NotebookLM

Understanding SAML: A Technical Guide for Developers

2024-11-22

Introduction


Security Assertion Markup Language (SAML) is a standard for exchanging authentication and authorization data between parties. It enables Single Sign-On (SSO), simplifying access to multiple services with a single set of credentials. For developers new to SAML, it can seem complex, but understanding its components and workflows can demystify the process and empower you to implement secure authentication systems.


This post will provide an overview of SAML, followed by a technical deep dive into its key components and how they interact.


What is SAML?


SAML is an XML-based framework for exchanging user authentication and authorization data between two main entities:

Service Provider (SP): The application or service a user wants to access.

Identity Provider (IdP): The entity responsible for authenticating the user and providing identity information to the SP.


The goal of SAML is to allow the SP to rely on the IdP to authenticate users, removing the need for the SP to handle authentication directly.


Key Concepts in SAML


1. SingleSignOnService (SSO)


The SingleSignOnService is a core feature of SAML. It allows users to authenticate once with the IdP and gain access to multiple SPs without re-entering credentials. This is achieved using SAML assertions, which are secure XML documents containing authentication and authorization data.


Workflow:

1. A user attempts to access a protected resource at the SP.

2. The SP generates a SAML Authentication Request and redirects the user to the IdP.

3. The IdP authenticates the user (e.g., via username/password or multi-factor authentication).

4. The IdP generates a SAML Assertion, signs it, and redirects the user back to the SP.

5. The SP validates the assertion and grants the user access.





2. SingleLogoutService (SLO)


The SingleLogoutService ensures that when a user logs out from one SP, they are also logged out from all other connected SPs and the IdP.


Workflow:

1. A user initiates logout at an SP.

2. The SP sends a SAML Logout Request to the IdP.

3. The IdP propagates the logout to other SPs where the user has active sessions.

4. The IdP sends a SAML Logout Response to the initiating SP to confirm the logout.


SLO is critical for maintaining session consistency and preventing orphaned sessions across services.







3. Service Provider (SP)


The SP is the consumer of SAML assertions. It relies on the IdP to authenticate users. SPs trust the IdP’s assertions because they are digitally signed, ensuring integrity and authenticity.


Key Responsibilities:

Generate SAML Authentication Requests.

Validate SAML Assertions received from the IdP.

Enforce authorization rules based on attributes in the assertion.


4. Identity Provider (IdP)


The IdP is the authority that authenticates users and issues SAML assertions. It is the cornerstone of trust in a SAML setup.


Key Responsibilities:

Authenticate users securely.

Provide identity information and attributes to SPs.

Manage the trust relationship with SPs, usually established via certificates.


5. SAML Federation


SAML federation refers to the establishment of trust relationships between multiple SPs and IdPs. This allows seamless interoperability in environments where multiple organizations or services share a common user base.


Federation typically involves:

Exchanging metadata files, which include details about endpoints, certificates, and supported bindings.

Agreeing on shared identifiers and attributes (e.g., email, user ID).

Setting up signing and encryption certificates for secure communication.




How Authentication Data is Transferred in SAML


The SAML protocol involves several key steps:

1. Authentication Request (AuthnRequest):

The SP sends an AuthnRequest to the IdP’s SingleSignOnService endpoint.

This request includes metadata like the SP’s unique identifier, requested bindings, and a unique ID.

2. User Authentication:

The IdP authenticates the user via its chosen method (e.g., credentials, biometrics).

Upon success, the IdP generates a SAML assertion.

3. SAML Assertion:

The assertion contains:

Subject: The authenticated user’s identifier (e.g., email or username).

Conditions: Validity period and audience restrictions.

Attributes: Optional user attributes (e.g., roles, groups).

Authentication Statement: Details about the authentication event.

The assertion is signed using the IdP’s private key for integrity and authenticity.

4. Response:

The IdP sends the SAML assertion in a SAML Response back to the SP, often via the user’s browser.

The SP validates the response using the IdP’s public key.

5. Access Grant:

Upon successful validation, the SP grants the user access to the requested resource.


SAML Bindings: Transporting Data


SAML defines several “bindings” for transporting messages between SPs and IdPs:

HTTP Redirect Binding: Typically used for AuthnRequests, where the request is encoded in a URL.

HTTP POST Binding: Used for SAML Responses, where the assertion is embedded in an HTML form.

SOAP Binding: Often used for back-channel communications like SingleLogoutService.


Each binding ensures secure and reliable transmission of data.


Security Considerations


Signing and Encryption: All assertions and responses should be signed, and sensitive data can be encrypted to prevent tampering and eavesdropping.

Replay Attacks: Use unique IDs and timestamps in SAML messages to prevent reuse.

Certificate Management: Regularly update and rotate signing and encryption certificates to maintain trust.


Practical Implementation Tips


1. Leverage Existing Libraries: Use established SAML libraries (e.g., simplesamlphp, OneLogin for Python, Sustainsys.Saml2 for .NET) to handle complex tasks like request generation and assertion validation.

2. Understand Metadata: Exchange and validate metadata files to establish trust between SPs and IdPs.

3. Test Extensively: Use tools like SAML-tracer or browser developer tools to debug SAML message flows.

4. Monitor Logs: Both SPs and IdPs should log SAML events for troubleshooting and auditing.


Conclusion


SAML is a robust framework for implementing SSO and federated identity management. By understanding its components—SP, IdP, assertions, and services like SSO and SLO—you can build secure and user-friendly authentication systems. While the initial learning curve may be steep, the benefits of implementing SAML make it worth the effort.


If you’re ready to get started, dive into the configuration and setup of your IdP and SP, and let SAML simplify authentication for your users.

Understanding SAML: A Technical Guide for Developers

2024-11-22

Introduction


Security Assertion Markup Language (SAML) is a standard for exchanging authentication and authorization data between parties. It enables Single Sign-On (SSO), simplifying access to multiple services with a single set of credentials. For developers new to SAML, it can seem complex, but understanding its components and workflows can demystify the process and empower you to implement secure authentication systems.


This post will provide an overview of SAML, followed by a technical deep dive into its key components and how they interact.


What is SAML?


SAML is an XML-based framework for exchanging user authentication and authorization data between two main entities:

Service Provider (SP): The application or service a user wants to access.

Identity Provider (IdP): The entity responsible for authenticating the user and providing identity information to the SP.


The goal of SAML is to allow the SP to rely on the IdP to authenticate users, removing the need for the SP to handle authentication directly.


Key Concepts in SAML


1. SingleSignOnService (SSO)


The SingleSignOnService is a core feature of SAML. It allows users to authenticate once with the IdP and gain access to multiple SPs without re-entering credentials. This is achieved using SAML assertions, which are secure XML documents containing authentication and authorization data.


Workflow:

1. A user attempts to access a protected resource at the SP.

2. The SP generates a SAML Authentication Request and redirects the user to the IdP.

3. The IdP authenticates the user (e.g., via username/password or multi-factor authentication).

4. The IdP generates a SAML Assertion, signs it, and redirects the user back to the SP.

5. The SP validates the assertion and grants the user access.





2. SingleLogoutService (SLO)


The SingleLogoutService ensures that when a user logs out from one SP, they are also logged out from all other connected SPs and the IdP.


Workflow:

1. A user initiates logout at an SP.

2. The SP sends a SAML Logout Request to the IdP.

3. The IdP propagates the logout to other SPs where the user has active sessions.

4. The IdP sends a SAML Logout Response to the initiating SP to confirm the logout.


SLO is critical for maintaining session consistency and preventing orphaned sessions across services.







3. Service Provider (SP)


The SP is the consumer of SAML assertions. It relies on the IdP to authenticate users. SPs trust the IdP’s assertions because they are digitally signed, ensuring integrity and authenticity.


Key Responsibilities:

Generate SAML Authentication Requests.

Validate SAML Assertions received from the IdP.

Enforce authorization rules based on attributes in the assertion.


4. Identity Provider (IdP)


The IdP is the authority that authenticates users and issues SAML assertions. It is the cornerstone of trust in a SAML setup.


Key Responsibilities:

Authenticate users securely.

Provide identity information and attributes to SPs.

Manage the trust relationship with SPs, usually established via certificates.


5. SAML Federation


SAML federation refers to the establishment of trust relationships between multiple SPs and IdPs. This allows seamless interoperability in environments where multiple organizations or services share a common user base.


Federation typically involves:

Exchanging metadata files, which include details about endpoints, certificates, and supported bindings.

Agreeing on shared identifiers and attributes (e.g., email, user ID).

Setting up signing and encryption certificates for secure communication.




How Authentication Data is Transferred in SAML


The SAML protocol involves several key steps:

1. Authentication Request (AuthnRequest):

The SP sends an AuthnRequest to the IdP’s SingleSignOnService endpoint.

This request includes metadata like the SP’s unique identifier, requested bindings, and a unique ID.

2. User Authentication:

The IdP authenticates the user via its chosen method (e.g., credentials, biometrics).

Upon success, the IdP generates a SAML assertion.

3. SAML Assertion:

The assertion contains:

Subject: The authenticated user’s identifier (e.g., email or username).

Conditions: Validity period and audience restrictions.

Attributes: Optional user attributes (e.g., roles, groups).

Authentication Statement: Details about the authentication event.

The assertion is signed using the IdP’s private key for integrity and authenticity.

4. Response:

The IdP sends the SAML assertion in a SAML Response back to the SP, often via the user’s browser.

The SP validates the response using the IdP’s public key.

5. Access Grant:

Upon successful validation, the SP grants the user access to the requested resource.


SAML Bindings: Transporting Data


SAML defines several “bindings” for transporting messages between SPs and IdPs:

HTTP Redirect Binding: Typically used for AuthnRequests, where the request is encoded in a URL.

HTTP POST Binding: Used for SAML Responses, where the assertion is embedded in an HTML form.

SOAP Binding: Often used for back-channel communications like SingleLogoutService.


Each binding ensures secure and reliable transmission of data.


Security Considerations


Signing and Encryption: All assertions and responses should be signed, and sensitive data can be encrypted to prevent tampering and eavesdropping.

Replay Attacks: Use unique IDs and timestamps in SAML messages to prevent reuse.

Certificate Management: Regularly update and rotate signing and encryption certificates to maintain trust.


Practical Implementation Tips


1. Leverage Existing Libraries: Use established SAML libraries (e.g., simplesamlphp, OneLogin for Python, Sustainsys.Saml2 for .NET) to handle complex tasks like request generation and assertion validation.

2. Understand Metadata: Exchange and validate metadata files to establish trust between SPs and IdPs.

3. Test Extensively: Use tools like SAML-tracer or browser developer tools to debug SAML message flows.

4. Monitor Logs: Both SPs and IdPs should log SAML events for troubleshooting and auditing.


Conclusion


SAML is a robust framework for implementing SSO and federated identity management. By understanding its components—SP, IdP, assertions, and services like SSO and SLO—you can build secure and user-friendly authentication systems. While the initial learning curve may be steep, the benefits of implementing SAML make it worth the effort.


If you’re ready to get started, dive into the configuration and setup of your IdP and SP, and let SAML simplify authentication for your users.

Creating Custom Zip Files with Git

2024-11-15


Managing project distributions often requires creating archives that exclude certain files or directories. Git offers a streamlined approach to this through the .gitattributes file, enabling precise control over the contents of your archives.

Why Use Git for Custom Archives?

Utilizing Git's archiving capabilities provides several advantages:

  • Consistency: Ensures that archives are generated from a specific commit, maintaining version integrity.
  • Automation: Facilitates the creation of scripts for automated deployment processes.
  • Customization: Allows exclusion of unnecessary files, resulting in cleaner and more efficient distributions.

Understanding the .gitattributes Configuration

The .gitattributes file defines how Git handles various files within your repository. Below is an example configuration:

Let's break down this configuration:

  • General Settings:
    • * text=auto eol=lf: Normalizes line endings to LF, ensuring consistency across different operating systems.
    • *.php diff=php: Specifies that PHP files should use PHP-specific diff settings.
  • Export Ignoring: The export-ignore attribute tells Git to exclude specified files or directories from the archive:
    • .editorconfig, .gitattributes, .gitignore: Configuration files not needed in the distribution.
    • /.git, /.idea: Version control and IDE-specific directories.
    • /tests, /util, /vendor: Directories containing tests, utilities, and dependencies that may not be necessary for the end-user.
    • phpunit.xml: Configuration file for PHPUnit, typically not required in the final product.

Creating the Archive

With the .gitattributes file configured, you can create a zip archive using the following command:

git archive --worktree-attributes --format=zip --output="$(basename "$PWD").zip" HEAD

This command generates a zip file of the current project, excluding the files and directories specified with the export-ignore attribute.

Conclusion

Leveraging Git's archiving features through the .gitattributes file allows for efficient and customized distribution of your projects. By specifying which files to exclude, you can create cleaner archives tailored to your deployment needs.

Creating Custom Zip Files with Git

2024-11-15


Managing project distributions often requires creating archives that exclude certain files or directories. Git offers a streamlined approach to this through the .gitattributes file, enabling precise control over the contents of your archives.

Why Use Git for Custom Archives?

Utilizing Git's archiving capabilities provides several advantages:

  • Consistency: Ensures that archives are generated from a specific commit, maintaining version integrity.
  • Automation: Facilitates the creation of scripts for automated deployment processes.
  • Customization: Allows exclusion of unnecessary files, resulting in cleaner and more efficient distributions.

Understanding the .gitattributes Configuration

The .gitattributes file defines how Git handles various files within your repository. Below is an example configuration:

Let's break down this configuration:

  • General Settings:
    • * text=auto eol=lf: Normalizes line endings to LF, ensuring consistency across different operating systems.
    • *.php diff=php: Specifies that PHP files should use PHP-specific diff settings.
  • Export Ignoring: The export-ignore attribute tells Git to exclude specified files or directories from the archive:
    • .editorconfig, .gitattributes, .gitignore: Configuration files not needed in the distribution.
    • /.git, /.idea: Version control and IDE-specific directories.
    • /tests, /util, /vendor: Directories containing tests, utilities, and dependencies that may not be necessary for the end-user.
    • phpunit.xml: Configuration file for PHPUnit, typically not required in the final product.

Creating the Archive

With the .gitattributes file configured, you can create a zip archive using the following command:

git archive --worktree-attributes --format=zip --output="$(basename "$PWD").zip" HEAD

This command generates a zip file of the current project, excluding the files and directories specified with the export-ignore attribute.

Conclusion

Leveraging Git's archiving features through the .gitattributes file allows for efficient and customized distribution of your projects. By specifying which files to exclude, you can create cleaner archives tailored to your deployment needs.

Mammouth AI: One Subscription to Rule Them All?

2024-09-24

Introduction

I’ve recently become interested in the world of artificial intelligence. Having worked with several AI tools and purchased subscriptions to ChatGPT and MidJourney, I was intrigued when I started seeing ads for Mammouth AI — a platform that promises access to multiple top-tier AI models for a low monthly fee.

However, with little publicly available information and no free tier options, I was hesitant to dive in without knowing more. But for the sake of my YouTube and blog audience, I decided to take the plunge, sign up, and share my experience. Here’s what I found.

What Is Mammouth AI?

Mammouth AI positions itself as a one-stop platform for accessing various large language models (LLMs) and image generators. The homepage makes some bold claims, offering access to industry leaders like GPT-4, Claude AI, and MidJourney for just €10 a month. The platform promises a user-friendly interface that simplifies the use of these models, even for those who may not be particularly tech-savvy.

Signing Up and Getting Started

The sign-up process was straightforward and required committing to a monthly subscription. I decided to go ahead and choose the Starter plan at €10/month to give you all a firsthand look at what Mammouth AI has to offer.

First Impressions

Upon logging in, I found a clean, straightforward interface that indeed seems designed for ease of use. Mammouth AI offers features like one-click reprompting, where you can easily transfer a prompt from one model to another to see different outputs. 

Exploring Features

  • One/click Reprompting: I tested this by generating a Halloween-themed blog post across several models, including Llama, Claude AI, and Mistral. The process was seamless, and the reprompting feature worked well, providing varied results as expected.
  • Multilingual Capabilities: Since I’m learning Portuguese, I was curious about the platform’s ability to translate content. I had the Mistral model translate the Halloween blog post into European Portuguese, and the result looked good and well-formatted. The translation feature is likely a function of the models themselves rather than a unique offering from Mammouth AI.
  • File Upload and Data Analysis: I attempted to upload a CSV file for analysis, expecting to create a graph. Unfortunately, the platform doesn’t seem to support CSV uploads directly, forcing me to convert the file to a PDF. Even then, none of the models were able to generate the requested graph, with ChatGPT 4o providing Python scripts instead of a visual output. LLMs like ChatGPT can typically read text files natively, but this is restricted through the Mammouth AI interface. I have created graphs in ChatGPT numerous times, so I suspect that this limitation is do to how Mammouth AI in communicating with ChatGPT on the backend.
  • Image and File Upload: I also tested the platform’s ability to analyze images by uploading a photo of our dog. The models offered reasonable guesses about her breed, with detailed descriptions that matched her appearance. This feature seemed more polished than the file upload and analysis tool.
  • Image Generation: I tested the image generation capabilities by asking the models to create a Halloween scene featuring our dog. I used models like Stable Diffusion, DALL-E, and FLUX. While the results varied, based on the variations between the models themselves, each captured different elements of the prompt with varying degrees of success. However, MidJourney’s integration seemed to struggle, and I was unable to successfully generate an image while recording the video. Edit: I was able to generate an image with MidJourney after recording the video.
  • Chat History: The history bar on the left of the interface is welcome, but is very basic with chat naming based on the first few words of the prompt, and delete as the only option. Noted improvements would include renaming capability and a warning before permanently deleting a chat.

Areas for Improvement

While Mammouth AI shows promise, there are a few areas where it could improve:

  • No Free Tier: The absence of a free trial makes it difficult for users to test the platform without committing to a subscription.
  • Limited File Support: The platform’s inability to handle common file types like CSV directly is a significant drawback, especially for users looking to perform data analysis.
  • Unstable Integrations: Some integrations, like MidJourney, were unresponsive during my tests, indicating that the platform might still need some refinement.
  • Interface: History renaming and delete warnings would be easy and helpful improvements to that feature. Native platforms offer a richer experience including image generation controls and special code display boxes that are not available in Mammouth AI.

Final Thoughts

Mammouth AI offers a compelling proposition: access to multiple AI models for a single, affordable subscription. For newbies, AI enthusiasts, and developers who want to experiment with various LLMs without juggling multiple subscriptions, this could be a valuable tool. However, the platform’s limitations mean that it may not yet be the perfect solution for everyone.

I’m still on the fence about committing to Mammouth AI myself, but I plan to continue experimenting with the platform, especially once the promised web search feature becomes available. I’ll be sure to share my findings in a follow-up video or blog post.

Conclusion

Thank you for joining me on this first exploration of Mammouth AI. If you found this review helpful, be sure to check out my YouTube channel, where I’ll continue to explore AI tools, programming tips, and more. If you’d like to support my work, you can visit ThreeLeaf.com and check out the affiliate links on my page.

Mammouth AI: One Subscription to Rule Them All?

2024-09-24

Introduction

I’ve recently become interested in the world of artificial intelligence. Having worked with several AI tools and purchased subscriptions to ChatGPT and MidJourney, I was intrigued when I started seeing ads for Mammouth AI — a platform that promises access to multiple top-tier AI models for a low monthly fee.

However, with little publicly available information and no free tier options, I was hesitant to dive in without knowing more. But for the sake of my YouTube and blog audience, I decided to take the plunge, sign up, and share my experience. Here’s what I found.

What Is Mammouth AI?

Mammouth AI positions itself as a one-stop platform for accessing various large language models (LLMs) and image generators. The homepage makes some bold claims, offering access to industry leaders like GPT-4, Claude AI, and MidJourney for just €10 a month. The platform promises a user-friendly interface that simplifies the use of these models, even for those who may not be particularly tech-savvy.

Signing Up and Getting Started

The sign-up process was straightforward and required committing to a monthly subscription. I decided to go ahead and choose the Starter plan at €10/month to give you all a firsthand look at what Mammouth AI has to offer.

First Impressions

Upon logging in, I found a clean, straightforward interface that indeed seems designed for ease of use. Mammouth AI offers features like one-click reprompting, where you can easily transfer a prompt from one model to another to see different outputs. 

Exploring Features

  • One/click Reprompting: I tested this by generating a Halloween-themed blog post across several models, including Llama, Claude AI, and Mistral. The process was seamless, and the reprompting feature worked well, providing varied results as expected.
  • Multilingual Capabilities: Since I’m learning Portuguese, I was curious about the platform’s ability to translate content. I had the Mistral model translate the Halloween blog post into European Portuguese, and the result looked good and well-formatted. The translation feature is likely a function of the models themselves rather than a unique offering from Mammouth AI.
  • File Upload and Data Analysis: I attempted to upload a CSV file for analysis, expecting to create a graph. Unfortunately, the platform doesn’t seem to support CSV uploads directly, forcing me to convert the file to a PDF. Even then, none of the models were able to generate the requested graph, with ChatGPT 4o providing Python scripts instead of a visual output. LLMs like ChatGPT can typically read text files natively, but this is restricted through the Mammouth AI interface. I have created graphs in ChatGPT numerous times, so I suspect that this limitation is do to how Mammouth AI in communicating with ChatGPT on the backend.
  • Image and File Upload: I also tested the platform’s ability to analyze images by uploading a photo of our dog. The models offered reasonable guesses about her breed, with detailed descriptions that matched her appearance. This feature seemed more polished than the file upload and analysis tool.
  • Image Generation: I tested the image generation capabilities by asking the models to create a Halloween scene featuring our dog. I used models like Stable Diffusion, DALL-E, and FLUX. While the results varied, based on the variations between the models themselves, each captured different elements of the prompt with varying degrees of success. However, MidJourney’s integration seemed to struggle, and I was unable to successfully generate an image while recording the video. Edit: I was able to generate an image with MidJourney after recording the video.
  • Chat History: The history bar on the left of the interface is welcome, but is very basic with chat naming based on the first few words of the prompt, and delete as the only option. Noted improvements would include renaming capability and a warning before permanently deleting a chat.

Areas for Improvement

While Mammouth AI shows promise, there are a few areas where it could improve:

  • No Free Tier: The absence of a free trial makes it difficult for users to test the platform without committing to a subscription.
  • Limited File Support: The platform’s inability to handle common file types like CSV directly is a significant drawback, especially for users looking to perform data analysis.
  • Unstable Integrations: Some integrations, like MidJourney, were unresponsive during my tests, indicating that the platform might still need some refinement.
  • Interface: History renaming and delete warnings would be easy and helpful improvements to that feature. Native platforms offer a richer experience including image generation controls and special code display boxes that are not available in Mammouth AI.

Final Thoughts

Mammouth AI offers a compelling proposition: access to multiple AI models for a single, affordable subscription. For newbies, AI enthusiasts, and developers who want to experiment with various LLMs without juggling multiple subscriptions, this could be a valuable tool. However, the platform’s limitations mean that it may not yet be the perfect solution for everyone.

I’m still on the fence about committing to Mammouth AI myself, but I plan to continue experimenting with the platform, especially once the promised web search feature becomes available. I’ll be sure to share my findings in a follow-up video or blog post.

Conclusion

Thank you for joining me on this first exploration of Mammouth AI. If you found this review helpful, be sure to check out my YouTube channel, where I’ll continue to explore AI tools, programming tips, and more. If you’d like to support my work, you can visit ThreeLeaf.com and check out the affiliate links on my page.