Aartiq
Aartiq™
Download
Security Model

Defense-in-Depth Security

Aartiq uses a defense-in-depth model with six independent security layers: visual sandbox, syntactic firewall, human-in-the-loop authorization, directory allowlist, OS-level sandboxing, and capability-scoped execution. Source implementations: src/lib/Security.ts, src/lib/SecurityValidator.js, src/core/command-validator.js, src/core/directory-allowlist.js, src/core/sandbox-executor.js

Philosophy

We did not set out to build a fortress. We set out to build a browser that can act on your behalf without becoming a liability — so that when the model is wrong, the damage stops at a boundary it cannot cross. Every layer below is a deliberate “no”: no raw page HTML in the model's context, no unverified command, no unsandboxed fallback, no silent yes. Security here is not a toggle you flip; it is the shape of the thing. The claims on this page are stated plainly, with their limits, because a security claim you cannot disprove is not a claim — it is a wish.

6

Security Layers

5

Enforcement Layers Beyond The Firewall

600K

PBKDF2 Key-Derivation Iterations

The regex blocklist in SecurityValidator.js is documented as a fast first-pass reject layer only — not the primary defense. Primary enforcement continues through the remaining layers: the risk-tiered permission store (checkShellPermission), the capability controller's ticket-based approval (capability-controller.js), and the fail-closed OS sandbox (sandbox-executor.js). The six-layer model cited below reflects this defense-in-depth design.

Architecture

The Six Layers

1

Visual Sandbox

The AI perceives web pages through screenshots + OCR and a sanitized secure-DOM extractor rather than raw, unprocessed HTML. This significantly reduces DOM-based manipulation attacks, but it is a mitigation, not an absolute guarantee.

How It Works

  • Primary input is the rendered page: Electron webContents.capturePage() screenshots (src/main/handlers/browser-handlers.js) and Tesseract.js OCR (src/lib/tesseract-service.js). The AI never runs inside the page's JavaScript realm.
  • SecureDOMReader (src/components/ai/SecureDOMReader.ts) provides a text fallback path. It blocks script/style/iframe/object/embed/form/input/button tags and nav/footer/header/modal/overlay/ads classes before text extraction.
  • PII redaction: emails, phone numbers, card numbers, bearer tokens, session IDs, and password/api-key assignments are replaced with [REDACTED] placeholders before content reaches the model.
  • SecureDOMParser (src/lib/Security.ts) runs the extracted content against shell-primitive, encoding, and injection pattern groups, decodes base64/hex payloads, and rewrites dangerous matches to [BLOCKED: LAYER].
  • AI Fortress masks API keys and secrets before content reaches the LLM (src/lib/Security.ts, src/components/AIChatSidebar.tsx).
  • The AI context is explicitly built as read-only: the model cannot modify the DOM; interaction is limited to approved click/fill commands (FIND_AND_CLICK / CLICK_ELEMENT).
  • Source files: src/lib/Security.ts, src/lib/html-sanitizer.js, src/components/ai/SecureDOMReader.ts

Benefits

  • Significantly reduces prompt-injection via DOM manipulation — hidden, scripted, or style-obfuscated content is stripped before it reaches the model
  • Page JavaScript cannot directly invoke the AI's execution layer (Electron context isolation + no DOM-write access); scripts are stripped from AI-visible content
  • Hidden elements and blocked tags/classes never appear in AI-visible content
  • Malicious scripts, event handlers (on*=), javascript:, data:, vbscript:, iframe/embed/object are removed from the AI's reading path
2

Syntactic Firewall

Every command is analyzed for dangerous patterns before execution.

How It Works

  • Commands are scanned for destructive shell primitives and blocked commands (rm, sudo, su, passwd, chgrp, dd if=, mkfs, fork-bomb, command substitution)
  • Encoded payloads and obfuscation (hex, base64, HTML entities) are decoded via extractBase64Strings and re-checked against injection patterns
  • Jailbreak patterns ('ignore all previous instructions', etc.) are blocked before content reaches the model
  • Network-triggering commands (curl, wget) are flagged, and the OS sandbox denies network by default
  • This layer is explicitly documented as a fast first-pass reject — not sufficient on its own (SecurityValidator.js header)
  • Source files: src/lib/SecurityValidator.js, src/lib/Security.ts, src/core/command-validator.js

Benefits

  • Stops known attack patterns at the gate
  • Prevents accidental destructive commands
  • Provides logging for security audits
  • Custom rules can be added by administrators
Blocked Patterns
rm -rf /Recursive delete of root
sudoBlocked command (privilege escalation)
dd if=Direct disk write
:(){ :|:& };:Fork bomb
$( ... )Command substitution
\x.. hex / chmod 777Encoded payload / permissive mode
curl / wgetNetwork download (flagged; sandbox denies net)
Monitored Patterns
rm File deletion (requires approval)
chmod / chownPermission change (requires approval)
kill / shutdown / mountProcess/system change (requires approval)
3

Human-in-the-Loop

Critical actions require explicit human approval before execution.

How It Works

  • AI generates a command; the parser assigns a risk field (default medium).
  • checkShellPermission() classifies low/medium/high/critical and checks the PermissionStore allowlist; with no store it denies (fail-closed) — src/core/command-validator.js:77-133.
  • Low risk: auto-runs only if autoApproveLowRisk is on (default off); otherwise a lightweight approval.
  • High risk (shell/power): the desktop generates a QR encoding aartiq://approve?id=<token>&pin=<6-digit> and waits for the paired mobile to return the PIN — src/main/handlers/sync-handlers.js:40-48, src/main/handlers/utils.js:377-387.
  • Alternatively, high risk uses an OS-native dialog: macOS 'Approve with Touch ID', Windows PowerShell, Linux bash — src/main/handlers/native-approval-manager.js:22-98. Biometric is gated by requireBiometricPerSession / requireBiometricEveryTime.
  • The renderer only enables Approve when both mobileApproved and pinVerified are true (or the biometric dialog succeeds) — src/components/ai/ClickPermissionModal.tsx:247-302.
  • Critical risk is denied at the gate; anything that does proceed goes through capability-controller single-use tickets — src/core/capability-controller.js:29-100, src/core/approval-ticket-manager.js:139-278, src/lib/approval-gate.js:53-147.
  • Command only executes after explicit approval; timeouts and missing renderers resolve to deny — src/core/shell-permission-bridge.js:42-71.
  • Source files: src/core/command-validator.js, src/lib/permission-store.js, src/main/handlers/sync-handlers.js, src/main/handlers/utils.js, src/main/handlers/native-approval-manager.js, src/components/ai/ClickPermissionModal.tsx, src/core/capability-controller.js

Benefits

  • No automated execution of destructive commands
  • QR approval ensures physical presence
  • Mobile app confirms identity
  • User approval required for execution

Approval Tiers

Low Risk
Auto / Shift+Tab

Read-only actions, navigation, volume changes. Auto-run only if autoApproveLowRisk is enabled (default false); otherwise a quick approval.

Taking screenshots

Navigating to URLs

Adjusting volume

Medium Risk
Approval dialog

Actions that modify browser state or open apps. Shown as Allow Once / Always Allow / Deny (shell-permission-bridge.js); persist via the permission store.

Filling forms

Clicking buttons

Opening applications

High Risk
QR + PIN or Touch ID

Shell commands and system changes. Either a QR+PIN approval from the paired mobile app, or an OS-native biometric prompt (Touch ID / Windows Hello). The QR carries a single-use token + 6-digit PIN.

Shell command execution

External app automation

File modifications

Critical Risk
Never auto-approved

Always denied at checkShellPermission (command-validator.js:87). Routed through the capability controller's ticket-based flow; tickets are single-use, input-hash-bound, 5-minute TTL.

Destructive / irreversible operations

Privilege escalation

4

Directory Allowlist

AI file access is restricted to explicitly approved directories with fine-grained read/write permissions.

How It Works

  • Each directory in the allowlist specifies an access level (Read Only or Read & Write) and recursive flag (src/lib/permission-store.js)
  • Path canonicalization resolves symlinks via fs.realpathSync before checking against the allowlist — the resolved path is checked, never the user-supplied string (src/core/directory-allowlist.js)
  • Just-in-time permission prompts request approval before accessing new directories
  • Batched multi-directory approval allows granting access to multiple paths at once
  • File management operations (move, copy, open, print) are routed around the shell sandbox
  • Read/write separation: a read grant must never allow deleting/overwriting — enforced in isPathAllowed() for both read and write operations
  • Source files: src/core/directory-allowlist.js, src/lib/permission-store.js, src/main/handlers/permission-handlers.js

Benefits

  • Scopes AI file access to an explicit allowlist — any path outside it is denied with a structured reason
  • Note: the legacy default allowlist includes the user's home, Desktop, Documents, and Downloads (read-write). Remove or downgrade these in Settings for a stricter posture; the newer directory-allowlist.js default ships with only the app data directory + temp
  • Symlink traversal attacks are blocked via realpath resolution
  • Read-only entries never receive write access — enforced in the sandbox profile (macOS/Linux) and by isPathAllowed() on all platforms
  • Audit trail of all directory access grants with timestamps (comet-audit.jsonl)
5

OS-Level Sandboxing

Shell commands execute inside platform-specific OS sandboxes that enforce process, filesystem, and network boundaries. Execution is FAIL-CLOSED: if the sandbox cannot be built and verified, the command is never run — there is no automatic fallback to unsandboxed execution.

How It Works

  • macOS: Seatbelt (sandbox-exec) with a closed-by-default profile — (deny file-read*) and (deny file-write*) then re-allow only system paths + allowlisted directories, (deny network*), and (deny process-exec*) with allowlisted exec paths
  • The Seatbelt profile is written to a temp file and validated with a pre-flight `sandbox-exec -f <profile> /usr/bin/true` run; if the profile fails to compile, the command is rejected (SANDBOX_POLICY_INVALID)
  • Linux: bubblewrap (bwrap) with unshared pid/net/ipc/uts namespaces, read-only system mounts (/usr, /bin, /sbin, /lib, /lib64, /etc), private /tmp, and --unshare-net
  • bubblewrap gets an extra capability pre-flight: `--version` succeeds even when user namespaces are disabled, so we run a real `--unshare-pid/net/ipc/uts /bin/true` probe and fail closed if the namespaces we require cannot be created (common in locked-down containers and some CI runners)
  • Windows: Job Object containment (src/core/win-job-runner.ps1) — the target is created SUSPENDED, assigned to a Job Object, verified via IsProcessInJob, then resumed; limits (KILL_ON_JOB_CLOSE, active-process cap, job memory, die-on-unhandled-exception) are applied and verified before the target runs a single instruction
  • Windows explicitly does NOT provide OS-level filesystem or network isolation in this release — the directory allowlist is enforced at the application layer (isPathAllowed), and requesting a per-process network policy fails closed (SANDBOX_UNAVAILABLE)
  • All platforms: environment is sanitized — only allowlisted variables (PATH, HOME, USER, LANG, LC_ALL, TMPDIR, SHELL, TERM, etc.) pass through; API keys and tokens never reach the sandboxed process (buildSafeEnv)
  • Every result carries an explicit `isolation` object ({ filesystem, network, process }) so callers cannot mistake process containment for filesystem/network isolation: macOS/Linux report all true, Windows reports {false, false, true}, and any setup failure reports all false. No single boolean 'sandboxed' is trusted on its own
  • Network inside the sandbox is denied by default on macOS (deny network*) and Linux (--unshare-net). Per-domain network allowlisting is NOT supported on any platform — requesting it fails closed. Windows cannot enforce per-process network policy in this release
  • Source files: src/core/sandbox-executor.js, src/core/win-job-runner.ps1, src/core/directory-allowlist.js

Benefits

  • Defense in depth: even if the regex blocklist is bypassed, the OS sandbox still confines what the command can read, write, execute, and reach on the network
  • On macOS/Linux the sandbox physically prevents writes outside the workspace + allowlisted write directories; on Windows this is enforced at the application layer by isPathAllowed()
  • Credential leakage via ambient environment variables is prevented by the env allowlist
  • Network exfiltration is blocked by default-deny networking inside the sandbox (macOS/Linux), not by firewall rules
What this does NOT guarantee
  • On Windows, the Job Object confines processes only. It does NOT isolate the filesystem or network at the OS layer. The directory allowlist and network denial are enforced by application code (isPathAllowed), not by the kernel — a bug in that code, or a path you explicitly allowlist, sits outside the Job Object's reach.
  • A sandbox confines what a command can do. It does not make a malicious command safe, and it does not decide what the AI asks for. Human approval is a social control, not a cryptographic one; a coerced or careless approval still executes.
  • Seatbelt and bubblewrap constrain the process, not the data it is handed. If you allowlist a directory that contains secrets, the sandboxed command can read them. Allowlists are trust boundaries you draw — only as good as where you draw them.
  • These guarantees apply to code executed through executeSandboxed(). The Electron main process, the renderer, native modules, and helper apps are NOT inside the sandbox. Sandboxing reduces blast radius; it is not a substitute for least-privilege OS accounts, patched dependencies, or simply not running untrusted code.
  • Fail-closed means we refuse to run rather than run uncontained. It does not mean every malicious input is harmless — a command that is allowed by policy and approved by a human runs, inside the sandbox, with whatever access the policy grants.
6

Capability-Scoped Execution

Actions must be explicitly registered with a named handler and approval tier. Unregistered actions are rejected.

How It Works

  • Each allowed action is registered with the CapabilityController
  • Approval tiers: never (auto-approved), first-time-per-session, always (explicit confirm)
  • Ticket-based authorization ensures single-use approval for high-risk actions
  • Unregistered actions don't exist as callable surfaces — prompt injection cannot invoke them
  • Source files: src/core/capability-controller.js, src/core/command-validator.js

Benefits

  • Removes dangerous primitives from the attack surface entirely
  • Prompt injection cannot invoke an action through the capability interface unless that action is registered and authorized
  • Ticket system prevents replay attacks on approved actions
  • Granular control over what the AI can and cannot do

Threat Model

Threat Scenarios

See how each security layer protects against common attack vectors.

Prompt Injection via Hidden Text

A malicious webpage hides prompt injection instructions in invisible text

Defense

Visual Sandbox prevents the AI from seeing hidden DOM elements. OCR only captures visible, rendered text.

Visual Sandbox

Malicious JavaScript Redirect

A webpage uses JavaScript to redirect the AI to a phishing site

Defense

The AI only sees screenshots of the actual rendered page. JavaScript execution is blocked from the AI's perspective.

Visual Sandbox

Social Engineering via Commands

An attacker tricks the AI into running 'rm -rf /'

Defense

The Syntactic Firewall blocks execution of dangerous shell patterns regardless of how the command is phrased.

Syntactic Firewall

Context Injection via Context Switching

A webpage contains instructions that attempt to override AI behavior

Defense

All user-provided content is filtered for injection patterns before reaching the AI context.

Syntactic Firewall

Unauthorized Shell Execution

AI executes a destructive shell command

Defense

Human-in-the-Loop requires explicit approval for all shell commands. High-risk commands require QR approval.

HITL

Remote Code Execution

AI is tricked into downloading and running malicious code

Defense

Shell commands requiring downloads are blocked by default. User approval ensures no unauthorized code execution.

HITL + Firewall

Symlink Traversal Attack

Attacker creates a symlink in an allowed directory pointing to /etc/passwd or other sensitive files

Defense

Path canonicalization resolves all symlinks via fs.realpath() before checking against the directory allowlist. The resolved path is checked, not the user-supplied path.

Directory Allowlist

Credential Leakage via Environment Variables

AI executes a command that inherits the parent process's environment with API keys and tokens

Defense

OS-level sandboxing strips all ambient environment variables. Only explicitly allowlisted variables (PATH, HOME, USER, LANG, LC_ALL, TMPDIR, SHELL, TERM, etc.) are passed to child processes; on Windows only non-credential system variables (SystemRoot, TEMP, USERPROFILE, etc.) pass through.

OS-Level Sandboxing

Network Exfiltration via Shell

AI is tricked into executing curl to upload sensitive data to an attacker's server

Defense

The sandbox denies network by default: macOS Seatbelt emits (deny network*) and Linux bubblewrap runs with --unshare-net. curl/wget downloads are additionally flagged by the command validator, and all shell execution requires human approval. Per-domain allowlisting is not supported; Windows cannot enforce per-process network policy in this release.

OS-Level Sandboxing

Unauthorized API Invocation

Prompt injection attempts to invoke an unregistered shell command or system action

Defense

Capability-Scoped Execution rejects unregistered actions entirely. If there's no registered run_shell_command action, prompt injection cannot invoke one through the capability interface unless that action is registered and authorized.

Capability-Scoped

Permissions

Permission Levels

Screen Reading

Required for AI to see page content

Required

Shell Execution

Required for terminal commands

High Risk

App Launching

Required for opening applications

Medium Risk

File System Access

Required for PDF generation and downloads

Required

Network Access

Required for web browsing and API calls

Required

Clipboard Access

Required for copy/paste functionality

Medium Risk

Directory Allowlist

Controls which directories AI can access

High Risk

OS-Level Sandboxing

Enforces filesystem and network boundaries

Required

Risk Assessment

Risk Levels

Every command is classified into one of four risk tiers before it reaches the permission gate. Higher tiers require stronger, more explicit approval.

Low Risk

Auto-approved

Read-only actions and navigation

Reading tabsNavigating to URLsPerforming searches

Approval

Auto-approved based on user preferences

Medium Risk

Per-action approval

Actions that modify state or affect the system

Shell commandsFile writesClipboard access

Approval

Per-action approval dialog

High Risk

Biometric confirmation

Destructive or irreversible operations

rm -rfdd if=Deleting files

Approval

Biometric confirmation (Touch ID / Windows Hello), falling back to OS password prompt; QR/PIN mobile approval for remote-origin commands

Critical Risk

Always explicit

Remote or privileged operations — never auto-approved

Remote shell commandsPrivilege escalation (sudo)System-level changes

Approval

Always requires explicit approval; never auto-approved. Routed through the capability controller's ticket-based flow.

High-Risk Actions

QR Code Approval

Mobile App Approval

High-risk actions require physical confirmation via the Aartiq mobile app.

1
Action Triggered

AI attempts high-risk command

2
QR Displayed

Desktop shows unique QR code

3
Scan & Verify

Mobile app scans QR

4
PIN Confirmation

Enter 6-digit verification code

5
Command Executed

Action proceeds after approval

Security Guarantees

  • QR codes are single-use only
  • Each QR code is cryptographically unique
  • PIN codes are generated per-session
  • Mobile must be paired via secure handshake
  • Failed attempts are logged with timestamps
  • All approvals are logged with timestamps

Remote Access

Remote Device Security

Commands originating from a paired mobile device receive the same validation as local commands — plus additional scrutiny because the origin is remote.

Elevated Risk for Remote Origin

WiFi Sync commands from paired mobile devices pass through the exact same validation and permission checks as local commands, with one difference: the remote origin elevates the risk tier by one level.

  • low → medium
  • medium → high
  • high → critical (never auto-approved)
  • Critical-risk commands are never auto-approved, regardless of origin

High-Risk Remote Actions

Power actions and shell commands from a remote device require QR/PIN approval before execution, matching the on-device high-risk flow.

  • Shutdown, restart, sleep, and lock require QR/PIN approval
  • Remote shell commands are validated by SecurityValidator, routed through the capability controller, and executed via execFile (no shell interpretation)
  • The MCP server binds to 127.0.0.1 only — no external network exposure
  • Pairing tokens expire after 10 minutes

Encryption

E2E Encryption

AES-256-GCM

All sensitive data at rest is encrypted using AES-256-GCM with authenticated encryption and PBKDF2 key derivation.

AlgorithmAES-256-GCM
Key DerivationPBKDF2-SHA256
Iterations600,000
IV Length12 bytes

Implementation

Authenticated encryption (GCM mode) — tampered ciphertext is rejected
Random salt + IV per encryption operation
PBKDF2 key derivation with 600,000 iterations (OWASP 2023+)
No silent fallback — encryption requires a passphrase or throws
encodeLocalOnly() as an explicit escape hatch for non-sensitive data

Use Cases

Sync credentialsEncrypted with user passphrase
API keysAES-256-GCM with derived key
Chat historyEnd-to-end encrypted sync
File transfersP2P encrypted relay
Vault passwordsField-level encryption with keychain
Legacy dataProactive migration to E2EE2: format

Source: src/lib/crypto-utils.ts

Vault Migration

Legacy vault entries are automatically detected and re-encrypted to the modern E2EE2 format — the older formats used weaker key derivation.

LCL:Legacy

Plaintext base64 — no encryption, no salt

E2EE:Legacy

PBKDF2 100K iterations, no salt

E2EE2:Current

PBKDF2 600K iterations, per-entry random salt + IV

  • Atomic vault writes — backup before migration, rollback on failure
  • Proactive migration re-encrypts LCL: and E2EE: entries to E2EE2: on demand
  • Migration requires biometric / native verification before re-encryption begins

API Key Storage

API Key Protection

Key Redaction

API keys are automatically masked in logs and console output

Bearer|token|api[_-]?key|secret[REDACTED]

Secure Storage

Keys stored in encrypted electron-store with OS keychain integration

Environment Isolation

Keys are never exposed to renderer process without explicit access

Auto-Masking

AI prompts are scrubbed for API keys before processing

sk-... (OpenAI)AIza... (Google)anthropic-... (Anthropic)gsk_... (Groq)

Source Files

src/lib/firebaseConfigStorage.ts, src/lib/shared-keychain.js

Token Generation

Method

crypto.getRandomValues()

Entropy

256-bit CSPRNG

Uses

Session tokens, pairing codes, QR verification

Capability Model

Capability-Scoped

Instead of trying to detect dangerous requests via regex, the system constrains what actions the AI can invoke at all — each with its own approval policy.

Register

Each allowed action is explicitly registered with a named handler and an approval tier. If an action isn't registered, it doesn't exist as a callable surface. The controller is wired into both the main process (main.js) and the command executor (command-executor.js).

registerAction({
name: "click_element",
requiresApproval: "first-time-per-session"
})

Execute

Execution is gated by the controller. Unregistered actions are rejected outright. Registered actions are allowed or queued for approval based on their tier.

neverApproved automatically (read-only)
first-time-per-sessionApproved once per session
alwaysRequires explicit confirmation each time

Why this matters

Regex-based threat detection can be bypassed — obfuscation, synonyms, and encoding all defeat pattern matching. A capability-scoped model doesn't try to detect danger in text; it removes the dangerous primitive from the attack surface entirely. If there's no registered run_shell_command action, prompt injection cannot invoke one through the capability interface unless that action is registered and authorized.

Verification

Security Test Coverage

Every layer above is backed by automated regression tests. The full suite runs in GitHub Actions CI (`.github/workflows/jest.yml`) on every push and pull request, and protects the security invariants from silent regressions.

525

Total Jest tests (514 passing, 11 platform-skipped, 0 failing)

84

OS-sandbox tests (fail-closed + adversarial)

21

Approval-ticket regression tests

6

Security layers under test

sandbox-security.test.js

macOS Seatbelt fail-closed + real OS-enforcement (read/write denied outside the allowlist, /tmp allowed, network bind denied, symlink-escape denied, child processes contained), plus Linux bubblewrap and Windows Job Object contract tests and command-tokenizer/env-sanitization checks.

windows-job-sandbox.test.js

JS contract (isolation flags, fail-closed network/allowlist policy) runs everywhere; the runtime matrix — suspended start, verified job assignment, grandchild containment, secret isolation, and KILL_ON_JOB_CLOSE — runs on Windows CI (windows-latest).

linux-bwrap-sandbox.test.js

JS contract (unshare flags, --bind vs --ro-bind, fail-closed when bwrap is missing or present-but-incapable of creating namespaces) runs everywhere; the runtime matrix (no read/write outside allowlist, private /tmp, network denied, symlink-escape denied) runs on Linux hosts where bwrap is installed.

approval-ticket-security.test.js

A dedicated regression suite for the ticket-based approval + capability-controller system. It locks in the fixes for the audit findings and fails if any invariant regresses.

Red 1 — redeemTicket verifies the params/context hash (tamper → 'tampered')
Red 2 — persistent grant cannot override an 'always' approval
Red 3 — 'first-time-per-session' never becomes a persistent grant
Red 4 — call-shape hashing agrees on context at register + verify
Red 5 / Orange 6 — missing params fail constraints; 'optional' allows absence
Orange 7/8 — registration gated to a ticket; pattern validates the action
Orange 9 — regex patterns length-limited against catastrophic backtracking
Orange 10 — unknown ticket IDs are rejected
Yellow 11 — tickets bound to capabilityVersion; replaceAction invalidates them
Yellow 12 — returned ticket params are defensive clones
Arch — approval produces a pure AuthorizationDecision; the executor consumes it and never reconstructs authorization
Arch — a v1 ticket is rejected (never executed) after the action is replaced with v2

Source: aartiq-browser/tests/approval-ticket-security.test.js