
Release of Aperture for the InterSystems contest entry.
A management portal for InterSystems IRIS, with no separate application server.
See who loses what before you save, follow every background job, read every log and find the entries worded like any of them with IRIS Vector Search.
Online demo · For judges · Why Aperture · Run it · Architecture · Bonuses · Verification

Aperture is an entry for the InterSystems Programming Contest: Build Your Own Management Portal.
It is a set of static files that IRIS serves itself. Your browser calls IRIS’s own
SysAdmin REST API (/api/admin,
273 operations) directly, signed in as you, so IRIS checks your privileges on every call and audits
you by name. The one thing that API cannot give you, the instance’s log files, comes through a small
read-only reader the IPM package adds, written in Embedded Python, with a wording index on IRIS Vector
Search beside it.
databases, open USER and choose202 Accepted lands in the Job Center with console output and progress.SYS), open an entry and choose.http file.operator / SYS: the security area disappears, and every disableddocker run -d --name aperture -p 127.0.0.1:52773:52773 ghcr.io/mxsalata/aperture,_SYSTEM / SYS (more).zpm "install iris-aperture", then /aperture/index.html on the| Demo account | Password | Privileges | What it shows |
|---|---|---|---|
_SYSTEM |
SYS |
everything | the whole portal |
operator |
SYS |
%Admin_Operate, %Admin_Task, %Admin_Journal |
security screens disappear, actions explain what they need |
auditor |
SYS |
%Admin_Secure only |
only the security area is usable |
The demo is Aperture itself, built with a mock of the whole API that runs in the browser (Mock
Service Worker), seeded to look like a busy instance with interoperability productions, tasks,
journals, users and audit history. Writes change that in-memory instance; the same mock drives the
unit and browser tests. Every real deployment also has a Try the demo button on its sign-in page.
POST /login gives you your ownGET /info) and shows exactly that.%All holders, certificates, tasks,202 Accepted answers become jobs by themselves; the Jobmessages.log, alerts.log, SystemMonitor.log and.http file or curl (Ideas Portal DPI-I-813).![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| Contest area | Where in Aperture | Boundary |
|---|---|---|
| Web applications and REST APIs | Security → Web applications (list, detail, JWT and CORS settings, create, edit, delete); every /v2/web-app* operation in the API Explorer; REST services (every REST application and its routes, from /api/mgmnt; the routes export as a Postman collection or a .http file, any route copies as curl: Ideas Portal DPI-I-813) |
/api/mgmnt takes a password only, so a JWT session asks for it once (kept in the tab’s memory); routes are what the dispatch class declares |
| Permission management | Users, Roles, Resources, Services, SQL privileges; escalation-role login; every edit reviewed field by field against a fresh read; a change that would leave nobody able to administer security is refused, and one to a role says who loses what | the portal adds no privilege of its own: what the account cannot do stays visible and disabled, with the resource it needs |
| Security and secrets | TLS & certificates (configurations with a connection test, X.509 credentials with certificate expiry); Wallet & OAuth 2.0 (collections with their use and edit resources, write-only secrets with usage, allowed hosts and TLS; the authorization server and its clients, the servers this instance is a client of with their client configurations, resource servers); audit settings; LDAP, MFT, encryption and the remaining OAuth writes in the Explorer | passwords, secrets, tokens and private keys are redacted before they are rendered, copied or exported; wallet secret values are never fetched |
| Task management | Tasks (schedules, run now or at a time, suspend and resume, history), the task manager daemon, upcoming runs | the task is re-read after every change and the page says when IRIS reports something else |
| OS management | Dashboard, Host monitor (CPU, memory, disk from /api/monitor), Processes, Devices (with the telnet and default-device settings), Locks, Databases, Namespaces, Journals, License, Web sessions; ECP, external language servers and file-system access through the Explorer |
host measurements come from the native monitor service, not from the SysAdmin API |
| The logs | Logs hub → messages.log, alerts.log and SystemMonitor.log with their rotations (any messages.old_* file selectable, Ideas Portal DPI-I-966; newest first, older windows on request, severity filter), Similar entries on any entry (IRIS Vector Search: the entries worded like it, with how often and when), the audit log (asynchronous query, events, purge), journal records, task history, this tab’s changes; a security change can be matched to the audit record that proves it |
the log files have no route in the SysAdmin API: they are read by the package’s own read-only reader (/api/aperture, Embedded Python) in bounded windows, never whole; ^ERRORS and SQL diagnostics stay in the classic portal |
The published image, IRIS Community 2026.2 with Aperture installed through its IPM package:
docker run -d --name aperture -p 127.0.0.1:52773:52773 ghcr.io/mxsalata/aperture
Then open http://localhost:52773/aperture/index.html (the built-in web server needs the file name, a
bare /aperture/ answers 404) and sign in as _SYSTEM (or SuperUser) with SYS. That is the
image’s demonstration password; -e IRIS_PASSWORD=... makes another one the password of every
enabled account (but CSPSystem, the image’s own Web Gateway account) at every start. With Docker
Compose, https://github.com/MxSalata/aperture/blob/main/docker-compose.yml runs the same image and takes IRIS_PASSWORD
from a .env next to it (https://github.com/MxSalata/aperture/blob/main/.env.example): docker compose up -d. CI publishes the
image only after checking it against the SysAdmin API, both with a password set at start and with
the demonstration one: latest and the version from main, edge from the development branch.
From the sources, with nginx serving the portal in front of IRIS:
git clone https://github.com/MxSalata/aperture.git
cd aperture
docker compose -f https://github.com/MxSalata/aperture/blob/main/docker-compose.build.yml up --build
/api/admin, /api/monitor, /api/mgmnt and /api/aperture to IRIS: same origin, so no CORS and no Basic-auth pop-ups; sends a Content-Security-Policy). A second instance goes behind the same nginx under a path prefix (/iris-b, the pattern is documented in https://github.com/MxSalata/aperture/blob/main/docker/nginx/default.conf.template) and gets a connection profile with that prefix as its base URLwww/ build, refreshed with npm run build:www)_SYSTEM with IRIS_PASSWORD from .env, or with SYS when none is set.docker-compose.override.yml next to the compose file: services: { iris: { cpuset: "0-19" } }.127.0.0.1 only by default. To use it from other machines, set IRIS_PASSWORD, put TLS in front (nginx and IRIS’s web server speak plain HTTP), then start with APERTURE_BIND=0.0.0.0.The iris service is built from https://github.com/MxSalata/aperture/blob/main/docker/iris/Dockerfile on top of
intersystems/iris-community:2026.2, pinned by digest (sha256:cd2ebcab…50bdaa, Build 221U).
IRIS for Health Community 2026.2 is tested too, against a real instance
(evidence): put
IRIS_IMAGE=containers.intersystems.com/intersystems/irishealth-community:2026.2@sha256:7c06b6b3d950bc25f3e353b0a65db0c4045f251fb9103eade63514302b662cf3
in a .env file next to the compose file. The portal image pins node:22-alpine and
nginx:1.30-alpine by digest, and CI pins every GitHub Action to a commit. At build time
https://github.com/MxSalata/aperture/blob/main/docker/iris/init.script fetches the InterSystems Package Manager from the
community registry (installer 0.10.9, checked against its SHA-256) and installs Aperture through its own IPM package (zpm "load" of https://github.com/MxSalata/aperture/blob/main/module.xml): the built portal is copied into the instance’s manager
directory (mgr/aperture/), the /aperture web application is created, the /api/aperture log reader
(Aperture.API over Aperture.Logs, Embedded Python)
is created, and Aperture.Installer, also Embedded Python, enables
/api/admin with password + JWT authentication. The build log ends with the installer’s readiness
report; you can print it again at any time:
docker exec aperture iris session IRIS -U USER "##class(Aperture.Installer).Doctor()" # aperture-iris when built from the sources
zpm "install iris-aperture"
copies the pre-built portal (www/) into the instance’s manager directory (mgr/aperture/, which
durable %SYS keeps across container updates), creates the /aperture
web application and the /api/aperture log reader (a read-only %CSP.REST class over an Embedded
Python file reader, needs %Admin_Operate:USE) with its wording index (Aperture.LogLine, a
%Library.Vector column under an HNSW index, the package’s only tables), and runs Aperture.Installer,
an Embedded Python class that enables /api/admin with password and JWT authentication and prints a
readiness report (IRIS version, JWT availability, the API’s authentication settings, the portal’s
files, the log reader and the log files it can read). Then open
http://<host>:52773/aperture/index.html. From a checkout, zpm "load /path/to/aperture"
installs the same package; ##class(Aperture.Installer).Doctor() prints the report again.
/api/admin web application. Aperture speaks SysAdmin API v2, which ships with 2026.2; IRIS 2026.1 serves only v1 (every /v2 path answers 404) and older releases have no SysAdmin API, so sign-in there stops with an explanation. Sign-in uses JWT and falls back to HTTP Basic when /login is unavailable (for example with JWT authentication switched off on /api/admin).%Admin_* privilege (GET /info refuses everyone else)./api/aperture web application (created by zpm "install", zpm "load" and the Docker image; password authentication, %Admin_Operate:USE). On an instance without the package that screen says so and everything else works./api/admin/v2 sends no CORS headers, by design (InterSystems, in the contest announcement thread, 22 September 2026), so a browser cannot call it across origins whatever the web application’s allow-list says. The three supported topologies are all same-origin: nginx in front of IRIS (compose), the /aperture web application served by IRIS itself (IPM), and the Vite dev server’s proxy. Further instances go behind the same nginx under a path prefix.npm install
cp https://github.com/MxSalata/aperture/blob/main/.env.example .env # VITE_IRIS_URL=http://localhost:52773
npm run dev # http://localhost:5173, /api/admin proxied to IRIS
Other scripts:
| Script | What it does |
|---|---|
npm run build |
type-check + production build (dist/, browser routing, for nginx) |
npm run build:www |
build for IRIS-hosted deployment (www/, relative URLs + hash routing) |
npm run build:demo |
build the online demo (dist-demo/, in-browser mock) |
npm test |
Vitest unit tests (client, auth, privileges, async jobs, spec index) |
npm run test:e2e |
Playwright end-to-end tests against the demo build, with axe-core WCAG 2.1 AA audits |
npm run smoke |
walks the main screens in headless Chromium and refreshes docs/screenshots/ |
npm run verify:live |
conformance check of a real instance (IRIS_URL, IRIS_USER, IRIS_PASSWORD), the log reader included, saves JSON evidence |
npm run gen:api |
regenerate src/api/schema.d.ts and the operation index from spec/mainspec_v2.json |

| Endpoint | What Aperture uses it for |
|---|---|
/aperture |
the portal’s static files, served by IRIS (or by nginx, or by GitHub Pages for the demo) |
/api/admin |
SysAdmin API v2: administration, sign-in (JWT, or HTTP Basic where JWT is off) and the account’s privileges |
/api/monitor |
host CPU, memory and disk, licence use and the interoperability metrics |
/api/mgmnt |
the instance’s REST applications and the routes their dispatch classes declare |
/api/aperture |
the package’s own read-only log reader and wording index (Embedded Python, IRIS Vector Search) |
The portal and the APIs share an origin (the SysAdmin API sends no CORS headers, by design), so the
same build is served by IRIS itself, behind nginx, or by the Vite dev server’s proxy; the demo swaps
the network for the in-browser mock. More in https://github.com/MxSalata/aperture/blob/main/docs/ARCHITECTURE.md.
openapi-typescript turns spec/mainspec_v2.json into src/api/schema.d.ts;openapi-fetch gives compile-time checked paths, query parameters, bodies and responses for every operation.call() / result() unwrap { status: { Errors, summary }, console, result }ApiError carrying the server’s summary and console lines.POST /login → JWT access + refresh tokens (IRIS 2026.2+). A fetch middleware adds/login does not exist, the client falls back to HTTP Basic.role field of /login.202 Accepted, parses the Location/v2/async-result?id=…) and registers a job. The Job Center polls untilFinished | Failed | Canceled, shows Console, Result and ProgressCurrent/ProgressTotal, andGET /info reports the user’s %Admin_* permissions. Navigation, the commandscripts/build-spec-index.mjs) extracts a compact indexsrc/lib/requestExport.ts writes a Postman collection (format.http file or a curl command from any list of generated requests, whether they comefeatures/explorer/requests.ts) or from a REST application’sapi/mgmnt.ts); the password stays in the browser and body secrets are redacted./api/aperture, a read-only %CSP.REST class (https://github.com/MxSalata/aperture/blob/main/ipm/cls/Aperture/API.cls) over anLogs.cls). It catalogues messages.log (wherever ConsoleFile putsalerts.log, SystemMonitor.log and their rotations, and answers windows of whole lines of%Admin_Operate:USE. The browser parses the lines (src/lib/messagesLog.ts); the mock servessrc/mocks implement the API against an in-memory instance,| Bonus | Where to see it |
|---|---|
| Community Opportunity ideas | DPI-I-966: every rotated messages.log readable in the portal (Logs → Messages log). DPI-I-813: requests generated from OpenAPI for Postman, VS Code and curl (REST services → Export; API Explorer) |
| Embedded Python | Aperture.Installer (configures /api/admin, prints the readiness report), Aperture.Logs (the log reader), Aperture.LogIndex and aperture_vectors.py (the wording index) |
| Vector Search | Aperture.LogLine: a %Library.Vector of 256 doubles under a %SQL.Index.HNSW index; /api/aperture/logs/similar ranks with VECTOR_COSINE (Logs → Messages log → Similar entries) |
| Docker | ghcr.io/mxsalata/aperture, https://github.com/MxSalata/aperture/blob/main/docker-compose.yml, https://github.com/MxSalata/aperture/blob/main/docker-compose.build.yml |
| IPM | zpm "install iris-aperture" (https://github.com/MxSalata/aperture/blob/main/module.xml) |
| Online demo | https://mxsalata.github.io/aperture/ |
Two ideas with the “Community Opportunity” status on the InterSystems Ideas Portal are implemented here; each is one screen away in the demo.
| Idea | What it asks | Where Aperture does it |
|---|---|---|
| DPI-I-966 Option to show older message.log in IRIS SMP | “It would be nice that we could choose in SMP portal to display any messages.old_* file”: today it takes a remote desktop session to the server |
Logs → Messages log. The package’s log reader (/api/aperture, Embedded Python) catalogues messages.log, alerts.log, SystemMonitor.log and every rotation IRIS or an administrator leaves beside them (messages.old_20260412_1, messages.log.1, messages_20260412.log), newest first. The File selector lists them under their kind, and a rotation reads exactly like the current file: windows of whole lines, newest first, older on request, the same severity filter. Verified on IRIS Community 2026.2 in CI (verify-iris). |
| DPI-I-813 Make REST API Debugger for VSCode Recognise Open API Spec | Requests should come from the OpenAPI description instead of being typed field by field, and be importable into Postman. The idea is written for the VS Code ObjectScript extension’s REST debugger | Done in the portal, and handed to VS Code and Postman as files. Every REST application’s routes (REST services → Export) and every SysAdmin API operation (API Explorer → Export, and “Request for curl, VS Code and Postman” under each operation) are generated as ready requests: method, path, path and query parameters with their types, examples and required flags, an example body from the schema, the privileges needed. Saved as a Postman collection (format v2.1, one folder per group, Basic auth from variables), as a .http file the VS Code REST Client extension and JetBrains IDEs run, or copied as curl. The password is never written into a file, and secret values in a body are redacted. |
verify-iris job builds the imagezpm "load" of its https://github.com/MxSalata/aperture/blob/main/module.xml, the Embedded Python installer), starts ithttps://github.com/MxSalata/aperture/blob/main/scripts/live-check.mjs: JWT sign-in and refresh, /info, list202 round trip, the portal served at /aperture/index.html, thenpm run verify:live runs the same check against your instance^ERRORS and SQL| Area | Screens | Notable |
|---|---|---|
| Dashboard | live stats, sparklines, your choice of charts (global refs/s, disk I/O, message throughput and queued messages per namespace from the interoperability metrics, cache efficiency, logical requests, routine refs, processes and sessions, license use), host gauges, health, alerts, upcoming tasks, busy processes, resource seizes | polls /v2/monitor/* every 3 s, keeps history while you navigate |
| Job Center | every 202 Accepted response, with console output, progress, pause / resume / cancel |
fed automatically by the API client (Location header or body GUID); toasts on completion |
| Health check | sixteen read-only checks run with your own access (disks, databases, journal, backup, UnknownUser, auditing, open services and applications, %All holders, certificates, tasks, monitor, alerts, licence, severe log entries), findings by severity with what they mean, what to do, the evidence and a link to the screen that fixes it; what the account may not read is listed as not checked; Markdown and JSON export; a dashboard card | src/features/health/checks.ts (pure checks, one test each), runner.ts |
| Activity | every change this tab sent and what the server answered, exportable as JSON; a security write can be matched to the %System/%Security/* audit record that proves it |
recorded by the client middleware; audit lookup runs as an async task around the request time |
| Host monitor | CPU, memory, disk, licence and alerts.log from the native /api/monitor service |
OpenMetrics parsed in the browser; degrades to “unavailable” honestly |
| Databases | configuration + local file view, free disk space per disk and per database, metrics (async), mount/dismount, compact, defragment, integrity check, truncate, expand, volumes, create, delete | dangerous actions require typing the name |
| Namespaces | create, delete, enable interoperability, copy mappings, global/package/routine mappings | |
| Processes | live list, detail with variables and roles, suspend/resume/terminate, broadcast | |
| Locks, Devices, Journals, Tasks, Web sessions, License | lock removal with transaction check, devices with the telnet and default-device settings, journal files/records/settings/switching, task schedules + history + task manager (state re-read after every change), session ending, license key/usage/servers | |
| Logs | a hub over every log of the instance, and a Messages log screen: messages.log, alerts.log, SystemMonitor.log and their rotations read in bounded windows through the package’s /api/aperture reader, newest first, older on request, severity filter, the raw line of every entry; “Similar” on any entry lists the entries worded like it across messages.log and its rotations, with how often and when, through IRIS Vector Search (an HNSW index over hashed words, matched by wording, not by meaning) |
https://github.com/MxSalata/aperture/blob/main/ipm/cls/Aperture/API.cls, Logs.cls, LogIndex.cls (Embedded Python), src/lib/messagesLog.ts, logVectors.ts |
| Security | users, roles, resources, services, web applications (JWT, CORS), audit events + log + purge, TLS/SSL configs + test, X.509 credentials with certificate expiry, wallet collections and write-only secrets, OAuth 2.0 in its three roles, SQL privileges | secrets are redacted at the render boundary |
| Permission safety | a change to a user or a role that would leave no enabled account holding %All or %Admin_Secure:U is refused; every such change lists, per account it reaches, the privileges lost and gained, counting granted roles and public permissions | features/security/adminGuard.ts, impact.ts |
| REST services | every REST web application of the instance, the spec-first classes no application serves, and the routes each one declares with what each takes, from /api/mgmnt (outside the SysAdmin API); the routes export as a Postman collection or a .http file, any route copies as curl |
a JWT session gives the password once, kept in the tab’s memory only |
| API Explorer | every one of the 273 operations rendered from the OpenAPI document: parameters, request body form or JSON, privileges, documented responses, response as table / fields / JSON; each operation also as curl, a .http block or a Postman item with the values typed, and the whole API or a group as a file |
reachable from the command palette |
| Everywhere | ⌘K command palette, a navigation menu you can rearrange (hold an entry and drag it, or Alt+arrows), auto-refresh beside every Refresh button (off, 5, 15, 30 or 60 s, remembered per screen), privilege badges, raw JSON of every response with passwords, secrets, tokens and private keys redacted (and a count of what was hidden), responsive layout, multiple saved connections, escalation-role login, LIVE / OFFLINE / DEMO indicator, read-only tabs that send nothing that changes the instance | src/lib/redact.ts |
| Tables | filter, sort and page live in the URL (share a link to exactly what you see; Back restores it), column choices and page size are remembered per table, every table exports its filtered rows as CSV, “no rows” and “nothing matches your filter” are different messages | stateKey / exportName on DataTable |
| Instances | each saved connection has a colour (a bar under the header, so production never looks like staging) and an optional time zone, browser-tab titles carry the instance name and its LIVE flag, and an account without %Admin_Operate lands on a screen it can use |
https://github.com/MxSalata/aperture/blob/main/docs/ARCHITECTURE.md §2.10 for the time policy |
| Appearance | light, pastel (soft lavender, mint, peach, butter, sky and rose, with a periwinkle accent), dark or system theme plus an independent contrast axis (system / normal / high), applied before the first paint; live regions, keyboard-reachable tooltips, reduced-motion and forced-colors support; colour tokens and chart palettes tested for WCAG 2 AA contrast | Appearance menu; axe-core audits all five modes in CI |
| Change review | every edit form shows old → new per field, re-reads the object to detect concurrent edits, and only then applies; where IRIS merges a PUT, only the changed fields are sent | reviewChanges() in src/components/ReviewChanges.tsx |
Things noticed while implementing the whole specification (reported for the API team). The specification is
vendored at commit f764aea427e5c0b1dd08a4c18a0457e0ff7b3b34 of
intersystems-community/sysadmin-api-specification.
LocalDatabaseList is declared as an object, but GET /v2/database-dirs returns an array. Aperture accepts both.GET /info as returning the Info object without the standard {status, console, result} envelope; IRIS 2026.2 (Build 221U) wraps it like every other endpoint. Aperture accepts both.POST /v2/database-dir/integrity-check takes its targets in the body (Databases[]) while its siblings (compact, defragment, …) use the dir query parameter.POST /v2/journal/switch-dir documents no body, so the target directory can only be the configured alternate directory.Location header points at /v1/async-result?id=… (on 2026.2 as in the spec example) while the documented endpoint is /v2/async-result; the id works on both. Aperture only relies on the id query parameter, and falls back to the GUID in the body when the header is not exposed (2026.2 sends no GUID in the body).status.errors (objects with a code) instead of the documented status.Errors strings, and IRIS 2026.2 accepts ServerDefinition where the spec names the OAuth client field OAuth2ServerDefinition (both reported in the IRIS Workbench verification record). Aperture normalizes the envelopes and adapts the field; see src/lib/quirks.ts.Accept-Language; a client that sends none gets the messages in Arabic (خطأ #420: Namespace … does not exist, observed in CI). Browsers always send the header, and Aperture shows the numeric code and id next to the text, which are stable.PublicPermission "" or null on creation and on edit with a 400 whose error list and summary are both empty, and a creation without the field with a 400 (ERROR #40301, required), although the spec’s “a string consisting only of ‘R’, ‘W’, and ‘U’” includes the empty string. Public access can only be taken away in the Management Portal; Aperture’s form says so instead of sending a request IRIS refuses without a reason. First reported for creation in the iris-fieldwork verification record, confirmed for both on IRIS for Health 2026.2 (writes.json); quirk resource-create-empty-public.GET /v2/security/services answers Enabled as a boolean and sends no EnabledBoolean; the spec declares Enabled: string plus EnabledBoolean: boolean. A client written to the spec shows every service as disabled (Aperture did, until checked against IRIS for Health 2026.2).GET /v2/processes spells the executable field EXEname; the spec declares EXEName, so a column bound to the spec stays empty.SystemUsage.BusyProcesses of GET /v2/monitor/dashboard/main always has ten rows { Process, Commands }, padded with { Process: "", Commands: 0 }; the spec declares Process as an integer and says nothing about the padding.POST /v2/journal/file/records returns half the maxRows it is given (200 → 100 records, the default 1000 → 500); the records are the first ones, contiguous.GET /v2/task/upcoming stops at 100 runs when maxRows is not given; the spec documents 1000 as the default for every list.GET /v2/databases and GET /v2/database-dirs answer 403, with an empty error list, to an account holding %Operator, although the spec allows %Admin_Operate:U; GET /v2/database-dir (one directory) is readable with it. As %Operator, GET /v2/security/ldap/configurations answers 500 (<INVALID OREF> in %Api.Admin.Util.ClassQuery) instead of 403.GET /v2/async-result or the /v1 path of the Location header) logs a severity-2 alert, ERROR #7846: WQM attach passed invalid token, from TryToKillQueue^%Api.Admin.Util.AsyncTask. The answer is unchanged, but each re-read lands in messages.log and /api/monitor/alerts and sets iris_system_state to 1 (Warning): a polling client that reads a finished task twice makes the instance look degraded to every monitor.POST /refresh rotates both, and the previous access token stops working at once; presenting a refresh token that was already used answers 401 and revokes the whole session; POST /logout needs the access token in Authorization (the refresh token in the body alone gets 401) and revokes both tokens with or without a body.ERROR #5002: ObjectScript error: <INVALID OREF>…); a client rendering them as text must unescape them.GET /v2/security/sql-privileges names the object and the action Object and Action; the spec (SQLPrivilegeList) says Name and Privilege. A client written to the spec shows empty columns and revokes the wrong privilege.POST /v2/security/oauth2/revoke does not exist: IRIS 2026.2 serves the operation at POST /v2/security/oauth2/server/revoke (the documented path answers 404 to every method, the other 405 with Allow: POST to a GET). The routes %Api.Admin declares, as /api/mgmnt lists them, also have HEAD on the three SQL privilege paths, which the spec does not document. The Explorer sends the revoke to its real path./api/admin, POST /login does not answer 404: IRIS asks for a password before the request reaches the API, so it is a bodiless 401 with WWW-Authenticate: Basic, whatever the body carries. A wrong password with JWT on is the same bodiless 401 with WWW-Authenticate: Bearer. A client that falls back to Basic only on 404 reports a wrong password to every user of such an instance (Aperture did); the header is the only difference, and proxies that hide it to keep the browser’s login dialog away must pass it on. Observed on IRIS Community 2026.2 Build 221U, on /api/mgmnt, whose JWT authentication is off by default, and on the /api/admin of IRIS for Health 2026.2 switched off for the test and on again (jwt-off.json): the portal signed in with Basic there, through the dev server and served by IRIS itself, with no browser login dialog.| File | Purpose |
|---|---|
| https://github.com/MxSalata/aperture/blob/main/docs/ARCHITECTURE.md | how the layers fit: typed client, auth, async jobs, privileges, mock, deployment |
| https://github.com/MxSalata/aperture/blob/main/docs/COVERAGE.md | all 273 operations: the screen that calls each, what a real IRIS for Health instance answered (139 verified), and what the demo answers |
| https://github.com/MxSalata/aperture/blob/main/docs/VERIFICATION.md | the conformance check against a real instance, and how to run it against yours |
| https://github.com/MxSalata/aperture/blob/main/docs/verification/ | recorded answers of real IRIS 2026.2 instances, one folder per run, the evidence for the spec findings below |
| https://github.com/MxSalata/aperture/blob/main/CHANGELOG.md | what changed, and why |
React 19 · TypeScript 5.9 · Vite 7 · Mantine 8 (+ charts, spotlight, notifications, modals) · TanStack Query 5 · TanStack Table 8 · React Router 7 · zustand · openapi-typescript / openapi-fetch · Mock Service Worker 2 · Vitest 4 · Playwright · nginx · IPM · ObjectScript (%CSP.REST, %Persistent) · Embedded Python (installer, log reader, wording index) · IRIS Vector Search (%Library.Vector, %SQL.Index.HNSW, VECTOR_COSINE)
MIT, see https://github.com/MxSalata/aperture/blob/main/LICENSE.