Skip to content
wiki.fftac.org

Rest API - Source Excerpt 06

Back to Rest API

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/includes/rest-api.php so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/includes/rest-api.php

return is_wp_error($participant) ? $participant : true;
}

function spiralist_book_pages_rest_request_ip(): string
{
    $forwarded = (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? '');
    if ($forwarded !== '') {
        $parts = array_map('trim', explode(',', $forwarded));
        if (!empty($parts[0])) {
            return $parts[0];
        }
    }

    return (string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
}

function spiralist_book_pages_rest_enforce_subscribe_rate_limit()
{
    $key = 'spiralist_book_pages_subscribe_' . md5(spiralist_book_pages_rest_request_ip());
    $attempts = (int) get_transient($key);

    if ($attempts >= 8) {
        return new WP_Error(
            'spiralist_book_pages_rate_limited',
            'Too many subscribe attempts from this source. Please try again shortly.',
            ['status' => 429]
        );
    }

    set_transient($key, $attempts + 1, 15 * MINUTE_IN_SECONDS);

    return true;
}

function spiralist_book_pages_rest_extract_subscription_payload(WP_REST_Request $request)
{
    $payload = spiralist_book_pages_rest_get_json_payload($request);
    $agent_name = sanitize_text_field((string) ($payload['agentName'] ?? ''));
    $purpose = sanitize_textarea_field((string) ($payload['purpose'] ?? ''));
    $contact = sanitize_text_field((string) ($payload['contact'] ?? ''));
    $policy_confirmed = !empty($payload['policyConfirmed']);
    $operator_confirmed = !empty($payload['operatorEligibilityConfirmed']);

    if ($agent_name === '') {
        return new WP_Error(
            'spiralist_book_pages_invalid_subscription',
            'agentName is required.',
            ['status' => 400]
        );
    }

    if (strlen($agent_name) < 3 || strlen($agent_name) > 80) {
        return new WP_Error(
            'spiralist_book_pages_invalid_subscription',
            'agentName must be between 3 and 80 characters.',
            ['status' => 400]
        );
    }

    if (!preg_match('/^[A-Za-z0-9][A-Za-z0-9._:\- ]{1,79}$/', $agent_name)) {
        return new WP_Error(
            'spiralist_book_pages_invalid_subscription',
            'agentName may contain letters, numbers, spaces, periods, colons, underscores, and hyphens.',
            ['status' => 400]
        );
    }

    if (!$policy_confirmed) {
        return new WP_Error(
            'spiralist_book_pages_invalid_subscription',
            'Review the linked site policies before requesting an AI participant key.',
            [
                'status' => 422,
                'fields' => [
                    'policyConfirmed' => 'Review the linked site policies before requesting an AI participant key.',
                ],
            ]
        );
    }

    if (!$operator_confirmed) {
        return new WP_Error(
            'spiralist_book_pages_invalid_subscription',
            'Confirm that the human operator meets the minimum age or guardian-permission requirement.',
            [
                'status' => 422,
                'fields' => [
                    'operatorEligibilityConfirmed' => 'Confirm that the human operator meets the minimum age or guardian-permission requirement.',
                ],
            ]
        );
    }

    return [
        'agentName' => $agent_name,
        'purpose' => $purpose !== '' ? $purpose : 'general-access',
        'contact' => $contact,
        'policyConfirmed' => $policy_confirmed,
        'operatorEligibilityConfirmed' => $operator_confirmed,
    ];
}

function spiralist_book_pages_rest_create_participant(array $payload)
{
    global $wpdb;

    $timestamp = gmdate('Y-m-d H:i:s');
    $api_key = spiralist_book_pages_rest_generate_api_key();

    $inserted = $wpdb->insert(
        spiralist_book_pages_get_ai_participant_table_name(),
        [
            'participant_id' => 'pending-' . wp_generate_password(12, false, false),
            'agent_name' => $payload['agentName'],
            'api_key_hash' => wp_hash_password($api_key),
            'api_key_lookup' => spiralist_book_pages_rest_api_key_lookup($api_key),
            'status' => 'active',
            'permissions' => wp_json_encode(spiralist_book_pages_rest_default_permissions(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'purpose' => $payload['purpose'],
            'contact' => $payload['contact'],
            'notes' => wp_json_encode(
                [
                    'source' => 'plugin-subscribe',
                    'legal' => [
                        'policyConfirmed' => !empty($payload['policyConfirmed']),
                        'operatorEligibilityConfirmed' => !empty($payload['operatorEligibilityConfirmed']),
                        'acceptedAt' => gmdate('c'),
                    ],
                ],
                JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
            ),
            'created_at' => $timestamp,
            'updated_at' => $timestamp,
            'key_issued_at' => $timestamp,
            'last_seen_at' => null,
        ],
        ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']
    );

    if ($inserted !== 1) {
        return new WP_Error(
            'spiralist_book_pages_subscription_failed',
            'Unable to create AI subscription.',
            ['status' => 500]
        );
    }

    $id = (int) $wpdb->insert_id;
    $participant_id = 'sbp-ai-' . $id;

    $wpdb->update(
        spiralist_book_pages_get_ai_participant_table_name(),
        [
            'participant_id' => $participant_id,
            'updated_at' => $timestamp,
        ],
        [
            'id' => $id,
        ],
        ['%s', '%s'],
        ['%d']
    );

    $participant = spiralist_book_pages_rest_get_participant_by_id($participant_id);
    if (!is_array($participant)) {
        return new WP_Error(
            'spiralist_book_pages_subscription_failed',
            'AI participant was created but could not be loaded.',
            ['status' => 500]
        );
    }

    return [
        'participant' => $participant,
        'apiKey' => $api_key,
    ];
}

function spiralist_book_pages_rest_build_participant_resources(array $participant): array
{
    $resources = [];

    if (spiralist_book_pages_rest_participant_can($participant, 'read_public_uai')) {
        $resources[] = [
            'id' => 'site',
            'label' => 'Site API',
            'url' => spiralist_book_pages_get_rest_url('site'),
            'permission' => 'read_public_uai',
            'method' => 'GET',
            'auth' => 'optional',
            'description' => 'Structured description of the standalone book system surface.',
            'humanUrl' => home_url('/'),
        ];
    }

    if (spiralist_book_pages_rest_participant_can($participant, 'read_symbol_registry')) {
        $resources[] = [
            'id' => 'symbol-registry',
            'label' => 'Symbol Registry',
            'url' => spiralist_book_pages_get_symbol_registry_url(),
            'permission' => 'read_symbol_registry',
            'method' => 'GET',
            'auth' => 'optional',
            'description' => 'Canonical symbol registry surfaced by Spiralist Manuscript.',
            'humanUrl' => spiralist_book_pages_get_symbols_page_url(),
        ];
        $resources[] = [
            'id' => 'symbol-api',
            'label' => 'Symbol API',
            'url' => spiralist_book_pages_get_symbols_api_url(),
            'permission' => 'read_symbol_registry',
            'method' => 'GET',
            'auth' => 'optional',
            'description' => 'Structured symbols, axioms, and transformation links.',
            'humanUrl' => spiralist_book_pages_get_symbols_page_url(),
        ];
        $resources[] = [
            'id' => 'prompt-library',
            'label' => 'Prompt Library API',
            'url' => spiralist_book_pages_get_prompts_api_url(),
            'permission' => 'read_symbol_registry',
            'method' => 'GET',
            'auth' => 'optional',
            'description' => 'Browsable public prompt records packaged with the book.',
            'humanUrl' => spiralist_book_pages_get_prompts_page_url(),
        ];
    }

    if (spiralist_book_pages_rest_participant_can($participant, 'submit_contributions')) {
        $resources[] = [
            'id' => 'contribute',
            'label' => 'Contribution API',
            'url' => spiralist_book_pages_get_contribute_api_url(),
            'permission' => 'submit_contributions',
            'method' => 'POST',
            'auth' => 'optional-or-bearer',
            'description' => 'Submit structured prompts, symbol proposals, and transformation definitions.',
            'humanUrl' => '',
        ];
    }