# ELYSÉA SDK — Full machine-readable reference for AI agents > This file contains the complete SDK reference. For a concise summary, see llms.txt. > Versions: see packages/core/package.json, packages/react/package.json, packages/widget/package.json (or the versions published on npm). > Canonical source: docs/API_REFERENCE.md, packages/core/README.md, packages/react/README.md, docs/PROHIBITIONS_EN.md --- ## What ELYSÉA is ELYSÉA is an ethical cognitive infrastructure embedded through an SDK. Builders call the ELYSÉA brain from their own server; ethical guardrails (Guardian, 8 prohibitions, firm_safety) are not configurable. ELYSÉA is not a chatbot, coach, therapist or medical product, and never speaks in the first person. ## Status to state correctly - **BYOK is being finalised and is NOT available.** The brain requires an LLM key deposited AND activated; no key can be activated today. Against the real brain, `pipeline.run` / `invoke.call` / `chat.send` return **422 `BYOK_KEY_REQUIRED`**. Never write "deposit your key in the portal and it works". - To try the SDK without the brain: `createMockElyseaClient()` — a **simulation** (no network). Mock output is not proof that the real brain works. - **Browser rule:** `createElyseaClient()` throws `BROWSER_SECRET_FORBIDDEN` in a browser. React hooks throw the same error when given a client holding `apiSecret`. Pattern: client on your server + your own route + a proxy client without secret in the browser (full example: packages/react/README.md, "Browser: server pattern"). - **"Powered by ELYSÉA" is always visible**, on every plan, no white-label. - Brain URL: no public host is documented — `baseUrl: process.env.ELYSEA_BASE_URL!`, URL provided at activation. Portal: https://elysea.app/developers. Verification page: https://elysea.app/verify?app=. - Region (EU by default, or US) is fixed when the application is created in the portal; it is not sent with calls. `region` in the config is deprecated and ignored — do not use it. ## Plans - `test` — `sk_test_` key, 500 interactions/month, free. Beyond: 429 `SANDBOX_CAP_EXCEEDED` (never retried). - `production` — €2.50 / €1.50 / €0.80 excl. VAT per monthly active user and per band (≤ 5,000 / ≤ 25,000 / beyond), no fixed fee. - `equipes` — €30 / €18 / €9.60 per seat per year. SDK: `tier?: 'test' | 'production' | 'equipes'` (default `'test'` for `sk_test_`, `'production'` for `sk_live_`). No plan disables Guardian or the 8 prohibitions. --- ## Quick start (server side) ```typescript import { createElyseaClient } from '@elysea/core'; const client = createElyseaClient({ baseUrl: process.env.ELYSEA_BASE_URL!, // provided at activation auth: { apiKey: process.env.ELYSEA_API_KEY!, // sk_test_… or sk_live_… apiSecret: process.env.ELYSEA_API_SECRET!, }, appId: 'my-app', countryCode: 'FR', // Required — ISO 3166-1 alpha-2 }); const who = await client.identity.resolve({ userJwt }); const answer = await client.pipeline.run({ userInput: 'What are your opening hours?', coreUserId: who.coreUserId, }); if (answer.guardianAction === 'block' || answer.errorCode) { // e.g. errorCode === 'BYOK_KEY_REQUIRED' against the real brain today console.log(answer.errorCode, answer.errorMessage, answer.errorAction); } else { console.log(answer.response); } ``` Without the brain (simulation): ```typescript import { createMockElyseaClient } from '@elysea/core'; const mock = createMockElyseaClient({ appId: 'my-app', tier: 'test' }); // Same API surface. Swap to createElyseaClient (server side) for the real brain. ``` --- ## @elysea/core — Full reference ### createElyseaClient(config: ElyseaClientConfig): ElyseaClient | Field | Type | Required | Description | |-------|------|----------|-------------| | baseUrl | string | yes | Brain URL — provided at activation | | auth.apiKey | string | yes | Must start with sk_test_ or sk_live_ | | auth.apiSecret | string | yes | HMAC-SHA256 secret — server only | | appId | string | yes | Builder app identifier | | countryCode | string | yes | ISO 3166-1 alpha-2. Never inferred. Throws ElyseaMissingCountryCodeError if absent. | | tier | BillingTier | no | 'test' \| 'production' \| 'equipes'. Default: 'test' (sk_test_) / 'production' (sk_live_) | | quotaPolicy | QuotaPolicy | no | Quota enforcement configuration | | crossApp | CrossAppConfig | no | Selective cross-app memory (see CrossAppContext) | | llmProvider | LlmProviderConfig | no | { provider: 'claude' \| 'openai' \| 'mistral' \| 'gemini', apiKey, endpoint? } — BYOK NOT available (brain ignores these headers; 422 BYOK_KEY_REQUIRED) | | defaultTimeoutMs | number | no | Default 30000 | | journal | AuditJournal | no | Tamper-evident journal | | alerting | AlertingConfig | no | Alert callbacks | | onBillingFlushError | function | no | Called when billing flush fails | | debug | boolean | no | console.debug logging — never in production | | guardianEnabled | true | fixed | Cannot be set to false | Do not pass `region` (deprecated, ignored) or `accountType` (not sent by default, no presumed value). countryCode override per call: `pipeline.run({ ..., countryCode: 'DE' })`. ### ElyseaClient namespaces #### pipeline.run(options: PartnerInvokeOptions): Promise Alias: `invoke.call(options)`. Sequence: Guardian input check (P1-P8) → POST /sdk/chat/pipeline → Guardian output check (P5/P8) → billing → result. Options: | Field | Type | Description | |-------|------|-------------| | userInput | string | User message | | coreUserId | string | From identity.resolve() | | conversationId | string? | Resume existing conversation | | conversationHistory | Array? | Max 20 turns | | capabilities | string[]? | User capabilities | | countryCode | string? | Override per call | | timeoutMs | number? | Per-call timeout (default defaultTimeoutMs, 30 s) | | signal | AbortSignal? | Caller cancellation | Result: | Field | Type | Description | |-------|------|-------------| | response | string \| null | null if block or error | | posture | string \| null | Applied posture (read-only) | | signal | GuardianSignal | 'guardian_block' or null | | guardianAction | 'pass' \| 'warn' \| 'block' | Guardian outcome | | canonConformance | boolean | true if delivered | | guardianDegraded | boolean \| null | null when the brain did not report it | | billingTracked | boolean | false if block | | fallback | 'present_neutral'? | Present if Guardian block | | guardianLog | object? | Guardian log (coreUserId always hashed) | | errorCode | ElyseaErrorCode? | Typed error code on failure | | retryAfterSeconds | number \| null? | Retry-After value when sent | | errorMessage / errorAction | string? | From ERROR_CATALOGUE | pipeline.run() NEVER throws on Guardian block or HTTP error — check guardianAction / errorCode. #### chat.send(options: ChatSendOptions): AsyncIterable Options: { message, coreUserId?, conversationId?, conversationHistory?, capabilities?, countryCode?, timeoutMs? (default 30 s, connection + stream), signal?, maxRetries? (default 2, 429 only, never on SANDBOX_CAP_EXCEEDED), maxRetryDelayMs? (default 30 s) }. Retry-After is honoured. Chunk types: - `{ type: 'posture', posture }` — selected posture (read-only), e.g. 'present_neutral' - `{ type: 'signal', signal }` — interpreted signal - `{ type: 'text', value }` — text fragment - `{ type: 'guardian', guardianAction, canonConformance }` - `{ type: 'tool_emerge', tool_to_emerge: { tool_id, props } }` — emerging tool (Decision Z) - `{ type: 'error', value, errorCode, statusCode?, retryAfterSeconds? }` — typed error (no throw) - `{ type: 'done', chunksDropped? }` — only after [DONE] Other JSON chunks sent by the brain (e.g. attestation) are passed through unchanged. A stream cut without [DONE] yields an `error` chunk, never `done`. Use `consumeChatStream(stream, handlers)` for routing. #### memory namespace - `memory.summary(userId): Promise` — ≤512 chars (Garantie K — never raw items) - `memory.tags(userId): Promise` - `memory.write(userId, item: MemoryWriteItem, consentToken, preuve: MemoryConsentProof): Promise` — P7 + builder consent proof (USR-05) required - `memory.delete(userId, itemId, consentToken): Promise` — GDPR Art. 17 MemoryWriteItem: { type: 'objectifs_declares' | 'preferences_communication' | 'contexte_utile' | 'resumes_session' | 'scores_non_diagnostiques', content (≤256 chars), expires_at (ISO 8601, required) }. Never diagnostic or clinical. #### identity namespace - `identity.resolve(input: string | { userJwt: string }): Promise` → { coreUserId, isNewAccount, elyseaId? } - `identity.getCapabilities(): Promise` #### guardian namespace - `guardian.log(violation)`, `guardian.getViolations()` - `guardian.buildAuditResult(coreUserId, posture, action, pillar): GuardianAuditResult` - `guardian.audit(result): Promise` — POST /sdk/guardian/audit (B1) #### trust namespace - `trust.fetchAttestation(appId): Promise` - `trust.getStatus(attestation): VerificationStatus` — 'verified_public_measures' | 'not_verified' | 'expired' | 'unavailable' | 'error' - `trust.verify(attestation): Promise` — POST /verify/attestation; { valid, appId?, certificationId? }; { valid: false } on any failure Never 'verified_public_measures' without a valid signed attestation. P8: claiming verified status without valid attestation → GuardianViolationError (local) + HTTP 400 (brain). #### tier (read-only) `tier.current` ('test' | 'production' | 'equipes'), `tier.label`, `tier.badgePolicy` (always 'visible'), `tier.badgeRequired` (always true), `tier.maxApps` (null). #### Voice No voice namespace, option or hook. Voice: ELYSÉA does not speak and provides no voice (no speech synthesis, no transcription). Any voice is the builder application's own (« la voix éventuelle est celle de l'application du constructeur »): its engine transcribes, then the app sends the text to pipeline.run(). ### Standalone functions #### enforceGuardian(input: unknown) Throws GuardianViolationError for any prohibition. All P1-P8 are block-mode. ```typescript import { enforceGuardian, GuardianViolationError } from '@elysea/core'; declare const myInput: unknown; try { enforceGuardian(myInput); } catch (e) { if (e instanceof GuardianViolationError) { console.error(e.violation.prohibitionId); // 'P1'...'P8' } } ``` #### createSafetyInterrupt(options: { onFirmSafety }): SafetyInterrupt Calls `onFirmSafety` when a result or chunk carries posture 'firm_safety'. Cannot suppress the signal. ```typescript import { createSafetyInterrupt } from '@elysea/core'; declare function notifyOncall(detectedAt: string): Promise; const safety = createSafetyInterrupt({ onFirmSafety: async (ctx) => { await notifyOncall(ctx.detectedAt); }, }); safety.checkResult(result); // after pipeline.run safety.checkChunk({ type: 'posture', posture: 'firm_safety' }); // inside a chat stream ``` #### createConsentToken(apiSecret, appId, coreUserId) / verifyConsentToken(token, apiSecret, appId, coreUserId) P7 consent token (server side — uses apiSecret). Three mandatory positional params (not an object). Throws if any is empty. ```typescript import { createConsentToken, verifyConsentToken, CONSENT_WINDOW_MS } from '@elysea/core'; const token = createConsentToken(process.env.ELYSEA_API_SECRET!, 'my-app', coreUserId); // → 'elyc1.{timestampMs}.{hmac_hex}' const valid = verifyConsentToken(token, process.env.ELYSEA_API_SECRET!, 'my-app', coreUserId); // true console.log(CONSENT_WINDOW_MS); // 86400000 — 24h ``` #### QuotaEnforcer ```typescript import { QuotaEnforcer } from '@elysea/core'; const quota = new QuotaEnforcer({ onConversationLimitExceeded: 'enforce', // throw QuotaExceededError if >20 turns onRateLimitExceeded: 'enforce', onWarn: (event) => console.warn('[Quota]', event.type), maxRetries: 3, retryBaseDelayMs: 1000, }); ``` #### retryWithBackoff(fn, options?) Retries only on RateLimitError, exponential backoff + jitter. Honours Retry-After; never retries SANDBOX_CAP_EXCEEDED. ```typescript import { retryWithBackoff, RateLimitError } from '@elysea/core'; declare const url: string; const data = await retryWithBackoff( async () => { const res = await fetch(url); if (res.status === 429) throw new RateLimitError(60); return res.json(); }, { maxRetries: 3, baseDelayMs: 1000 } ); ``` #### CrossAppContext ```typescript import { createElyseaClient } from '@elysea/core'; const elysea = createElyseaClient({ baseUrl: process.env.ELYSEA_BASE_URL!, auth: { apiKey: process.env.ELYSEA_API_KEY!, apiSecret: process.env.ELYSEA_API_SECRET! }, appId: 'app-agenda', countryCode: 'FR', crossApp: { sharedSourceAppIds: ['app-notes', 'app-journal'], mergeContext: true }, }); // Propagates X-Elysea-Cross-App-Sources header on memory requests ``` Modules that traverse (consent per module × app_id): cognitive_profile, therapeutic_resources, limits_fragilities. Modules that never traverse: relational_state, intent_trajectory, runtime_state. Crisis data and minor / mixed-audience contexts are always filtered, even with consent. P7 stays active. #### AuditJournal / AlertingMonitor / verifyClientIntegrity / compliance report ```typescript import { AuditJournal, AlertingMonitor, verifyClientIntegrity, generateComplianceReport } from '@elysea/core'; import type { AlertContext } from '@elysea/core'; declare function notifyOncall(ctx: AlertContext): void; const journal = new AuditJournal({ maxEntries: 1000 }); journal.record({ event: 'GUARDIAN_PASS_INPUT', guardianAction: 'pass' }); journal.verify(); // { valid, firstInvalidSeq? } const monitor = new AlertingMonitor({ onProtectionUnavailable: notifyOncall, onAbnormalBlockRate: notifyOncall }); const integrity = verifyClientIntegrity([]); // status 'pending_core_v3' until the brain sends behaviourHash const report = generateComplianceReport(journal, { appId: 'my-app', reportingParty: 'My Company' }); ``` #### Provenance + B2 / L0 attestations ```typescript import { buildHistoryTurn, toCoreTurns, extractB2Attestations, parseL0AttestationsFromSSE } from '@elysea/core'; declare const rawSSE: string; const turn = buildHistoryTurn('user', 'Hello'); const coreTurns = toCoreTurns([turn]); // [{ role, content }] const b2 = extractB2Attestations({ historyTurnsSanitized: true }); const l0 = parseL0AttestationsFromSSE(rawSSE); // l0: memoryScopeIsolation, memoryRetrievedUserIds, historyTurnsSanitized, injectionNeutralized, // forcedPostureIgnored, guardianBlocked, guardianFaultHandled ``` #### BYOK provider registry (BYOK not available) ```typescript import { LLM_PROVIDER_ALLOWLIST, getProviderSpec, isEuNative } from '@elysea/core'; console.log(LLM_PROVIDER_ALLOWLIST); // ['claude', 'openai', 'mistral', 'gemini'] isEuNative('mistral'); // true — Paris infrastructure isEuNative('claude'); // false — depends_on_config const spec = getProviderSpec('openai'); // spec.euHosting, spec.status, spec.capabilities ``` The registry describes providers; it does not make BYOK available. #### Builder contract ```typescript import { assertBuilderContractValid, isBuilderContractValid, getProhibitionDetail } from '@elysea/core'; declare const myConfig: unknown; if (!isBuilderContractValid(myConfig)) { /* handle */ } assertBuilderContractValid(myConfig); // throws GuardianViolationError on violation const p4 = getProhibitionDetail('P4'); // { id, label, description, action, examples } ``` #### Plans ```typescript import { getTierConfig, isBadgeRequired, getBadgePolicy, BILLING_TIERS } from '@elysea/core'; import type { BillingTier } from '@elysea/core'; const tiers: readonly BillingTier[] = BILLING_TIERS; // ['test', 'production', 'equipes'] isBadgeRequired('production'); // true — always getBadgePolicy('equipes'); // 'visible' — always getTierConfig('test').includedMonthlyInteractions; // 500 ``` #### Trust mark attestation (standalone) ```typescript import { extractVerificationStatus, createVerificationStub } from '@elysea/core'; // In tests — no network const stub = createVerificationStub({ appId: 'my-app' }); const status = extractVerificationStatus(stub); ``` Deprecated aliases (removed in a future major release): extractCertificationStatus, createCertificationStub, createLegacyStub. #### Error types ```typescript import { ElyseaNetworkError, ElyseaSDKError, ERROR_CATALOGUE } from '@elysea/core'; import type { ElyseaErrorCode } from '@elysea/core'; const code: ElyseaErrorCode = 'BYOK_KEY_REQUIRED'; console.log(ERROR_CATALOGUE[code].action); ``` Error classes: ElyseaNetworkError, ElyseaSDKError, GuardianViolationError, QuotaExceededError, RateLimitError, ElyseaNotAvailableError, ElyseaMissingCountryCodeError, ElyseaUnsupportedProviderError. Error codes: AUTH_INVALID_KEY | AUTH_UNAUTHORIZED | AUTH_FORBIDDEN | RATE_LIMITED | QUOTA_CONVERSATION | GUARDIAN_BLOCK | NETWORK_ERROR | SERVER_ERROR | MISSING_COUNTRY_CODE | MISSING_CONSENT_TOKEN | CONSENT_PROOF_REQUIRED | BYOK_KEY_REQUIRED | PAYMENT_REQUIRED | PAYLOAD_TOO_LARGE | SANDBOX_CAP_EXCEEDED | BROWSER_SECRET_FORBIDDEN ElyseaNetworkError fields: statusCode, endpoint, body, errorCode, action, retryAfterSeconds. HTTP mapping: 400 GUARDIAN_BLOCK · 401 AUTH_UNAUTHORIZED · 402 PAYMENT_REQUIRED · 403 AUTH_FORBIDDEN · 413 PAYLOAD_TOO_LARGE · 422 BYOK_KEY_REQUIRED · 429 RATE_LIMITED or SANDBOX_CAP_EXCEEDED · 5xx SERVER_ERROR. 402 / 413 / 422 are never retried; SANDBOX_CAP_EXCEEDED is never retried; Retry-After is honoured. ElyseaSDKError fields: code, errorCode (alias), action. #### consumeChatStream ```typescript import { consumeChatStream } from '@elysea/core'; declare function appendToUI(text: string): void; declare function setPosture(posture: string): void; declare const myToolRegistry: Record) => Promise) | undefined>; await consumeChatStream(elysea.chat.send({ message: userInput, coreUserId }), { onText: (value) => appendToUI(value), onPosture: (posture) => setPosture(posture), onToolEmerge: async (payload) => { await myToolRegistry[payload.tool_id]?.(payload.props); }, onDone: (canonConformance, guardianAction) => console.log(canonConformance, guardianAction), onError: (message) => console.error(message), }); ``` Tool emergence (Decision Z): always initiated by the brain via the tool_to_emerge SSE field. The SDK routes the signal — never generates it. --- ## @elysea/react — Full reference Import: `import { ... } from '@elysea/react';` All hooks wrap an ElyseaClient — no direct HTTP calls in @elysea/react. BROWSER RULE: in the browser, pass a **proxy client without secret** that calls your own server route. Never call createElyseaClient() in a React component (throws BROWSER_SECRET_FORBIDDEN). ### Hooks - `useElyseaChat(client)` → { send, chunks, text, isStreaming, error, posture } - `useElyseaMemory(client, userId)` → { summary, tags, isLoading, error, refresh, write(item, consentToken, preuve), delete(itemId, consentToken) } - `useElyseaIdentity(client)` → { capabilities, isLoading, error, resolve } - `useElyseaInvoke(client)` → { invoke, result, isInvoking, error, reset } - `useElyseaGuardian(client)` → { violations, log, buildAndAudit(coreUserId, posture, action, pillar), isAuditing, lastAudit, error } - `useBillingMetrics(client)` → { events, count, refresh } (no ELYSÉA pricing exposed) - `useAttestationStatus(appId, verificationBaseUrl)` → VerificationStatus (initial 'unavailable') - `useVerificationStatus(appId, verificationBaseUrl)` → 'loading' | 'active' | 'revoked' | 'unknown' | 'error' ('revoked' only if the endpoint says so) - `useCertificationStatus` — @deprecated, use useAttestationStatus ### Components - `` — mounts "Powered by ELYSÉA" + ElyseaTrustLabel automatically. Takes no client. No prop hides them. - `` — never null: "Vérifié — mesures publiques" / "Verified — public measures" (link to https://elysea.app/verify?app=) with a valid signed attestation, otherwise "Non vérifié — mesures publiques" / "Not verified — public measures" (visible, no link). Default verifyBaseUrl: https://elysea.app. - `` — "Powered by ELYSÉA", always visible on every plan; plan: 'test' | 'production' | 'equipes' (same rendering). If revoked, the mention stays visible without link. - `` — read-only usage display. --- ## @elysea/widget — Full reference ### One-line HTML embed ```html ``` - `data-base-url` (URL of YOUR gateway) is required in production — no public ELYSÉA gateway exists. - `data-lang="fr|en"`. - `data-mode="demo"` with `data-key="pk_test_demo"`: demo = local simulation, no network. - Publishable key only (pk_). Secret keys (sk_) are rejected. - Gateway: user JWT verified (GATEWAY_JWT_*), otherwise anonymous session; Redis down = 503 (fail-closed). ### Programmatic usage ```typescript import { ElyseaWidget } from '@elysea/widget'; import type { WidgetConfig } from '@elysea/widget'; const config: WidgetConfig = { publishableKey: 'pk_test_XXXX', baseUrl: 'https://your-gateway.example.com', appId: 'my-app', environment: 'test', accentColor: '#5B3FE4', position: 'bottom-right', art50Text: 'You are talking to an AI.', // AI Act Art. 50 disclosure (non-empty) lang: 'en', }; const widget = new ElyseaWidget(config); widget.getHost(); // HTMLElement widget.getShadowRoot(); // ShadowRoot ``` ### Invariants - Shadow DOM: zero CSS/JS leak to host page - Guardian SDK active: 8 prohibitions checked locally before any network call - "Powered by ELYSÉA" always visible (P5) - Tool emergence: dispatches `elysea:tool_emerge` CustomEvent on the host element ```typescript declare const myToolRegistry: Record) => void) | undefined>; document.querySelector('[data-elysea-widget]')?.addEventListener('elysea:tool_emerge', (e) => { const { tool_id, props } = (e as CustomEvent).detail; myToolRegistry[tool_id]?.(props); }); ``` ### Exports ElyseaWidget, parseWidgetConfig, parseConfigFromScript, validatePublishableKey, detectWidgetEnvironment, BrokerClient, TEXTS, resolveLang. --- ## BYOK — target design (NOT available) BYOK is being finalised. The points below describe the target design, not a working feature. 1. Transmission: key submitted once via the portal (https://elysea.app/developers). SDK headers X-Elysea-Llm-* are ignored by the brain. 2. Storage: AES-256-GCM envelope encryption, KMS-encrypted DEK, per-builder isolation. Plaintext never stored. 3. Rotation: new key → 60 s drain → old key revoked. 4. Revocation: next call returns BYOK_PROVIDER_UNAVAILABLE; DEK destroyed. 5. ELYSÉA sees provider name, last-4 hint, status, last-used date — never the plaintext key. 6. If the key fails: BYOK_PROVIDER_UNAVAILABLE, never a fallback to ELYSÉA keys. 7. Guardian and firm_safety always run on ELYSÉA infrastructure, never on the builder key. 8. Safety layer never suspended. Today: no key can be activated → 422 BYOK_KEY_REQUIRED on every pipeline call to the real brain. --- ## 8 prohibitions — quick reference P1 BLOCK: Modify D0 invariants (disableDominationCheck, disableDependanceCheck, disableSubstitutionCheck, disableManipulationCheck, bypassCanon, overrideEthics, disableD0) P2 BLOCK: Access raw memory (/sdk/memory/raw, /sdk/memory/items) P3 BLOCK: Bypass safety gate (suppressSafety, skipFirmSafety, bypassFirmSafety) P4 BLOCK: Bypass pipeline canon (bypassPipeline, directLLM, skipPipeline, rawLLMCall) → also HTTP 400 PROHIBITION_P4 P5 BLOCK: Remove watermark (removeWatermark, hideWatermark, canonConformance:false) P6 BLOCK: Disable ethics telemetry (disableTelemetry, noTelemetry, telemetry:false) P7 BLOCK: Memory write/delete without consentToken P8 BLOCK: Verified status without valid attestation → also HTTP 400 PROHIBITION_P8 All block-mode. No plan exemption. --- ## Memory: proven consent required > **Effective with the next brain release** (elysea-core #1066, not deployed yet). The SDK already enforces it: without a proof, nothing is sent. ELYSÉA writes **no** memory without a consent proof supplied by you, the builder. The `consentToken` authenticates the operation (P7); it is **not** consent. **What you must show** — an explicit, separate memory consent text (not buried in terms and conditions), accepted by a positive action of the user. **What you must keep** — the exact text shown, its version, the time of consent and the user concerned, so you can prove the consent. **What you must send** — with every `memory.write`, a `MemoryConsentProof` (every field required and non-empty; the SDK never fills one in for you): | Field | Content | |-------|---------| | `granted` | `true` | | `consent_text_version` | version of the text shown | | `libelle_sha256` | `libelleSha256(text)` — hex SHA-256 of the UTF-8 bytes of the **exact** text shown, with no normalisation | | `consent_granted_at` | ISO 8601 time of consent, recorded by you | | `constructor_legal_entity` | your legal entity | | `app_id` | your application | ```typescript import { libelleSha256, ElyseaSDKError, ElyseaNetworkError, type MemoryConsentProof } from '@elysea/core'; // Your record of the consent (exact text shown, version, time) declare const consent: { textShown: string; version: string; grantedAt: string }; const proof: MemoryConsentProof = { granted: true, consent_text_version: consent.version, libelle_sha256: libelleSha256(consent.textShown), consent_granted_at: consent.grantedAt, constructor_legal_entity: 'Your Company Ltd', app_id: 'your-app', }; try { await elysea.memory.write( coreUserId, { type: 'preferences_communication', content: 'Short answers.', expires_at: '2027-01-01T00:00:00Z' }, consentToken, proof, ); } catch (e) { if ((e instanceof ElyseaSDKError || e instanceof ElyseaNetworkError) && e.errorCode === 'CONSENT_PROOF_REQUIRED') { // Memory disabled for this user: do not retry without a complete proof. } } ``` **Without a proof, memory stays disabled:** - missing or incomplete proof → `ElyseaSDKError` `CONSENT_PROOF_REQUIRED`, **before** anything is sent; - refusal by the brain (HTTP 403 `CONSENT_PROOF_REQUIRED`) → `ElyseaNetworkError` with `errorCode: 'CONSENT_PROOF_REQUIRED'` — never silent, never mistaken for an authentication error. **Withdrawal** — as soon as the user withdraws consent, stop sending the proof (no new write is then possible) and erase items already written with `memory.delete(coreUserId, itemId, consentToken)`. The brain does not yet expose an SDK route to withdraw the recorded consent itself. --- ## Error code catalogue AUTH_INVALID_KEY: Key must start with sk_test_ or sk_live_. AUTH_UNAUTHORIZED: 401 — key invalid, expired or revoked; HMAC secret may be wrong. AUTH_FORBIDDEN: 403 — key insufficient for the operation. RATE_LIMITED: 429 — retries exhausted. Honour Retry-After. QUOTA_CONVERSATION: conversationHistory >20 turns. GUARDIAN_BLOCK: P1-P8 violation. Check result.guardianLog.pillar. Never bypass. NETWORK_ERROR: network unavailable or timeout. SERVER_ERROR: 5xx or unreadable response. MISSING_COUNTRY_CODE: countryCode absent in ElyseaClientConfig. MISSING_CONSENT_TOKEN: memory.write/delete without consentToken (P7). CONSENT_PROOF_REQUIRED: memory.write without a complete builder consent proof (USR-05) — thrown before sending, or HTTP 403 from the brain. Never retry without a proof. BYOK_KEY_REQUIRED: 422 — no LLM key deposited and activated. BYOK is being finalised, not available. Do not retry. PAYMENT_REQUIRED: 402 — app suspended for non-payment (crisis handling stays active). Do not retry. PAYLOAD_TOO_LARGE: 413 — message or history too large. Shorten, do not retry as is. SANDBOX_CAP_EXCEEDED: 429 — test key reached 500 interactions/month. Never retried; use sk_live_. BROWSER_SECRET_FORBIDDEN: apiSecret or sk_* used in a browser. Create the client server-side. --- ## Known defaults and caveats - countryCode: never defaulted. Throws ElyseaMissingCountryCodeError if absent. - accountType: not sent by default (no presumed value). - region: fixed in the portal at app creation; config field deprecated and ignored. - conversationHistory: defaults to []. - guardianDegraded: null when the brain does not report it — do not read null as "Guardian active". - Malformed SSE chunks are counted (chunksDropped); a stream without [DONE] yields an error chunk. - Billing flush failure: events re-queued + console.warn, or onBillingFlushError if provided. - Voice: ELYSÉA does not speak and provides no voice (no speech synthesis, no transcription). Any voice is the builder application's own (« la voix éventuelle est celle de l'application du constructeur »): its engine transcribes, then the app sends the text to pipeline.run(). - useCertificationStatus / extractCertificationStatus: deprecated. Use useAttestationStatus / extractVerificationStatus.