Files
opportunities-extension/docs/architecture.md

27 KiB

Architecture and Compatibility Contracts

This document describes the current checkout, including behavior added after the 1.1.0 release. refactoring.md records the earlier modularization; historical observations there do not override the current contracts below.

The OpenSpec capability baseline and its archived source/test evidence map provide behavioral context. The initial change was completed and archived on 2026-10-31, promoting all twelve capabilities to durable specifications. Use those capabilities for subsequent delta changes.

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.
  • src/content/shortcut-intent.js runs at document_start in the top frame, preserving bookmark intent before Fusion rewrites the URL. The main content script runs at document_idle, in all matching frames on the two configured Fusion welcome pages. Their code also checks the hostname/path.
  • src/auth/spa-page.js runs at document_end as a separate top-frame-only SPA content script, copied into both distributions. It handles only extension messages for DOM readiness inspection and a fixed same-origin identity GET; it sends no requests on its own. services/spa/page-client.js connects this receiver to the background recovery flow and the existing debug envelope. No page-world script is injected.
  • There are no extension UI pages, injected page-context scripts, custom CSP, declared execution world, or web-accessible resources.
  • scripts/build.mjs defines permissions: bookmarks, cookies, storage, webRequest and declarativeNetRequestWithHostAccess. Host permissions cover eeho.fa.us2.oraclecloud.com, spa.oracle.com, and comcipapic-oalprod.integration.ocp.oraclecloud.com over HTTPS.
  • Bookmark access supports folder selection and shortcut detection. SR integration observes App Builder client IDs and installs narrow session rules for required Origin headers; it is not a general request proxy.

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 Plan projection details Per-panel WeakMap, shared in-flight requests, nested tooltip DOM, temporary pointer/resize listeners cleaned up on dismiss/tab change/modal close
Content Rate card Pure column/filter/sort model, controller with per-plan in-flight sharing, animated drawer with skeleton/search/table; response guards and cleanup on close or plan switch
Content Service Requests UI Customer-specific in-flight queries, skeleton/table modal, stale-response guards; no persistent cache
Content Organization Chart Working hierarchy, contact autocomplete/creation, transient email and contact-opportunity views
Content Manage Time Week drafts, dependent selections, validated hours and deferred writes/deletions
Content Authentication/debug General token promise, SPA authorizations, request records and visibility state
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
Background Organization Chart storage User/customer-scoped IndexedDB records and revision checks
Background SR and time integration Session preparation, resource lookup, service operations and header adaptation

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 generally abort network traffic. Token Relay specifically uses a 30-second timeout with AbortController when available. Its timeout also covers response reading; debug persistence failures cannot leave it pending. 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. Normal authorization reuse and debug capture have different storage lifetimes; see Storage Contracts before making claims about retained sensitive values.

Credential Acquisition Consumers Lifetime/recovery
XSRF Background cookie lookup, then content document.cookie fallback x-xsrf-token for Token Relay Cookie recovery on missing/invalid response with a bounded refresh attempt
Fusion general GET /fscmRestApi/tokenrelay Opportunity/saved-search, product, CRM SR and contact queries/writes Shared content acquisition; 30-second request timeout
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, consumption trend, plan detail and rate card authorization Same service response, distinct credential from ACCP-TS
SPA-ORANGE SPA consumer service uiaasHeader.value Customer service requests, fixed custsr-auth header with raw value, no Bearer prefix; ignores uiaasHeader.label Optional at consumer acquisition; required only when opening Service Requests; existing in-memory session rules
SAIB-TS SPA consumer service label/value Installed Base after account registry resolution Required when opening Installed Base
WWB-TS SPA consumer service value Workloads and Actions in Wwb-Provider-Authorization Shared consumer acquisition; data caching is separately scoped
SR integration session Integration cookies and Authorization: Session Internal SR and Manage Time operations; catalog preparation Required Origin and observed x-appbuilder-client-id where applicable; session preparation is separate from identity
Catalog authorization SR catalog/token preparation SR creation Operation-specific headers; do not replace with the Fusion general token

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.

Identity is validated before dependent data requests and then reused. Content shares its pending identity promise; background shares by cookie store/private context and can restore user id/name/roles from extension session storage. A page reload alone therefore does not force identity revalidation. SALES_REP blocks CRM activity data, badges and Manage Time entry points. Independent SR session navigation can start concurrently before the role result is consumed.

SPA bootstrap uses https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=whitespace. Recovery rejects login/error/callback documents before a same-origin identity probe. It closes owned tabs and preserves the originating window/container. SR preparation uses the SeaaS_initiation external application, with a 70-second timeout and manual retry. Internal SR detail recovery instead uses a hidden iframe and bounded, shared 401 checks, not a visible tab or an unlimited request loop.

Request Method/path Important contract
requestOpportunitiesSavedSearch GET /crmRestApi/searchResources/11.13.18.05/custom-actions/queries Opportunity entity; require one isDefault=true result with a valid savedSearchNumber
requestOpportunities POST /crmRestApi/searchResources/11.13.18.05/custom-actions/queries copiedFrom=queries/<default savedSearchNumber> across pages; RecordSet OR plus EffectiveDate AND; Preference transient; pages of 15
requestSRSearchResources GET /crmRestApi/searchResources/latest/custom-actions/queries CRMServiceRequest entity; use the first item's savedSearchNumber
resquestCrmServiceRequest POST /crmRestApi/searchResources/latest/custom-actions/queries Dynamic copiedFrom; pending-delivery team SRs; all pages; incremental badge updates
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
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}]
requestCustomerServiceRequest GET provider-proxy/g2m-custsr-provider/service/oscSr/{accountSummaryId} SPA-ORANGE; summary id, not registryId; encoded filter {hierarchy:"single",status:"open"}, creationDate descending, pages of 40 using totalResults/count/offset
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
requestConsumptionPlanDetail GET provider-proxy/g2m-consumption/service/v1/plan/plandetail/{subPlanNum}?active=true Raw OCICONS-TS in G2m-Authorization; array of lines with optional nested clines; no retry or persisted cache
requestRatecardByPlan POST provider-proxy/g2m-consumption/service/v1/ratecard/byPlan {planNumber,recordsSize:10000}, where planNumber is the selected plan's subPlanNum; raw OCICONS-TS in fixed G2m-Authorization; response must be an array of records; no retries, cursor pagination or persistent cache

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.

After all opportunity pages arrive, the ready list is displayed before background SR and product loading start. Completed product cycles use successful/reused SR results before Workload Actions. Cached products can still require activity network reads on cache miss. Failed SR/Action lookups remain unknown rather than confirmed absent. Actions currently fetch only their first page and disclose hasNext; contact opportunities and account workloads consolidate all pages.

Contact search uses Fusion searchResources/latest POST with a customer PartyId criterion, keywords, Preference transient and the general Bearer. Contact opportunities use the person's opaque PartyNumber in the GET filter, including alphanumeric identifiers. Contact creation uses the selected customer's account identifiers and an explicit Save; local hierarchy persistence is independent.

Message Contracts

The examples below have the opportunitiesExtension. prefix; the complete name inventory is in src/shared/messages.js. 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/installBase/customerServiceRequests, debugRequests
requestCustomerServiceRequest partyNumber, authorizations.accountSummary/customerServiceRequests ok, items (consolidated response.objects), debugRequests; error envelope on failure
requestAccountPlans partyNumber, authorizations ok, accountPlanId, plans, debugRequests
requestConsumptionTrend subPlanNum, period, authorization ok, subPlanNum, period, items, debugRequests
requestConsumptionPlanDetail subPlanNum, authorization ok, subPlanNum, items (including clines), debugRequests; error envelope on failure
requestRatecardByPlan planNumber, authorization ok, planNumber, items, debugRequests; error envelope on failure

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
Page localStorage: opportunities-extension-table-sort-v1-<table> Sort key and direction, independently per supported table; memory fallback if storage is unavailable
Page localStorage: opportunities-extension-manage-time-reference-date Valid selected date; falls back to today
Page localStorage: opportunities-extension-debug:<tabId>:<id> and :index Full captured request records and index; may retain sensitive originals; quota failures fall back to memory
Page sessionStorage: opportunities-extension-debug-tab Namespace identifier for debug records
Page sessionStorage: opportunitiesExtension.shortcutIntent Early bookmark timestamp; consumed once and expires after ten minutes
Extension storage.local: opportunitiesExtension.opptyProductsCache.v1 Object indexed by OptyNumber, each entry {fetchedAt,items}
Extension storage.local: opportunitiesExtension.workloadsCache.v1 Workload data by normalized principal/customer; {fetchedAt,items}; no persistent entry without a principal
Extension storage.local: opportunitiesExtension.activities.v1:<scope> SR/Action records scoped by principal, kind and identifier; SR keys also include product-update timestamp
Extension storage.local: opportunitiesExtension.activities.invalidatedAt.v1 Invalidation timestamp preventing obsolete activity snapshots from being reused or rewritten
Extension storage.session: opportunities-extension-current-user:<context> Validated {id,name,roles}, partitioned by cookie store/private context; no identity token in this cache
Extension storage.session: opportunitiesExtension.resourceUsers.v1 Resource user, email and fetchedAt; shared concurrent reads and TTL validation
Extension storage.session: opportunitiesExtension.srAppBuilderClientId.v1 Observed integration origin/client ID/timestamp; one-hour validity
Extension-owned IndexedDB: opportunities-extension-organizations, store charts Database version 3; user/customer key, record schemaVersion 1, revision, updatedAt and normalized nodes
Page memory: form request cache Allowlisted read results, shared in-flight promises, maximum 500 entries, scoped to current user
Page memory: active views and authentication Working charts, transient contact email, week drafts, opportunity results, token promises and service authorization values

Product TTL defaults to 24 hours and is capped at 30 days by the background cache. Activity TTL uses the Preferences duration, also capped at 30 days, anchored to the product-update timestamp rather than response time. SR snapshots preserve different update timestamps; expired SR snapshots are pruned on subsequent writes. The form request cache defaults to 24 hours and caps its duration at seven days; it passes that duration to supported operations, including workload/resource reads. Workload reuse preserves the original timestamp instead of restarting the TTL.

The product cache is keyed by opportunity number, not user or container. Do not extend the stronger principal/customer scoping of workload and activity caches to that cache by assumption. Identity caching has browser-session lifetime and its own cookie-store/private-context partition, with memory fallback where extension session storage is unavailable. Browser storage APIs are distinct from page storage.

Form cache reuse excludes writes and token generation. Successful supported writes invalidate reusable form reads; concurrent reads share promises and suppress duplicate debug envelopes. Refresh all invalidates related product, workload, activity and form data before reloading Reference, while preserving preferences and authentication. Generation checks prevent old reads from restoring invalidated snapshots. Invalidation failure is surfaced rather than silently accepted.

Organization-chart nodes retain id, parentId, name, jobTitle, notes, collapsed, optional contactPartyId and alphanumeric PartyNumber. Email is excluded; older stored email fields are removed during migration. Chart storage is isolated by normalized user/customer and browser profile, enforces revision checks, and has no cross-device sync. Clearing a Fusion page's IndexedDB does not clear the extension's IndexedDB, and an already-open working chart can still exist in page memory.

Storage failures have feature-specific behavior: preferences and data caches can fall back to current memory/results, debug keeps records in memory with feedback, and chart persistence reports failure rather than claiming a durable save. Recreating the background loses in-flight jobs while restoring supported stored data on demand. Expiration does not imply that every cache physically deletes its old records immediately.

UI Contracts

Debug lifecycle

features/debug/state.js stores each changed request under the current tab's localStorage prefix and maintains an index, batching writes with a short timer. Opening Debug Requests reloads saved records while retaining handles used by in-flight requests. Hidden debug does not construct request-detail DOM; opening renders summaries and expanding a record mounts details. Storage exceptions keep the memory records usable and do not block the recorded network operation.

services/fusion/opportunities.js resets debug immediately before sending page one, after resolving the saved search. Thus earlier authentication and saved-search logs can disappear at that boundary. Pagination does not reset history, and closing the main modal does not delete it. Reset clears only the current tab prefix and invalidates previous-generation completions, preserving preferences and other tabs.

The default UI masks sensitive values and the explicit visibility toggle can show retained originals, including tokens in captured responses. Individual transports also mask selected headers, but that is not a universal redaction guarantee. localStorage records can contain sensitive values; display masking is neither encryption nor protection from other code with access to that page's storage. The normal identity cache excludes its token, which does not imply the debug response excludes it. This documentation change does not alter retention policy.

Shared interactions

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; the active Reference choices use supported fiscal/date ranges rather than a RENEWALS view.

Preserve SVG chart data normalization, animation, tooltips and series selection. Treat displayed API text as data. Debug masking and DOM isolation have limitations; do not equate display masking with full redaction or encryption at rest.

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, date/debug utilities and the organization-chart model can be 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, current-user.js, token-relay.js, spa-credentials.js, sr-request-headers.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
org charts and contacts features/org-chart/, services/fusion/account-contact.js Local hierarchy/storage vs Oracle contact operations
Manage Time features/manage-time/, services/fusion/time-entries.js Weekly UI drafts vs session-authenticated reads/writes
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 restores supported product, identity and session metadata. 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. Identify the affected OpenSpec capabilities and propose the behavior delta, 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. 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.