# Architecture and Compatibility Contracts This document describes the integrated modular implementation. Verification and preexisting risks are tracked in [refactoring.md](refactoring.md). ## Sources, Outputs, and Runtime Boundaries - Editable sources are factory modules under `src/`; the former monoliths were removed. - `scripts/sources.cjs` explicitly lists modules for each context. `compose.cjs` validates syntax, serializes CSS, wraps the modules in an IIFE and calls the corresponding application factory/start method. `build.mjs` writes two packages. - `dist/chromium` serves Chrome and Edge; `dist/firefox` serves Firefox. Do not edit outputs. - Both manifests use V3. Chromium has a classic background service worker; Firefox uses `background.scripts`. Neither entry point is declared as a module. - Content scripts run at `document_idle`, in all matching frames on the two configured Fusion welcome pages. Their code also checks the hostname/path. - There are no extension UI pages, injected page-context scripts, custom CSP, declared execution world, or web-accessible resources. - Permissions remain `cookies` and `storage`, with host access limited to the configured Fusion host and `spa.oracle.com`. Content owns the DOM and per-frame state. Background owns privileged cookies, temporary tabs, extension storage and SPA requests. These contexts communicate using serializable runtime messages, not shared variables. Page localStorage is different from extension storage and is not a safe place for credentials. ## Current Ownership and Side Effects | Context | Responsibilities | State and effects | | --- | --- | --- | | Content | Fusion tile and main/preferences/consumption modals | DOM nodes, injected CSS, keyboard/pointer/resize listeners, MutationObserver | | Content | Opportunity query and presentation | Filter sets, sorting, search, request versions, column formatters, dashboard animation | | Content | Products UI | Current request ID, product map, expanded rows, progress, last update | | Content | Consumption UI | Plan state, per-plan/period trend cache, chart navigation and selected series | | Content | AI summary | Per-opportunity in-flight state and summary; fresh generation on expansion | | Content | Authentication/debug | General and scoped token promises, SPA authorizations, request logs, visibility flag | | Background | Message dispatcher | Synchronously registered listener, asynchronous replies using `return true` | | Background | Products | Four workers, per-opportunity in-flight map, lazy cache read and serialized writes | | Background | Cookie/session recovery | Inactive tabs, polling/retry delays, cleanup in `finally` | | Background | SPA operations | HTTP/JSON parsing, account/plan extraction and debug envelopes | Closing the main modal hides it rather than destroying it. Many requests can continue while it is hidden. Version and request-ID checks suppress stale UI updates; they do not abort network traffic. No AbortController is currently used. Some listeners have window guards; there is no general disposal lifecycle. ## Authentication and API Routing Never substitute one token family for another or assume an opaque token is a JWT. Production tokens are held in memory, not deliberately persisted by the extension. | Credential | Acquisition | Consumers | Lifetime/recovery | | --- | --- | --- | --- | | XSRF | Background cookie lookup, then content `document.cookie` fallback | `x-xsrf-token` for both tokenrelays | Cookie recovery tab on missing/invalid response; existing retry rules | | Fusion general | GET `/fscmRestApi/tokenrelay` | Opportunity POST and product GET Bearer | Content promise/cache; no expiry-based refresh equivalent to Sales Common | | Sales Common | GET `/fscmRestApi/tokenrelay?scopesfq=urn%3Aopc%3Aresource%3Afusion%3Aeeho%3Asales-common%2F` | AI summary Bearer | In-memory `expires_in`, shared in-flight acquisition; at most one summary retry after 401/403 | | ACCP-TS | SPA consumer service `uiaasHeader.value` | Account summary authorization | Retrieved with the consumer service response; expiry recorded, not automatically acted on | | OCICONS-TS | SPA consumer service `uiaasHeader.value` | Account plans and consumption trend authorization | Same service response, distinct credential from ACCP-TS | Fusion calls use `https://eeho.fa.us2.oraclecloud.com`; SPA calls use `https://spa.oracle.com/oalcrm/web/api/`. Preserve `credentials: include` and operation-specific headers rather than unifying all calls under a default token. | Request | Method/path | Important contract | | --- | --- | --- | | requestOpportunities | POST `/crmRestApi/searchResources/11.13.18.05/custom-actions/queries` | RecordSet OR plus EffectiveDate AND, original fields, Preference transient, pages of 15 | | requestOpptyProducts | GET `/crmRestApi/rest/rv:be91c002-2e5d-4ed2-a37e-e0c837bf141f/en/11.13.18.05:9/opportunities/{number}/child/ChildRevenue` | Encoded identifier, original query/fields/order, count/offset pagination, maximum 250 pages | | requestGenerateAiSummary | POST `/api/sales-common/gen-ai/v1/summarizations/generate` | `{path,promptCode,object,puid}`; prompt `cx.sales.generate_opportunity_summary`; only summaryText displayed | | requestConsumerTokenService | GET `g2m-consumer-application/consumerTokenService` | No-store; parse JSON; HTML 200 recovery via inactive SPA UI tab; maximum five HTTP attempts | | requestAccountSummary | POST `provider-proxy/g2m-account/service/accountsummary` | ACCP-TS; `[{key:"regId",value:partyNumber}]` | | requestAccountPlans | POST `provider-proxy/g2m-consumption/service/v1/account/accountPlans/{id}` | OCICONS-TS; original hierarchy/products/language/planStatus payload | | requestConsumptionService{Period} | GET `provider-proxy/g2m-consumption/service/v1/plan/trend/{subPlanNum}?quarter=All&timePeriod={period}` | G2m-Authorization; quarterly/monthly/weekly/daily; weekly default | SPA parsing removes BOM and the existing anti-JSON-hijacking prefixes before JSON.parse. Content uses response.json directly. Do not silently unify these behaviors. POST retry policies require operation-specific review; do not add generic retries. ## Message Contracts All names below have the `opportunitiesExtension.` prefix. Unknown messages return false without a response. Supported background messages return true immediately and later call sendResponse. `platform/background-api.js` centralizes the existing callback/promise differences. `platform/content-messages.js` retains the original cookie callback path as well as general runtime messaging; no browser behavior was silently changed during extraction. | Message | Input | Result | | --- | --- | --- | | getXsrfToken | type | ok, cookieName, token, matchedCookieNames, lookupDetails; error on failure | | refreshXsrfCookie | type; sender tab context | ok, refreshed, cookieName, durationMs, error | | requestOpptyProducts | requestId, accessToken, optyNumbers, cacheMaxAgeMs | ok, requestId, completed, total, failed, cacheHits; error envelope on dispatcher failure | | opptyProductsProgress | Background to sender tab/frame | requestId, state, counters; per-item optyNumber/items/fromCache/cachedAt or error | | requestConsumerTokenService | type; sender tab context | ok, authorizations.accountSummary/accountPlans, debugRequests | | requestAccountPlans | partyNumber, authorizations | ok, accountPlanId, plans, debugRequests | | requestConsumptionTrend | subPlanNum, period, authorization | ok, subPlanNum, period, items, debugRequests | SPA failures return `{ok:false,error,debugRequests}`. Progress states include `running`, `complete` and `complete-with-errors`; content additionally tracks idle and error states. A new content request ID rejects progress from an older job. Do not expose a caller-provided arbitrary request URL through the background. ## Storage Contracts | Location/key | Value | | --- | --- | | Page localStorage: `opportunities-extension-filter-preferences` | JSON object: period, stages, statuses, customerKeys, ownerKeys, customerSearch, ownerSearch, tableSearch; unset customer/owner selections may be null | | Page localStorage: `opportunities-extension-theme` | `light` or `dark` | | Page localStorage: `opportunities-extension-opportunity-type-preferences` | JSON object: opportunityTypeValues, opptyProductsCacheHours | | Extension storage.local: `opportunitiesExtension.opptyProductsCache.v1` | Object indexed by OptyNumber, each entry `{fetchedAt,items}` | Product TTL defaults to 24 hours and is capped at 30 days in background. Storage failures are tolerated for the active view. Recreating background reloads stored products but does not restore in-flight jobs. Cache TTL does not physically delete expired entries. Account/container isolation limitations are tracked separately. ## UI Contracts Preserve labels/IDs/classes, column order, URLs, clipboard behavior, sort/filter semantics, formatting, theme colors, keyboard shortcuts and row expansion. WON includes WON_PENDING without rewriting StatusCode. Owner uses first/last name with the existing fallbacks. Changing reference resets customers/owners to all; closing/reopening preserves search. Fiscal years begin in June; RENEWALS currently has no date range and therefore does not load opportunities. Preserve SVG chart data normalization, animation, tooltips and series selection. AI text is inserted with textContent. Debug masking and DOM isolation have known limitations; see the separate security backlog rather than assuming full redaction. ## Module Ownership and Dependencies `content/main.js` and `background/main.js` only initialize. Each `application.js` is a composition root: constructs feature-owned state and factories, wires explicit function dependencies, then exposes `start`. The returned components are available to isolated tests, not published on the page/global object in production. Factories do not import or instantiate peers. Features receive service/auth ports; services/auth receive transport/platform ports; pure models receive data. The composition roots supply callback notifications for UI updates and debug events, preserving existing interactions without module-import cycles. Shared messages are the only source included in both contexts, with distinct instances/state. Some stateful UI collaborators intentionally share a feature-owned state object; these objects are created per application, never stored in a global registry. | Original responsibility | Integrated module(s) | Reason for boundary | | --- | --- | --- | | content initialization/tile | `content/main.js`, `fusion-page.js` | Page lifecycle and route guard | | modal/theme/CSS | `content/modal.js`, `theme.js`, `styles.js`, `styles/redwood.css` | DOM shell separate from operations and cascade | | fiscal dates/query payload | `features/opportunities/fiscal-periods.js`, `services/fusion/opportunities.js` | Pure date rules vs HTTP operation | | table/search/filters/formatting/dashboard | `features/opportunities/` | Related presentation responsibilities, explicit state | | saved settings | `features/preferences/storage.js`, `view.js` | Serialization/fallbacks vs dialog | | token families | `auth/content-session.js`, `sales-common.js`, `token-relay.js`, `spa-credentials.js` | Different credentials and lifetime policies | | cookie lookup/recovery, SPA session | `auth/xsrf-*.js`, `consumer-session.js` | Privileged recovery separate from UI | | product queue/cache | `features/products/background-jobs.js`, `cache.js`, `services/fusion/products.js` | Scheduling, persistence and pagination | | product progress/details | `features/products/content-jobs.js`, `details.js` | Message consumer vs row rendering | | AI summary | `features/ai-summary/content.js`, `state.js` | Per-opportunity lifecycle, safe text and auth port | | consumption | `features/consumption/`, `services/spa/` | Plans, chart model/view/controller vs HTTP operations | | debug | `features/debug/content-log.js`, `content-view.js` | Retention/formatting vs DOM presentation | | background dispatcher | `background/messages.js`, `shared/messages.js` | Stable serializable message contracts | | browser APIs | `platform/background-api.js`, `content-messages.js` | Only APIs actually used, not a universal wrapper | The background composition deliberately has no DOM dependency. Listener registration is synchronous, before any storage read or request. Worker restart reconstructs volatile state and lazily reads the existing product cache; tokens are not persisted. Content effects (DOM, fetch, storage, animation) enter through the environment at the composition root. localStorage access stays lazy to retain blocked-storage fallbacks. Tests may inject effects or individual callback ports without changing source strings. Time-sensitive modules are tested with isolated clocks. ## Adding a Feature 1. Define its message/API/storage contract and synthetic expected behavior first. 2. Add a factory under the owning feature, or a service under the correct host. Accept narrow function dependencies; do not call another factory inside it. 3. Add its source path to the appropriate array in `scripts/sources.cjs`. Source inclusion and duplicate factory checks run automatically. 4. Construct it in that context's `application.js`, supplying auth, platform and render callbacks explicitly. Extend `background/messages.js` only if crossing contexts is necessary; do not expose arbitrary destinations. 5. Add a `tests/*.test.cjs` suite using `tests/support/application.cjs` for composed integration or `vm.Script` for an individual pure factory. Use only synthetic data and fail unexpected network requests. 6. Run tests, build and `--check`; inspect the local fixture and load both packages in the target browsers. Record any browser not available for verification. As a concrete example, adding a new consumption period should first define its request/data/authorization contract and synthetic tests, then extend the period allowlist and normalization/presentation through the existing trend message. It must not add a URL-proxy message or reuse the general Fusion token. These edits belong in the consumption feature and SPA service, not the bootstrap. ## Source vs Generated Code Distribution `content.js` and `background.js` are generated bundles and may remain large. No copy/export/execute JavaScript generator feature was found in the source. AI summaries are text, not executable JavaScript. No empty generators subsystem was added. Build serialization uses JSON.stringify for CSS, retains quotes, backslashes, Unicode and line breaks, and is syntax-checked by vm.Script. Visual fixtures now load composed scripts normally; they no longer eval rewritten sources.