General integration — OAuth, SMTP and GenAI

Register one application and use its server-side client secret for all three services. User login and GenAI consent remain required.

Complete deployment, SMTP and agent contract

JSON for coding agents · Plain text

{
  "title": "SKO unified integration: Google OAuth, SMTP and user GenAI configuration",
  "version": "2.10.0",
  "origin": "https://oauth.skoonline.org",
  "agentInstruction": "Integrate Google OAuth, SMTP and user-owned GenAI through https://oauth.skoonline.org. Follow https://oauth.skoonline.org/generalInstruction.json. Register one client with OAuth, SMTP and GenAI permissions and use its single server-side secret for all three services. Use the hosted service UI, preserve existing app identities and roles, keep credentials server-side, and verify persistence after sign-out and restart. Implement configured optional per-app GCS storage and primary/debug/webhooks xAPI LRS settings using the optionalConnections instructions; leave unused profiles inactive. For personal text inference use backend-cached execution sessions and direct provider calls; follow directExecution below. Do not claim live deployment from a source commit.",
  "links": {
    "html": "https://oauth.skoonline.org/generalInstruction",
    "json": "https://oauth.skoonline.org/generalInstruction.json",
    "text": "https://oauth.skoonline.org/generalInstruction.txt",
    "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js"
  },
  "directExecution": {
    "standard": "Cache an explicitly authorized personal text execution session on the consuming app backend and call the provider directly; no per-generation gateway inference request.",
    "endpoint": "https://oauth.skoonline.org/genai/v1/execution-session",
    "sdkHelper": "https://oauth.skoonline.org/genaiInstruction/direct-execution.js",
    "cache": "Verified user/session scope, up to 15 minutes; coalesce loads; invalidate after local settings changes, reconnect, disconnect and logout.",
    "limits": "Requires fresh explicit direct-execution consent. No shared hosted-key export or central credit enforcement. Provider keys remain server-only; rotating them is required to revoke keys already handed out. Existing app adapters must be migrated individually."
  },
  "optionalConnections": {
    "precedence": "Register optional GCS and primary/debug/webhooks LRS settings at oauth.skoonline.org. In each app set USE_OAUTH_GCS=true to use registered storage and USE_OAUTH_LRS=true to use registered LRS profiles. These switches are independent; false or unset uses that app's original configuration. The backend calls POST /genai/services/config with Basic PROXY_CLIENT_ID:PROXY_CLIENT_SECRET and JSON {gcs:true,lrs:true} matching enabled switches. Use loadOptionalConnections from genai/sdk/optional-connections.js at server startup; it retrieves only requested settings. A complete gateway profile takes precedence, otherwise the complete original profile is the fallback. Never mix profiles. Network errors, HTTP 429 and 5xx use original configuration; 4xx authentication/configuration errors are surfaced. This selects destinations at initialization; it does not migrate GCS data or perform per-operation storage failover. Keep credentials and responses server-side.",
    "setup": "In Connect a new app or App settings, expand Optional storage and xAPI LRS connections. All four connections and every field are optional. Each LRS has URL, User, and Secret / Password. Leave unused settings blank.",
    "delivery": "These settings are per-app deployment configuration. Set USE_OAUTH_GCS and USE_OAUTH_LRS independently on the app server, then load the registered configuration with the SDK helper. The app backend retrieves the requested settings from POST /genai/services/config using its existing client credentials; OAuth tokens and GenAI grants do not include these settings.",
    "variables": {
      "USE_OAUTH_GCS": "true selects registered bucket; false/unset uses original",
      "USE_OAUTH_LRS": "true selects registered LRS profiles; false/unset uses originals",
      "credentials": [
        "OAUTH_BASE_URL",
        "PROXY_CLIENT_ID",
        "PROXY_CLIENT_SECRET"
      ],
      "originalBucket": "GCS_BUCKET",
      "originalLRSPrefixes": [
        "XAPI_LRS",
        "DEBUG_XAPI_LRS",
        "WEBHOOK_XAPI_LRS"
      ]
    },
    "storage": "Use the app runtime Google Cloud service account/Application Default Credentials with @google-cloud/storage. GCS_BUCKET is a bucket name, not gs:// or a URL. Grant appropriate private bucket permissions separately. An unset bucket leaves existing storage behavior unchanged; never silently replace durable storage with memory.",
    "xapi": "Read each LRS independently on the app backend. An empty or partial gateway profile uses the complete original profile as fallback. If neither is complete, keep that connection inactive. Construct Basic authentication from base64(User + \":\" + Secret/Password). Append statements to the configured xAPI base URL after normalizing its trailing slash. Send Content-Type: application/json and X-Experience-API-Version: 1.0.3 for xAPI 1.0.3 statements. Use valid actor, verb and object; obtain learner identity from a verified app session.",
    "routing": "Primary receives intended learning/assessment statements; debug receives explicitly enabled diagnostics with minimized data; webhooks receives statements for verified webhook events. Do not fan out automatically or fall back between profiles. The webhook LRS is an outbound destination, not an incoming webhook registration; authenticate incoming webhooks in the receiving app.",
    "reliability": "Generate and persist a statement UUID before delivery. For retry-safe xAPI 1.0.3 delivery, PUT /statements?statementId=<same UUID> with the same statement. Use bounded timeouts and an app-owned durable outbox for retries; classify failures and avoid blocking the learning activity. Do not claim delivery until HTTP success.",
    "secrets": "Never expose LRS credentials in browser bundles, public JSON, URLs, logs or commits. The administrator-only app detail route decrypts saved values for editing; lists and usage omit them. Copy into server secret configuration. Existing config updates omitting optionalConnections preserve values; explicit blanks clear fields.",
    "verification": [
      "All profiles blank: existing OAuth, SMTP, GenAI and learning flow still work.",
      "Bucket configured: explicitly verify private read/write with the app service account.",
      "Each LRS: explicit synthetic test, validate its target and authentication, confirm receipt independently.",
      "Check partial settings, invalid credentials, timeout and repeated delivery of the same statement ID.",
      "Check webhook signature/authentication and deduplicate the incoming event before creating a statement."
    ]
  },
  "connectionTesting": {
    "widget": "Supply genai.testConnection(id) as a backend adapter. Your AI offers Test generation for text and Check provider / model for other saved capabilities. No test runs automatically.",
    "endpoint": "POST /genai/v1/connections/:id/test with user Bearer token and app X-Client-Authorization. Enforce app-session authentication and exact Origin at the calling app adapter. The proxy checks current grant scope before testing.",
    "results": "generation_verified means a small text generation returned nonempty text; normal accounting applies. model_listed means discovery authenticated and included the model, not that generation works. unverified or unsupported never means success. Results are point-in-time and no model fallback occurs."
  },
  "personalAIConfiguration": {
    "version": "1.0.0",
    "portability": "The portal wizard exports credential-free configuration JSON and imports it with explicit connection mapping and review. POST /genai/api/configuration/import-preview accepts {document,mapping?}; it returns ready, requirements and a current-revision configuration without saving. Apply through revision-checked PUT /genai/api/configuration. Names do not establish ownership; create missing connections separately. Keys, grants and tokens are never imported.",
    "schema": "https://oauth.skoonline.org/genai/schemas/personal-ai-use-configuration.v1.json",
    "portal": "GET/PUT /genai/api/configuration. Revision-safe, credential-free connection inventory plus uses[capability]: {defaultConnection,backupConnections,fallbackMode}. Omitted uses are preserved. Inventory is server-owned; create credentials through the connection wizard.",
    "apps": "GET /genai/v1/connections includes configuration scoped to the current grant and effective routing. Existing APIs remain compatible. Use /genai/v1/routing for app overrides, not portal-wide configuration writes.",
    "migration": "Existing profiles/defaults/backups project to schema v1 without deleting credentials. Schema metadata drives wizard categories, source groups and stages. Unknown schema versions are rejected; consent and execution validation remain separate."
  },
  "intendedUses": {
    "request": "Send intended_uses: [\"text\",\"image\",\"speech\"] from the app backend on POST /genai/services/sso/start, alongside its existing proof, expected_email, state, callback and PKCE fields. Full-page /genai/connect accepts intended_uses=text,image,speech. SDK: link(callback, {intendedUses:[\"text\",\"speech\"]}).",
    "identifiers": [
      "text",
      "image",
      "speech",
      "transcription",
      "realtime",
      "video",
      "embeddings"
    ],
    "behavior": "The wizard focuses inventory, intended-use assignments, connection capabilities and consent suggestions on the requested uses. speech means TTS. Omitted context preserves all uses for older clients; unknown or empty lists return 400. Other saved user configurations remain unchanged.",
    "security": "This is setup context, not permission or execution support. User consent, connection scope, capability checks and verified identity remain mandatory. Configure requested uses on the app server and align the shared widget capabilities with implemented execution support."
  },
  "capabilityFiltering": {
    "behavior": "Select an AI use before provider/model. Provider templates are filtered by configuration hints; other providers remain available behind Show other providers (unverified). Discovery receives capability and returns matching models, unverifiedModels and excludedModels. Metadata and conservative model-family rules guide filtering, not account permissions or live execution verification.",
    "unknowns": "Users can explicitly show unverified models or enter a deployment ID. Known incompatible OpenAI model families are rejected on save. Input vision support is not image output support. Voice output metadata alone does not establish transcription or realtime endpoint compatibility.",
    "futureWorkflow": "An LLM may recommend next steps using discovered choices, but its recommendation is not provider capability evidence. Validate actual IDs, schemas, credentials and authorization independently before saving or executing."
  },
  "setupWorkflow": {
    "assistant": "Set LLM_PROVIDER, LLM_MODEL, LLM_ENDPOINT and LLM_API_KEY on the proxy. For azure, an empty LLM_API_KEY falls back to AZURE_OPENAI_API_KEY; model/endpoint also accept Azure variables. AZURE_OPENAI_API_VERSION is retained. A blank GENAI_MAX_INFLIGHT defaults to 12.",
    "allowance": "LLM_ALLOWANCE_TOKENS defaults to 100000 tokens per authenticated user per UTC month. Assistant usage is isolated from personal connection balances; exhaustion blocks further assistance. Zero internal tariffs do not mean Azure is free. No automatic backup or retry is implemented.",
    "flow": "Open Add connection → Let AI guide my setup. Describe the goal without secrets. The server assistant recommends a validated capability/provider and next steps. User explicitly applies the suggestion, then enters credentials, discovers models and reviews before saving. Recommendations are not live verification.",
    "privacy": "Goal descriptions are sent to the configured assistant; API keys and draft connection values are not. The assistant is not exposed as a connection that calling apps can authorize."
  },
  "setupAssistance": {
    "behavior": "The hosted wizard explains intended uses and every configuration field. Optional values default to provider behavior. Field-level Ask AI is opt-in, using a user-selected saved or hosted text connection with normal usage accounting. Without a text connection, static guidance remains available.",
    "privacy": "Only allowlisted field name, capability and provider metadata go into assistance prompts. Draft keys, endpoints and field values are excluded. AI advice is displayed as text and never automatically applied; model lists and provider documentation remain authoritative."
  },
  "aiRouting": {
    "capabilities": [
      "text",
      "image",
      "speech",
      "transcription",
      "realtime",
      "video",
      "embeddings"
    ],
    "portal": "https://oauth.skoonline.org/genai/",
    "setup": "In the centrally hosted setup portal, use the Defaults & backups wizard: AI use → Default & backups → Review → Save. Add missing connections through its provider/model wizard, then return to assign roles. Save one default plus up to three ordered backups per capability. Up to 100 connections can be stored. Voice/language/format and other capability settings are edited on each connection.",
    "read": "GET /genai/v1/connections returns routing[capability]: {primary,backups,fallbackMode,source,needsDefault}. Connections are filtered by the current app grant, current existence and matching capability. An unavailable default remains empty: never silently promote a backup.",
    "override": "PUT /genai/v1/routing with {capability,primary,backups} saves an app-specific override for this user. Send {capability,inherit:true} to restore their shared preferences. Use user Bearer token and X-Client-Authorization on the backend. SDK: setRouting(accessToken, options).",
    "execution": "Routing is a configuration contract. Existing selection APIs retain their behavior. Apps adopting routing must explicitly use routing.primary as the request connection. Backups require an explicit user choice; do not retry ambiguous failures, switch vendors automatically or assume modality execution support. Embedding models/dimensions must remain compatible with the index.",
    "authorization": "Grant approval is required independently for every primary and backup. Never expose keys. Account preferences do not authorize every app under the domain. Configuration of image/audio/video does not imply that inference for that capability is implemented by this gateway."
  },
  "sharedSettingsShell": {
    "connectionDisplay": "Your AI lists each returned authorized connection with model, configured capability, intended use and app selection status. Pass optional intendedUse for a connection-specific description; otherwise the widget supplies capability-based guidance. Pass routing to show default/backup roles. Unsupported capabilities are labeled, not offered as selectors.",
    "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js",
    "export": "mountAccountLauncher",
    "contract": "mountAccountLauncher(host, {account, smtp, genai, appModels, t}) returns {open, refresh, destroy}. The proxy owns the launcher, native dialog, header, tabs, scrolling, service widgets and app-model selector. Mount once and destroy on unmount. Do not reproduce this UI in the calling app.",
    "appModels": "Optional {load, select, openLocal}. load returns {models:[{id,displayName,available,locked}],current,disabledReason,localWizard,message}. select(id) persists through the app backend. openLocal() optionally returns a promise resolving when the app-specific local-runtime wizard closes. This legacy wizard is the only app-rendered extension.",
    "identity": "Keep the proxy receipt in the authenticated app session. During renewal bind expected_email to that session, verify the returned identity before replacing it, and pass expected_email to the SSO start endpoint. Never use an email address as proof of identity. A standalone proxy administrator session must not replace the app user.",
    "rollout": "Deploy the proxy before apps. Apps must display a retryable error if mountAccountLauncher is missing, rather than navigate to a standalone portal session."
  },
  "embeddedSSO": {
    "purpose": "Open personal GenAI setup in a near-full-screen proxy-hosted modal without repeating Google login.",
    "login": "The OAuth token exchange additionally returns sso_receipt and sso_expires_in. Encrypt the receipt in the calling app session; never put it in browser storage, URLs or user profile JSON. It proves a prior proxy-verified login; an app secret or email alone is not sufficient.",
    "emailLogin": "For email/password sessions authenticated by this gateway /user/login, send its signed token as email_token instead of receipt, only from the app backend with client Basic authentication and expected_email from the server session. Never forward passwords or accept identity tokens from request bodies. Expired email tokens require email/password sign-in again, not Google renewal. Email identities are separate from Google identities and never inherit portal admin privileges.",
    "start": {
      "method": "POST",
      "url": "https://oauth.skoonline.org/genai/services/sso/start",
      "authorization": "Basic base64(clientId:clientSecret)",
      "body": [
        "receipt OR email_token",
        "expected_email",
        "redirect_uri",
        "state",
        "code_challenge",
        "code_challenge_method"
      ],
      "response": [
        "action",
        "ticket"
      ]
    },
    "browser": "From an authenticated same-origin POST in the calling app, obtain the handoff and call openGenAISetup({handoff,t}) exported by the hosted services.js. The widget POSTs the one-use 60-second ticket into its modal iframe. Only same-site HTTPS sibling apps with an exact registered GenAI callback are supported.",
    "callback": "Verify the existing user, browser session, state and PKCE, exchange the code server-side and encrypt the grant as usual. For iframe requests, return same-origin HTML containing <p id=\"sko-link-result\" data-result=\"connected\">Connected</p>, or a safe error code in data-result. Set frame-ancestors self and no-store. The shared modal reads this marker only after the frame returns to the calling app origin.",
    "limits": "Deploy the proxy first, then the calling app. Pre-upgrade or expired sessions require one fresh app login. Embedded sessions cannot administer the proxy. Client disablement or secret rotation invalidates handoffs. User approval of AI access remains explicit. SMTP uses app authentication and needs no individual sign-in."
  },
  "deployment": {
    "gateway": {
      "BASE_URL": "https://oauth.skoonline.org",
      "STORAGE_TYPE": "gcs",
      "GCS_BUCKET_NAME": "YOUR_EXISTING_PRIVATE_BUCKET",
      "GENAI_ENABLED": "true",
      "GENAI_PUBLIC_ORIGIN": "Optional; inherits BASE_URL",
      "GENAI_GCS_BUCKET": "Leave unset to inherit GCS_BUCKET_NAME",
      "API_KEY_ENCRYPTION_SECRET": "Stable secret, at least 32 bytes",
      "GOOGLE_CLIENT_ID": "Existing Google OAuth client",
      "GOOGLE_CLIENT_SECRET": "Existing Google OAuth secret",
      "JWT_SECRET": "Existing stable secret",
      "SESSION_SECRET": "Existing stable secret",
      "GMAIL_USER": "Configured sender address for app SMTP",
      "GMAIL_APP_PASSWORD": "Server-only Google app password for that sender"
    },
    "googleCallbacks": [
      "https://oauth.skoonline.org/auth/google/callback",
      "https://oauth.skoonline.org/genai/auth/callback"
    ],
    "storage": "One private bucket, separate record namespaces. GenAI uses genai/v1/. Keep encryption secrets and the existing GenAI identity namespace stable. Bucket sharing does not merge authentication scopes.",
    "app": {
      "APP_URL": "https://YOUR_APP_HOST",
      "OAUTH_BASE_URL": "https://oauth.skoonline.org",
      "PROXY_CLIENT_ID": "careTraining",
      "PROXY_CLIENT_SECRET": "One-time secret from proxy Administration",
      "GENAI_TOKEN_ENCRYPTION_KEY": "Stable local encryption key: 32 random bytes encoded as 64 hex characters; not sent to proxy",
      "OAUTH_ENABLED": "true",
      "OAUTH_AUTO_REGISTER": "true only if new Google users may register",
      "SMTP_ENABLED": "true",
      "GENAI_ORIGIN": "Optional; inherits OAUTH_BASE_URL"
    },
    "consent": "Register exact app callback URLs. Add Google callback URLs in the Google OAuth client. Give the runtime service account access to the shared bucket. Configure sender/provider mail permissions before sending email."
  },
  "clients": {
    "administration": "https://oauth.skoonline.org/genai/#administration",
    "setup": [
      "Sign in with verified Google account xiangenhu@gmail.com. Only this account may administer the proxy.",
      "Register careTraining, enable oauth/smtp/genai and save the secret shown once. For an existing client, load it, save permissions and rotate its secret if needed.",
      "Register https://YOUR_APP_HOST/api/auth/oauth/callback under OAuth and https://YOUR_APP_HOST/api/ai/callback under GenAI. Register each exact production or development URL; no wildcards.",
      "Install the current server SDK before upgrading an existing client. Set PROXY_CLIENT_ID and PROXY_CLIENT_SECRET on the caller backend. Never pass the secret to shared browser JS."
    ],
    "authentication": "One client ID and secret for all three services. Send Authorization: Basic base64(clientId:secret) on OAuth start/token and SMTP send. GenAI token exchange uses the same Basic credentials. Every GenAI /v1 request additionally requires X-Client-Authorization: Basic base64(clientId:secret), alongside Authorization: Bearer <user-grant>. The current server SDK supplies both.",
    "controls": "Secrets are stored hashed and only shown at creation/rotation. Rotation invalidates the old secret and pending OAuth login tickets/codes. Disable the client or remove a service to block subsequent requests. Already-running requests may finish.",
    "migration": "Legacy OAuth/SMTP routes remain for existing callers; new integrations use the client-authenticated endpoints below. Existing GenAI registrations initially retain GenAI-only access. Saving their permissions enables the common-secret requirement. In careTraining PROXY_CLIENT_* takes precedence over legacy GENAI_CLIENT_*; SMTP_GATEWAY_TOKEN is unnecessary in common-client mode.",
    "dashboard": "Administration lists registered sites. Register new sites, manage callbacks and service permissions, rotate secrets, enable or disable sites, and open Usage for each site. Monthly UTC usage shows completed Google sign-ins, email outcomes, GenAI operations and reported tokens. Statistics start at deployment, may have index gaps and are not invoices; load every page for complete observed totals. No historical backfill.",
    "authorization": "The client secret identifies an application; it does not identify a person or grant access to all users. Google login still verifies the user, and GenAI access still needs that user’s explicit model consent."
  },
  "oauth": {
    "start": {
      "method": "POST",
      "url": "https://oauth.skoonline.org/genai/services/oauth/start",
      "authentication": "Basic client credentials",
      "body": {
        "redirect_uri": "https://YOUR_APP_HOST/api/auth/oauth/callback",
        "state": "<one-use browser-bound random state>",
        "code_challenge": "<SHA-256 verifier, base64url>",
        "code_challenge_method": "S256"
      }
    },
    "authorize": "Redirect the browser to the returned url. The proxy signs in with Google directly and returns code and state to the registered callback. No client secret appears in browser URLs.",
    "token": {
      "method": "POST",
      "url": "https://oauth.skoonline.org/genai/services/oauth/token",
      "authentication": "Same Basic client credentials",
      "body": {
        "code": "<returned single-use code>",
        "redirect_uri": "https://YOUR_APP_HOST/api/auth/oauth/callback",
        "code_verifier": "<server-stored PKCE verifier>"
      }
    },
    "callback": "Check state against the initiating browser session, exchange the code from your backend, validate the verified Google identity in response.user, preserve local roles, create the app session and redirect to a clean URL. Login tickets last 10 minutes; codes last 60 seconds and are single use.",
    "security": "Persist and encrypt PKCE verifiers server-side. Only exact registered HTTPS callbacks are accepted. Keep secrets off the browser. Codespaces URLs must be registered exactly and updated when the hostname changes.",
    "sharedIdentity": "Use the verified stable subject returned by the proxy; do not use display names as identifiers."
  },
  "smtp": {
    "endpoint": "https://oauth.skoonline.org/genai/services/smtp/send",
    "method": "POST",
    "authentication": "Authorization: Basic base64(PROXY_CLIENT_ID:PROXY_CLIENT_SECRET); smtp permission required",
    "body": {
      "to": "recipient@example.com",
      "subject": "Subject",
      "body": "Plain-text message",
      "requestId": "<unique UUID per intended email>"
    },
    "response": "Check HTTP success and success:true. Repeating a successful requestId with the same payload returns duplicate:true without resending. Reusing it with different contents is rejected.",
    "limitations": "One recipient and plain text, sent from the proxy-configured GMAIL_USER using GMAIL_APP_PASSWORD. Caller cannot override sender. Admin controls the per-client hourly limit. Pending or uncertain sends return an error and must not be automatically retried with a new requestId.",
    "verification": "Send a test message only after explicit operator action to a confirmed recipient. Mounting a UI component does not send mail."
  },
  "ui": {
    "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js",
    "exports": [
      "mountAccountLauncher",
      "mountAccountSettings",
      "mountGoogleSignIn",
      "mountSMTPSettings",
      "mountGenAISettings",
      "mountServiceSettings"
    ],
    "recommended": "Use mountAccountLauncher(host, {account?, smtp?, genai?, appModels?, t?}) for the proxy-owned launcher and full settings modal. account uses {load, actions?}; smtp uses {load, actions?}; genai uses {load, select, connect, disconnect, capabilities?}. Include only services the user may access. Do not recreate headings, wrappers or controls in each app.",
    "example": "import {mountAccountLauncher} from 'https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js';\nconst panel = mountAccountLauncher(document.querySelector('#account-settings'), {account: {load: loadAccount}, smtp: {load: loadEmailStatus}, genai: {load: loadAISettings, select: saveAISelection, connect: startAIConsent, disconnect: disconnectAI, capabilities: ['text']}});\n// Call panel.destroy() when the host view unmounts.",
    "adapters": "Host app supplies authenticated backend callbacks. No provider keys, service keys, app secrets or bearer tokens are passed to the UI.",
    "csp": "Add https://oauth.skoonline.org to script-src and style-src; retain other CSP protections. Assets and portal links resolve relative to import.meta.url.",
    "lifecycle": "Use separate empty host elements. Widgets isolate CSS in shadow roots, accept t(text), and return refresh()/destroy(). Keep local controls as a fallback.",
    "google": "mountGoogleSignIn(host, {signIn, t?}); signIn navigates to the host app backend login-start route.",
    "smtp": "mountSMTPSettings(host, {load, actions?, t?}); load returns {configured, connected?, message?, details?:string[]}; each action is {label,run}.",
    "genai": "mountGenAISettings(host, {load,select,connect,disconnect,capabilities?,t?}); load returns {configured,linked,providers,selectedProviderId,defaults,selectionUnavailable?,connectionError?}. Providers contain {id,name,model,capability}; defaults maps capability to ID. select(id,capability) persists through the backend. connect starts explicit GenAI consent; disconnect removes this app access locally.",
    "scope": "careTraining main adopts these controls. Other SKO apps must implement the adapters; this is not an automatic wildcard-site rollout."
  },
  "verification": [
    "Check wrong/rotated client secrets, disabled clients and disabled service scopes are rejected for all three services.",
    "Open the guide on the deployed domain; links and module assets must use that domain.",
    "Google login returns to the registered callback, restores the correct user and local role, and rejects reused or mismatched state.",
    "Verify SMTP only with an explicitly requested test message; confirm receipt and check failures without exposing tokens.",
    "Save provider credentials in the portal; authorize the consuming app; select a model; refresh, sign out/in and restart the app to verify persistence.",
    "Ensure revoked grants, cross-user access and unauthorized model IDs are rejected; no silent fallback to a different provider.",
    "Verify all services use the intended GCS bucket with separate prefixes; never make the bucket public.",
    "Non-text AI configurations can be saved; current proxy execution supports text chat only."
  ]
}

Complete GenAI integration

1. Keep your existing login

GenAI is an optional addition to the existing OAuth and SMTP gateway. Keep your app’s current login, user sessions, roles, email relay and OAuth callbacks. Add a “Connect GenAI Management” action in profile settings; linking AI access is separate from signing in.

Direct execution is the standard for personal text connections: obtain an explicitly consented backend-only execution session, cache it for up to 15 minutes, and call the provider from your app backend. The legacy proxy inference endpoint remains available for compatibility and centrally funded hosted accounts. Keys never reach the browser.

This guide describes the implemented v1 contract, not a generic OpenAI drop-in API. The service accepts messages, connection, maxTokens, requestId and stream. Its response and SSE events use the shapes below.

2. Use the deployed gateway origin

This deployment's GenAI origin: https://oauth.skoonline.org. Use this origin consistently for linking, management, token exchange and inference.

These documentation links follow the domain serving this page. The shared UI derives its assets and portal links from its own module URL. Consuming apps receive a trusted proxy origin from their backend configuration, not their own browser origin. Begin and finish login on the same domain.

Set BASE_URL to the deployed origin; GENAI_PUBLIC_ORIGIN optionally overrides it. Register the matching Google callback. The legacy account namespace is preserved independently of the routing origin; change GENAI_ACCOUNT_NAMESPACE only with a planned data migration.

3. Register the consuming application

A gateway administrator opens https://oauth.skoonline.org/genai/, then Administration → Register an application. Choose a stable app ID (for example vhs), a display name, and exact HTTPS callback URLs owned by that app.

Save the one-time app secret in your app backend’s secret configuration. Callback URLs must have no query string or fragment. Register production and staging explicitly. Never put the app secret in frontend JavaScript, localStorage, a URL, or a Git commit.

There are two callback settings: the consuming app callback is registered in GenAI Administration. The gateway’s own /genai/auth/callback must be registered in Google Cloud for its direct Google sign-in. Register consuming app callbacks separately under OAuth and GenAI in Administration. Select the services the client may use.

GENAI_ORIGIN=https://oauth.skoonline.org
PROXY_CLIENT_ID=your-registered-app-id
PROXY_CLIENT_SECRET=<secret-injected-on-your-server>
GENAI_CALLBACK_URL=https://yourapp.example/auth/genai/callback

Optional app storage and three xAPI LRS destinations

Register optional GCS and primary/debug/webhooks LRS settings at oauth.skoonline.org. In each app set USE_OAUTH_GCS=true to use registered storage and USE_OAUTH_LRS=true to use registered LRS profiles. These switches are independent; false or unset uses that app's original configuration. The backend calls POST /genai/services/config with Basic PROXY_CLIENT_ID:PROXY_CLIENT_SECRET and JSON {gcs:true,lrs:true} matching enabled switches. Use loadOptionalConnections from genai/sdk/optional-connections.js at server startup; it retrieves only requested settings. A complete gateway profile takes precedence, otherwise the complete original profile is the fallback. Never mix profiles. Network errors, HTTP 429 and 5xx use original configuration; 4xx authentication/configuration errors are surfaced. This selects destinations at initialization; it does not migrate GCS data or perform per-operation storage failover. Keep credentials and responses server-side.

In Connect a new app or App settings, expand Optional storage and xAPI LRS connections. All four connections and every field are optional. Each LRS has URL, User, and Secret / Password. Leave unused settings blank.

These settings are per-app deployment configuration. Set USE_OAUTH_GCS and USE_OAUTH_LRS independently on the app server, then load the registered configuration with the SDK helper. The app backend retrieves the requested settings from POST /genai/services/config using its existing client credentials; OAuth tokens and GenAI grants do not include these settings.

Use the app runtime Google Cloud service account/Application Default Credentials with @google-cloud/storage. GCS_BUCKET is a bucket name, not gs:// or a URL. Grant appropriate private bucket permissions separately. An unset bucket leaves existing storage behavior unchanged; never silently replace durable storage with memory.

Read each LRS independently on the app backend. An empty or partial gateway profile falls back to the complete original profile. If neither is complete, keep it inactive. Construct Basic authentication from base64(User + ":" + Secret/Password). Append statements to the configured xAPI base URL after normalizing its trailing slash. Send Content-Type: application/json and X-Experience-API-Version: 1.0.3 for xAPI 1.0.3 statements. Use valid actor, verb and object; obtain learner identity from a verified app session.

Primary receives intended learning/assessment statements; debug receives explicitly enabled diagnostics with minimized data; webhooks receives statements for verified webhook events. Do not fan out automatically or fall back between profiles. The webhook LRS is an outbound destination, not an incoming webhook registration; authenticate incoming webhooks in the receiving app.

Generate and persist a statement UUID before delivery. For retry-safe xAPI 1.0.3 delivery, PUT /statements?statementId=<same UUID> with the same statement. Use bounded timeouts and an app-owned durable outbox for retries; classify failures and avoid blocking the learning activity. Do not claim delivery until HTTP success.

Never expose LRS credentials in browser bundles, public JSON, URLs, logs or commits. The administrator-only app detail route decrypts saved values for editing; lists and usage omit them. Copy into server secret configuration. Existing config updates omitting optionalConnections preserve values; explicit blanks clear fields.

OAUTH_BASE_URL=https://oauth.skoonline.org
PROXY_CLIENT_ID=your-app-id
PROXY_CLIENT_SECRET=<server-secret>
USE_OAUTH_GCS=true
USE_OAUTH_LRS=true
# Keep original app GCS/LRS values for fallback.
const { loadOptionalConnections } = require('./optional-connections');
const connections = await loadOptionalConnections(process.env);

4. Let the user prepare and test a connection

In the GenAI portal, the user selects a provider/region, enters a model or Azure deployment name, saves their API key, and clicks Test connection. Existing connections can be edited, reused and removed. Tests make a small real provider request and can incur provider charges.

Personal provider: the vendor bills the user’s provider account directly. Local model: use an authenticated public HTTPS endpoint to Ollama, LM Studio, vLLM or another compatible runtime. A cloud proxy cannot reach the user’s localhost or private LAN.

Hosted model: available only when the operator configures it. Shared allowance or credit funds requests. This version has manual verified-credit administration, not online checkout. Administrators follow the same funding rules.

The 41 templates include direct APIs and compatible gateways. Bedrock, Vertex, Foundry, SageMaker and watsonx templates require a compatible gateway; they are not native IAM adapters. A catalog entry is not proof of current model access. Discover IDs from a saved connection or enter them manually, then test.

5. Install the server-side client

Download the SDK using the link at the top of this page and save it as genai-client.js in the consuming app backend. It is a CommonJS module for Node.js 22 or later. Download direct-execution.js alongside genai-client.js and install ipaddr.js for the DNS-pinned HTTPS provider transport. No npm package publication is required.

Pass only the origin, without /genai; the SDK adds the prefix. Keep the downloaded version pinned with your app and review updates before replacing it. Upgrade the SDK before enabling common-client authentication on an existing registration. Its requests include the app secret server-side in X-Client-Authorization, in addition to user-grant tokens.

const { GenAIClient } = require('./genai-client');
const client = new GenAIClient({
  origin: 'https://oauth.skoonline.org',
  clientId: process.env.PROXY_CLIENT_ID,
  clientSecret: process.env.PROXY_CLIENT_SECRET,
});

6. Link through explicit user consent

Run linking only from an already-authenticated app session. Protect the start action using the app’s existing CSRF mechanism. Generate the URL, state and PKCE verifier with client.link(callbackURL). Persist the transaction server-side under the current app user and session before redirecting.

The portal asks which connections the app may use and which is its default. Logging in alone never authorizes API-key use or spending. Existing gateway tokens and suite identity tokens are not GenAI inference tokens.

At the app callback, require the same authenticated app session; check the saved owner, expiry and returned state, and atomically consume the transaction once. Reject mismatches before exchanging the code. Do not trust a callback email or user ID. Use a dedicated GenAI callback; leave the existing OAuth callback unchanged.

After exchange, encrypt and persist the returned access/refresh tokens under that app user. Never expose them in frontend code or logs. The transaction store and token store below are integration adapters your app must supply; the fragments are not a complete session or persistence implementation.

// Start: after existing app authentication and CSRF checks.
const link = client.link(process.env.GENAI_CALLBACK_URL);
await linkStore.save({
  owner: currentUser.id, sessionId: currentSession.id,
  state: link.state, verifier: link.verifier,
  expiresAt: Date.now() + 10 * 60 * 1000,
});
res.redirect(link.url);

// Callback: consumeForSession must verify owner, session, state, expiry
// and atomically reject replay. It must throw on any mismatch.
const pending = await linkStore.consumeForSession({
  owner: currentUser.id, sessionId: currentSession.id,
  returnedState: req.query.state,
});
const tokens = await client.exchange({
  code: req.query.code,
  redirectUri: process.env.GENAI_CALLBACK_URL,
  verifier: pending.verifier,
});
await tokenStore.saveEncrypted(currentUser.id, tokens);
res.redirect('/profile');

Direct provider execution standard

POST /genai/v1/execution-session accepts {connection?}. Require the user-grant Bearer token and confidential app credentials in X-Client-Authorization. Browser-origin requests are rejected. The user must explicitly enable direct execution when granting connections; existing grants do not authorize credential handoff.

The response contains one approved personal text connection: provider, model, endpoint, apiVersion, apiKey and maxTokens, plus appId, revision and expiresAt. Keep this response in server memory only, scoped to the verified local user and session. Never expose it through a browser settings endpoint, logging or an error body.

Reuse the session across agents and HTTP turns until expiresAt, capped at 15 minutes and the access/grant expiry. Coalesce concurrent loads. Invalidate on app selection/routing changes, reconnect, disconnect and logout. Reload after expiry; do not reload for each agent. Other instances can retain their cache until expiry.

The SDK defaults to direct execution. Download /genaiInstruction/direct-execution.js beside the main SDK. Its transport pins public-address validation to socket lookup and rejects redirects. Applications with existing adapters may supply providerFetch(endpoint), with equivalent DNS and origin protections.

Generation requests go directly from the application backend to the provider. The gateway does not apply its inference admission queue or central budget ledger to those requests. Applications must durably attribute usage, manage concurrency and duplicate-request protection, and preserve incomplete-outcome handling. A provider interruption is never an instruction to retry automatically.

A cache expiry is not a provider credential expiry. Revocation blocks new configuration handoffs; only provider-side key revocation or rotation invalidates a key already handed to an app. Shared hosted credentials are not exported: use a personal connection or implement provider-scoped delegated credentials and budgets before migrating hosted execution.

This text-only contract does not migrate media generation, OAuth sign-in, SMTP delivery or optional GCS/LRS settings. Existing integrations require individual adapter upgrades; publishing this contract alone does not change them.

const client = new GenAIClient({origin: 'https://oauth.skoonline.org', clientId, clientSecret});
const result = await client.complete(accessToken, {messages, maxTokens: 'max'});
// Reuse this server-side client; write measured/uncertain usage to the app datastore.
// Explicit compatibility only: new GenAIClient({...options, executionMode: 'proxy'}).

7. Legacy proxy request reference

Obtain the user’s server-stored access token and list authorized connections with client.connections(accessToken). Omit connection to use the app grant’s default, or send an approved connection ID. Changing the account-wide default does not change an existing app grant. Relink to change the app’s permissions/default, then revoke its old grant.

Generate and durably persist the request ID before sending. The ID starts with the current UTC YYYY-MM followed by underscore and a unique suffix. Keep it with your app’s operation record for diagnosis.

Messages support system, user and assistant roles with text content: 1–100 messages, at most 100,000 characters in their serialized array. maxTokens must be an integer from 1 to 8192. Images, audio, tool calls, embeddings and arbitrary vendor parameters are not exposed by this version.

The gateway does not persist prompts or generated text in its usage ledger. Your app decides whether to store the returned response under its own privacy and retention policy.

const requestId = client.requestId();
await operationStore.create({ owner: currentUser.id, requestId });
const result = await client.complete(accessToken, {
  requestId,
  messages: [{ role: 'user', content: 'Explain this experiment.' }],
  maxTokens: 1024,
  // connection: approvedConnectionId, // optional
});
// result: {content, usage: {input, output} | null, requestId,
//          connection, model, state: 'completed' | 'uncertain'}
await operationStore.recordResult(requestId, result);

8. Streaming and legacy operation lookup

client.stream sends stream:true and parses named SSE events. delta carries {text}; done carries usage, requestId, connection, model and state. error carries a safe code/message. Forward text to your frontend using your app’s existing transport.

A stream ending without done is not proof that inference failed or was free. Missing provider usage produces state:uncertain. Inspect the operation before deciding what to do. A disconnected client may still leave a provider request running.

await client.stream(accessToken, {
  requestId, // already persisted; do not reuse an ID from another operation
  messages: [{ role: 'user', content: 'Explain this experiment.' }],
  maxTokens: 1024,
}, async (event, data) => {
  if (event === 'delta') sendTextToBrowser(data.text);
  if (event === 'done') await operationStore.recordMetadata(requestId, data);
  // The SDK throws on error events or a stream without a final done event.
});

// After a network interruption or duplicate-request response:
const status = await client.operation(accessToken, requestId);

9. Legacy proxy accounting and token lifecycle

Access tokens last 15 minutes. Refresh tokens rotate on use and expire no later than 30 days or the grant expiry. Serialize refreshes per app user across backend instances and atomically replace stored tokens. A lost exchange/refresh response can require relinking.

Authorization codes are one-use, app-bound, redirect-bound and PKCE-bound, with a two-minute expiry. App grants last 30 days. Users can revoke access in Applications; admins can disable an app. The next request checks the latest grant/app state. Revocation does not cancel already-running requests.

A used request ID returns HTTP 409; it does not replay the response. Do not silently retry inference with a new ID or switch to a host key. Check operation status, explain uncertainty, and require an explicit user decision if another generation is needed.

Previous-month request IDs are rejected for new inference. Older settled operation details can be compacted by the current ledger; keep required audit records in the consuming app. Pending and uncertain reservations remain held until verified reconciliation.

// Inside a distributed per-user refresh lock:
const nextTokens = await client.refresh(savedTokens.refresh_token);
await tokenStore.replaceEncryptedAtomically(currentUser.id, nextTokens);

10. HTTP endpoint reference

All paths below use the configured origin. Browser management APIs use the GenAI session plus same-origin protections. App inference APIs use GenAI user-grant Bearer token plus X-Client-Authorization Basic client credentialss, never gateway login JWTs.

Basic app credentials are base64(clientId + ":" + clientSecret). They identify the app at token exchange; credentials alone cannot select an arbitrary user or access personal keys.

GET  /genai/                          Portal
GET  /genai/connect                   Consent (client_id, redirect_uri, state,
                                     code_challenge, code_challenge_method=S256)
POST /genai/oauth/token               Basic app credentials; JSON body
GET  /genai/v1/connections            Bearer user-grant access token
POST /genai/v1/chat/completions       Bearer user-grant access token; JSON body
GET  /genai/v1/operations/:requestId   Bearer user-grant access token

// Authorization-code exchange JSON:
{ "grant_type": "authorization_code", "code": "<code>",
  "redirect_uri": "https://yourapp.example/auth/genai/callback",
  "code_verifier": "<server-stored-verifier>" }

// Refresh JSON:
{ "grant_type": "refresh_token", "refresh_token": "<stored-refresh-token>" }

11. Diagnostics and recovery

401 access_expired: refresh once through the serialized backend refresh path. Invalid refresh or revoked grant: reconnect. 403 grant_scope: choose an approved connection or relink.

402 credit_required: the hosted allowance/credit cannot fund the reservation. Offer a personal provider or administrator-assisted credit. Do not bypass funding for admins.

409 duplicate_request or request_id_conflict: inspect the original operation. 409 choose_connection or hosted_unavailable: return the user to connection settings.

429 pending_limit or monthly_request_limit: inspect unresolved operations or the initial-release capacity limits. 503 inference_busy: no provider work was started by that rejected request; communicate capacity to the user.

provider_auth, provider_model, provider_limit and provider_unavailable describe upstream key/access, endpoint/model, quota/rate, or network/provider failures. Use portal Test connection. Do not display raw vendor error bodies or credentials.

The portal may show genai_setup or an enablement notice until the administrator configures the module. Availability of this public instruction page does not prove inference is enabled.

12. Operator setup and release acceptance

Enable GENAI_ENABLED=true; set BASE_URL to the deployed HTTPS origin, STORAGE_TYPE=gcs and GCS_BUCKET_NAME to your private shared bucket. Leave GENAI_GCS_BUCKET unset to inherit that bucket. GENAI_PUBLIC_ORIGIN optionally overrides BASE_URL. Keep API_KEY_ENCRYPTION_SECRET stable (at least 32 bytes) and grant bucket access to the runtime service identity. GenAI uses the separate genai/v1/ namespace.

Preserve existing OAuth/SMTP environment values and CORS configuration. Register exact consuming-app redirects in REDIRECT_URI_ALLOWLIST and GenAI app callbacks separately in GenAI Administration. Register the gateway Google callbacks in the Google OAuth client. Verified admin email configuration is separate from app-user roles.

HOSTED_MODELS_JSON=[] enables personal/local usage without host keys. Hosted models require separately configured host credentials, operator-approved tariffs and allowances. Do not copy example prices as current vendor prices.

Acceptance checks: existing app login and SMTP still work; linking rejects wrong/replayed state; keys never reach the browser; direct keys reach only the consenting user’s authorized backend; saved connection test succeeds; non-authorized connections are rejected; streamed requests finish with usage or explicit uncertainty; revocation works; refreshes serialize; duplicate IDs never trigger a second generation.

Initial limits: 20 saved connections, 100 grants and 2,000 operations per account/month, five pending/uncertain calls, plus per-instance AI admission. This GCS-backed release is not certified for millions of active users. Normalize the operational datastore and perform production-like load testing before mass rollout.

Teacher/class sponsorship, online credit checkout, xAPI export and native enterprise IAM are not implemented in this addition. Keep these as explicit future work rather than inferring support from the provider templates.

13. Handoff for an AI coding assistant

Give the assistant the plain-text version of this page and the SDK. Ask it to integrate only the GenAI connection flow and request adapter, preserving your existing login and email behavior.

Integrate the SKO GenAI gateway at https://oauth.skoonline.org.
Read https://oauth.skoonline.org/genaiInstruction.txt and use the server-only SDK at
https://oauth.skoonline.org/genaiInstruction/sdk.js.
Preserve existing OAuth login, user identities, permissions and SMTP behavior.
Add Connect/Manage AI to profile settings with explicit per-app user consent.
Use backend-only execution sessions and direct provider calls for personal text.
Download direct-execution.js beside the SDK and install ipaddr.js.
Keep app credentials and user tokens encrypted on the backend.
Implement one-use state/PKCE linking, serialized token refresh, scoped connection
selection, durable request IDs, streaming and uncertain-outcome handling.
Do not use a gateway login JWT as a GenAI access token. Do not silently fall back
to a host key, retry inference with a new ID, or invent unsupported API features.
Use the app's durable session/token stores and include targeted integration tests.
Report runtime configuration still required; do not claim deployment from a commit.