first commit
CI / Build & Test (push) Successful in 45s

This commit is contained in:
Raven Scott
2026-03-16 13:05:58 -04:00
commit 0c054707bd
15 changed files with 3488 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
ci:
name: Build & Test
runs-on: ssh
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Check toolchain versions
run: |
node --version
npm --version
bare --version 2>/dev/null || echo "bare not in PATH (ok)"
- name: Install root dependencies
run: npm install
- name: Install tunnel dependencies
run: npm install
working-directory: tunnel
- name: Lint — syntax check tunnel
run: node --check tunnel/index.mjs
- name: Lint — syntax check build scripts
run: node --check scripts/build-tunnel.js
- name: Lint — syntax check connection plugin
run: python3 -m py_compile ansible_collections/anisail/anisail/plugins/connection/holesail.py
- name: Build all platform binaries
run: node scripts/build-tunnel.js --all
- name: Package release zips
run: |
cd releases
for host in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64; do
if [ -f "$host/anisail-tunnel" ]; then
zip -j "anisail-tunnel-${host}.zip" "$host/anisail-tunnel"
echo "Created anisail-tunnel-${host}.zip"
elif [ -f "$host/anisail-tunnel.exe" ]; then
zip -j "anisail-tunnel-${host}.zip" "$host/anisail-tunnel.exe"
echo "Created anisail-tunnel-${host}.zip"
fi
done
cd ..
- name: Smoke test — run native binary for current platform
run: |
echo "=== releases/ layout ==="
find releases/ -type f | sort
echo ""
MACHINE=$(uname -m)
echo "uname -m: $MACHINE"
BIN=""
for candidate in \
"releases/${MACHINE}/anisail-tunnel" \
"releases/x86_64/anisail-tunnel" \
"releases/aarch64/anisail-tunnel" \
"releases/arm64/anisail-tunnel" \
"releases/darwin-arm64/anisail-tunnel" \
"releases/darwin-x64/anisail-tunnel" \
"releases/linux-arm64/anisail-tunnel" \
"releases/linux-x64/anisail-tunnel" \
"releases/anisail-tunnel"; do
if [ -f "$candidate" ]; then
BIN="$candidate"
break
fi
done
if [ -z "$BIN" ]; then
echo "No runnable binary found — skipping smoke test"
exit 0
fi
echo "Testing binary: $BIN"
chmod +x "$BIN"
OUTPUT=$(timeout 5 "$BIN" 2>&1 || true)
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q "key.*required"; then
echo "PASS: binary started and reported missing key"
else
echo "FAIL: expected 'key required' message"
exit 1
fi
echo "PASS: smoke test complete"
- name: Generate checksums
run: |
cd releases
find . -name "*.zip" | sort | xargs sha256sum > SHA256SUMS.txt 2>/dev/null || true
cat SHA256SUMS.txt 2>/dev/null || echo "(no zips)"
cd ..
- name: List artifacts
run: |
echo "=== All release artifacts ==="
find releases/ -type f | sort
echo ""
find releases/ -type f -exec ls -lh {} \; | awk '{print $5, $9}'
- name: Publish rolling release
if: github.event_name == 'push'
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
TAG="latest-main"
TITLE="Latest build (main @ ${SHORT_SHA})"
COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | sed '/^Made-with:/d' | sed '/^$/d' | head -1)
BODY="Automated build from main branch.\n\n**Commit:** ${{ github.sha }}\n**Message:** ${COMMIT_MSG}\n\nThis release is updated on every push to main and always contains the latest tunnel binaries."
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
AUTH="Authorization: token ${{ secrets.RELEASE_TOKEN }}"
REPO_URL=$(git remote get-url origin | sed 's|https://|https://x-token:${{ secrets.RELEASE_TOKEN }}@|')
git remote set-url origin "${REPO_URL}"
git config user.email "ci@anisail"
git config user.name "CI"
git tag -f "${TAG}" "${{ github.sha }}"
git push origin "refs/tags/${TAG}" --force
echo "Tag ${TAG} force-pushed to ${{ github.sha }}"
EXISTING=$(curl -s -H "$AUTH" "${API}/releases/tags/${TAG}")
RELEASE_ID=$(echo "$EXISTING" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const r=JSON.parse(d);console.log(r.id||'')}catch{console.log('')}})")
echo "Existing release ID: $RELEASE_ID"
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ] && [ "$RELEASE_ID" != "" ]; then
curl -s -X PATCH \
-H "$AUTH" -H "Content-Type: application/json" \
"${API}/releases/${RELEASE_ID}" \
-d "{
\"name\": \"${TITLE}\",
\"body\": \"${BODY}\",
\"prerelease\": true,
\"target_commitish\": \"${{ github.sha }}\"
}"
echo "Updated release ${RELEASE_ID}"
ASSETS=$(curl -s -H "$AUTH" "${API}/releases/${RELEASE_ID}/assets")
echo "$ASSETS" | node -e "
let d='';
process.stdin.on('data',c=>d+=c).on('end',()=>{
try {
const assets = JSON.parse(d);
if (Array.isArray(assets)) assets.forEach(a => console.log(a.id));
} catch(_) {}
})" | while read ASSET_ID; do
[ -z "$ASSET_ID" ] && continue
echo "Deleting asset $ASSET_ID..."
curl -s -X DELETE -H "$AUTH" "${API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
done
else
RELEASE_ID=$(curl -s -X POST \
-H "$AUTH" -H "Content-Type: application/json" \
"${API}/releases" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"${TITLE}\",
\"body\": \"${BODY}\",
\"prerelease\": true,
\"target_commitish\": \"${{ github.sha }}\"
}" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).id)}catch{console.log('')}})")
echo "Created release ${RELEASE_ID}"
fi
for FILE in releases/*.zip releases/SHA256SUMS.txt; do
[ -f "$FILE" ] || continue
NAME=$(basename "$FILE")
echo "Uploading $NAME..."
curl -s -X POST \
-H "$AUTH" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${NAME}" \
--data-binary "@${FILE}"
echo ""
done
echo "Done — release ${TAG} updated to ${{ github.sha }}"
+35
View File
@@ -0,0 +1,35 @@
# Dependencies
node_modules/
# Build output (tunnel binaries and zips; CI publishes these)
releases/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.venv/
venv/
env/
# OS
.DS_Store
Thumbs.db
# Logs and debug
*.log
npm-debug.log*
.npm
# Environment / secrets
.env
.env.local
.env.*.local
# Editor / IDE
.idea/
.vscode/
*.swp
*.swo
*~
+162
View File
@@ -0,0 +1,162 @@
# Anisail
Ansible connection plugin that uses [Holesail](https://github.com/holesail/holesail) P2P tunnels to reach hosts behind NAT, firewalls, or CGNAT — with no port forwarding or static IPs.
- **On the target**: Run `holesail --live 22` (or your SSH port); copy the `hs://...` URL.
- **In inventory**: Set `ansible_connection` and `ansible_holesail_key`.
- Ansible starts a local tunnel, connects SSH through it, and runs your playbooks as usual.
Requires the **anisail-tunnel** binary (Bare-built, shipped in `releases/` or built via `npm run build:tunnel`) and **paramiko** on the control node.
## Installation
### 1. Install the collection
From the repo root (or after building the collection artifact):
```bash
ansible-galaxy collection install ansible_collections/anisail/anisail/ --force
```
Or link for development:
```bash
export ANSIBLE_COLLECTIONS_PATHS="$PWD/ansible_collections"
# or symlink/copy ansible_collections/anisail into your collections path
```
### 2. Install the tunnel binary
Either use a prebuilt binary or build it yourself (requires [Node.js](https://nodejs.org/) and `npm`).
**Option A — Build from source (Bare binary)**
```bash
npm install
npm run build:tunnel
```
Output: `releases/<platform>/anisail-tunnel` (e.g. `releases/darwin-arm64/anisail-tunnel`). Either add that directory to `PATH` or set `ansible_holesail_tunnel_path` in inventory to the full path.
**Option B — Use collection releases**
If you ship the built binary inside the collection (e.g. under `ansible_collections/anisail/anisail/releases/<platform>/anisail-tunnel`), the plugin will find it automatically. Copy from repo `releases/` after building:
```bash
cp -r releases/* ansible_collections/anisail/anisail/releases/
```
**Option C — System PATH**
Install the binary somewhere on `PATH` and name it `anisail-tunnel`. The plugin will use it if `ansible_holesail_tunnel_path` is not set and no collection-relative binary is found.
### 3. Install Paramiko (required)
```bash
pip install paramiko
```
## Inventory
Use the `holesail` connection and set the Holesail key (and optionally port/timeout):
```yaml
# inventory.yml
all:
hosts:
my_device:
ansible_connection: anisail.anisail.holesail # or "holesail" if collection is default
ansible_holesail_key: "hs://s000..." # from "holesail --live 22" on the target
ansible_user: admin # SSH user (optional)
# ansible_port: 22 # remote port to tunnel to (default 22)
# ansible_holesail_tunnel_path: /path/to/anisail-tunnel
# ansible_holesail_ready_timeout: 30 # seconds (default 30)
```
If your playbook sets `collections: [anisail.anisail]`, you can use the short name:
```yaml
ansible_connection: holesail
ansible_holesail_key: "hs://s000..."
```
## Target setup
On each device you want to manage, run Holesail in server mode exposing the SSH port:
```bash
npm i holesail -g
holesail --live 22
```
Use the printed URL (e.g. `hs://s000...`) as `ansible_holesail_key` for that host.
## Playbook example
```yaml
---
- name: Run through Holesail tunnel
hosts: my_device
gather_facts: true
tasks:
- name: Ping
ping:
- name: Run a command
command: uname -a
register: out
- name: Print result
debug:
var: out.stdout
```
Run as usual:
```bash
ansible-playbook -i inventory.yml playbook.yml
```
## Connection variables
| Variable | Description | Default |
|----------|-------------|---------|
| `ansible_holesail_key` | Holesail URL from target (`hs://...`) | (required) |
| `ansible_port` | Remote port to tunnel to (e.g. SSH 22) | 22 |
| `ansible_holesail_tunnel_path` | Path to anisail-tunnel binary | auto (collection bin/releases or PATH) |
| `ansible_holesail_ready_timeout` | Seconds to wait for tunnel ready | 30 |
| `ansible_user` | SSH user | play context default |
| `ansible_private_key_file` / `ansible_ssh_private_key_file` | SSH key path | — |
| `ansible_password` / `ansible_ssh_pass` | SSH password | — |
## Building the tunnel binary
From the repo root:
```bash
npm install
npm run build:tunnel # current platform only
npm run build:tunnel:all # all platforms (darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64)
```
Output is under `releases/<platform>/anisail-tunnel` (or `anisail-tunnel.exe` on Windows). The tunnel is a statically built Bare binary; no Node.js is required at runtime.
## CI and releases
The repo includes a Gitea Actions workflow (`.gitea/workflows/ci.yml`) that:
- Runs on push and pull requests to `main`
- Installs dependencies, lint-checks the tunnel and connection plugin, and builds tunnel binaries for all platforms (`node scripts/build-tunnel.js --all`)
- Packages each binary as `anisail-tunnel-<platform>.zip` and publishes a rolling release on push
**Required secret:** `RELEASE_TOKEN` — a token with permission to push the `latest-main` tag and create/update releases and upload assets. Configure it in your Gitea repo **Settings → Secrets**.
The workflow uses the same pattern as [Holesail-Browser CI](https://git.ssh.surf/snxraven/holesail-browser/src/branch/main/.gitea/workflows/ci.yml): force-push tag `latest-main`, create or update a prerelease, and upload all zip artifacts plus `SHA256SUMS.txt`.
## License
MIT
## References
- [Holesail](https://github.com/holesail/holesail) — P2P tunnel (npm, CLI, API)
- [Ansible connection plugins](https://docs.ansible.com/ansible/devel/plugins/connection.html)
- [Developing network plugins](https://docs.ansible.com/projects/ansible/latest/network/dev_guide/developing_plugins_network.html#network-connection-plugins)
@@ -0,0 +1,23 @@
# anisail.anisail
Ansible collection that provides the **holesail** connection plugin for reaching hosts behind NAT/firewalls via [Holesail](https://github.com/holesail/holesail) P2P tunnels.
## Connection plugin: holesail
- **Connection type**: `anisail.anisail.holesail` (or `holesail` when the collection is in use)
- **Requires**: `ansible_holesail_key` (Holesail URL, e.g. `hs://s000...`), anisail-tunnel binary, and the `paramiko` Python library.
See the [project README](https://github.com/anisail/anisail) at the repo root for full installation, inventory, and playbook examples.
## Quick example
**Inventory:**
```yaml
my_host:
ansible_connection: anisail.anisail.holesail
ansible_holesail_key: "hs://s000..."
ansible_user: admin
```
**Target:** Run `holesail --live 22` and use the printed URL as `ansible_holesail_key`.
@@ -0,0 +1,20 @@
---
namespace: anisail
name: anisail
version: 1.0.0
readme: README.md
description: Ansible connection plugin for Holesail P2P tunnels — reach hosts behind NAT/firewalls with no port forwarding.
license: MIT
authors:
- Anisail
repository: https://github.com/anisail/anisail
documentation: https://github.com/anisail/anisail
keywords:
- ansible
- connection
- holesail
- tunnel
- p2p
- nat
dependencies: {}
@@ -0,0 +1,3 @@
---
# Runtime requirements for anisail.anisail collection
requires_ansible: '>=2.14'
@@ -0,0 +1 @@
# Connection plugins for anisail.anisail
@@ -0,0 +1,316 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
Ansible connection plugin that uses Holesail P2P tunnels to reach hosts behind NAT.
Spawns anisail-tunnel binary, reads local port from stdout, then connects SSH over that port.
"""
from __future__ import annotations
import json
import os
import subprocess
from ansible.errors import AnsibleConnectionFailure
from ansible.plugins.connection import ConnectionBase
try:
import paramiko
HAS_PARAMIKO = True
except ImportError:
HAS_PARAMIKO = False
DOCUMENTATION = """
author: Anisail
connection_type: holesail
short_description: Connect via Holesail P2P tunnel then SSH
description:
- Runs an anisail-tunnel binary to create a local TCP tunnel to the target (via hs:// key),
then connects SSH to 127.0.0.1:<local_port>. Use when the target is behind NAT/firewall/CGNAT.
- On the target, run C(holesail --live 22) (or your SSH port) and set ansible_holesail_key to the printed URL.
version_added: '1.0.0'
options:
key:
description: Holesail URL (e.g. hs://s000...) from the target running C(holesail --live <port>).
type: string
required: true
vars:
- name: ansible_holesail_key
port:
description: Remote port to tunnel to (e.g. 22 for SSH).
type: int
default: 22
vars:
- name: ansible_port
- name: ansible_ssh_port
tunnel_path:
description: Path to the anisail-tunnel binary. If not set, resolved from collection bin/ or PATH.
type: string
default: ''
vars:
- name: ansible_holesail_tunnel_path
ready_timeout:
description: Seconds to wait for tunnel to be ready.
type: int
default: 30
vars:
- name: ansible_holesail_ready_timeout
host:
description: Hostname/IP (unused; tunnel key defines the target).
type: string
remote_user:
description: User for SSH login.
type: string
vars:
- name: ansible_user
- name: ansible_ssh_user
private_key_file:
description: Path to SSH private key.
type: string
vars:
- name: ansible_private_key_file
- name: ansible_ssh_private_key_file
password:
description: SSH password (if not using key).
type: string
vars:
- name: ansible_password
- name: ansible_ssh_pass
"""
def _find_tunnel_binary(plugin_path_option, collection_root: str | None) -> str:
"""Resolve anisail-tunnel binary path."""
if plugin_path_option and os.path.isabs(plugin_path_option) and os.path.isfile(plugin_path_option):
return plugin_path_option
if plugin_path_option and os.path.isfile(plugin_path_option):
return os.path.abspath(plugin_path_option)
if collection_root:
for sub in ('bin', 'releases'):
for name in ('anisail-tunnel', 'anisail-tunnel.exe'):
candidate = os.path.join(collection_root, sub, name)
if os.path.isfile(candidate):
return candidate
import platform as plat
system = plat.system().lower()
machine = plat.machine().lower()
if system == 'darwin':
platform = 'darwin-arm64' if machine in ('arm64', 'aarch64') else 'darwin-x64'
elif system == 'linux':
platform = 'linux-arm64' if machine in ('arm64', 'aarch64') else 'linux-x64'
elif system == 'windows':
platform = 'win32-x64'
else:
platform = f"{system}-{machine}"
candidate = os.path.join(collection_root, 'releases', platform, 'anisail-tunnel')
if os.path.isfile(candidate):
return candidate
if platform.startswith('win'):
candidate = os.path.join(collection_root, 'releases', platform, 'anisail-tunnel.exe')
if os.path.isfile(candidate):
return candidate
import shutil
return shutil.which('anisail-tunnel') or ''
class Connection(ConnectionBase):
"""Ansible connection over Holesail tunnel + SSH (Paramiko)."""
transport = 'holesail'
has_pipelining = False
def __init__(self, play_context, new_stdin, *args, **kwargs):
super().__init__(play_context, new_stdin, *args, **kwargs)
self._tunnel_process: subprocess.Popen | None = None
self._local_port: int | None = None
self._paramiko_client: "paramiko.SSHClient | None" = None
def _connect(self) -> None:
if not HAS_PARAMIKO:
raise AnsibleConnectionFailure(
'The paramiko Python library is required for the holesail connection plugin. '
'Install it with: pip install paramiko'
)
key = self.get_option('key')
if not key:
raise AnsibleConnectionFailure(
'ansible_holesail_key is required for the holesail connection. '
'Set it in inventory (e.g. ansible_holesail_key: "hs://s000...").'
)
remote_port = self.get_option('port') or 22
tunnel_path_opt = self.get_option('tunnel_path') or ''
ready_timeout_sec = self.get_option('ready_timeout') or 30
timeout_ms = ready_timeout_sec * 1000
collection_root = None
try:
from ansible.utils.collection_loader import AnsibleCollectionConfig
for p in (getattr(AnsibleCollectionConfig, 'collection_paths', None) or []):
for base in (p if isinstance(p, (list, tuple)) else [p]):
candidate = os.path.join(base, 'anisail', 'anisail')
if os.path.isdir(candidate):
collection_root = candidate
break
if collection_root:
break
except Exception:
pass
if not collection_root:
try:
plugin_dir = os.path.dirname(os.path.abspath(__file__))
collection_root = os.path.normpath(os.path.join(plugin_dir, '..', '..'))
if not os.path.isdir(collection_root):
collection_root = None
except Exception:
pass
binary = _find_tunnel_binary(tunnel_path_opt, collection_root)
if not binary:
raise AnsibleConnectionFailure(
'anisail-tunnel binary not found. Set ansible_holesail_tunnel_path or install the binary '
'in the collection bin/ or releases/<platform>/ directory, or have anisail-tunnel in PATH.'
)
argv = [
binary,
'--key', key,
'--remote-port', str(remote_port),
'--timeout', str(timeout_ms),
]
try:
self._tunnel_process = subprocess.Popen(
argv,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except OSError as e:
raise AnsibleConnectionFailure(f'Failed to start tunnel process: {e}') from e
line = None
try:
with self._tunnel_process.stdout:
line = self._tunnel_process.stdout.readline()
except Exception as e:
self._terminate_tunnel()
raise AnsibleConnectionFailure(f'Failed to read tunnel port: {e}') from e
if not line:
stderr = b''
if self._tunnel_process.stderr:
try:
stderr = self._tunnel_process.stderr.read()
except Exception:
pass
self._terminate_tunnel()
raise AnsibleConnectionFailure(
'Tunnel did not output local port. '
f'Stderr: {(stderr.decode("utf-8", errors="replace") or "").strip() or "none"}'
)
try:
data = json.loads(line.decode('utf-8').strip())
except (json.JSONDecodeError, UnicodeDecodeError) as e:
self._terminate_tunnel()
raise AnsibleConnectionFailure(f'Invalid tunnel output (expected JSON with local_port): {e}') from e
local_port = data.get('local_port')
if local_port is None:
self._terminate_tunnel()
raise AnsibleConnectionFailure('Tunnel output missing local_port')
self._local_port = int(local_port)
username = self.get_option('remote_user') or self._play_context.remote_user or 'root'
pkey = None
key_file = self.get_option('private_key_file')
if key_file and os.path.isfile(key_file):
try:
pkey = paramiko.RSAKey.from_private_key_file(key_file)
except paramiko.ssh_exception.SSHException:
try:
pkey = paramiko.Ed25519Key.from_private_key_file(key_file)
except paramiko.ssh_exception.SSHException:
pass
password = self.get_option('password') or self._play_context.password or None
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(
'127.0.0.1',
port=self._local_port,
username=username,
pkey=pkey,
password=password,
timeout=30,
banner_timeout=30,
auth_timeout=30,
)
except Exception as e:
self._terminate_tunnel()
raise AnsibleConnectionFailure(f'SSH connection to tunnel port failed: {e}') from e
self._paramiko_client = client
def _terminate_tunnel(self) -> None:
if self._tunnel_process is None:
return
try:
self._tunnel_process.terminate()
try:
self._tunnel_process.wait(timeout=5)
except subprocess.TimeoutExpired:
self._tunnel_process.kill()
self._tunnel_process.wait()
except Exception:
pass
self._tunnel_process = None
def exec_command(self, cmd, in_data=None, sudoable=True):
if self._paramiko_client is None:
raise AnsibleConnectionFailure('Not connected')
try:
stdin, stdout, stderr = self._paramiko_client.exec_command(cmd)
if in_data:
stdin.write(in_data)
stdin.channel.shutdown_write()
rc = stdout.channel.recv_exit_status()
return rc, stdout.read(), stderr.read()
except Exception as e:
raise AnsibleConnectionFailure(f'exec_command failed: {e}') from e
def put_file(self, in_path, out_path):
if self._paramiko_client is None:
raise AnsibleConnectionFailure('Not connected')
try:
sftp = self._paramiko_client.open_sftp()
try:
sftp.put(in_path, out_path)
finally:
sftp.close()
except Exception as e:
raise AnsibleConnectionFailure(f'put_file failed: {e}') from e
def get_file(self, in_path, out_path):
if self._paramiko_client is None:
raise AnsibleConnectionFailure('Not connected')
try:
sftp = self._paramiko_client.open_sftp()
try:
sftp.get(in_path, out_path)
finally:
sftp.close()
except Exception as e:
raise AnsibleConnectionFailure(f'get_file failed: {e}') from e
def close(self):
if self._paramiko_client:
try:
self._paramiko_client.close()
except Exception:
pass
self._paramiko_client = None
self._terminate_tunnel()
super().close()
+619
View File
@@ -0,0 +1,619 @@
{
"name": "anisail",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "anisail",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"bare-build": "^0.4.3"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/bare-addon-resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
"integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-module-resolve": "^1.10.0",
"bare-semver": "^1.0.0"
},
"peerDependencies": {
"bare-url": "*"
},
"peerDependenciesMeta": {
"bare-url": {
"optional": true
}
}
},
"node_modules/bare-ansi-escapes": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
"integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-stream": "^2.6.5"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-apk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/bare-apk/-/bare-apk-0.1.2.tgz",
"integrity": "sha512-lu5E7u33snoi3bFqGfVM0HtPm4v6U83zOP14mo2YQGBKWjhGsZS128ToUNdKGoG51QhPs1ltgt2zsBhs0LPOcA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-env": "^3.0.0",
"bare-fs": "^4.5.2",
"bare-os": "^3.6.2",
"bare-path": "^3.0.0",
"bare-subprocess": "^5.2.1",
"require-asset": "^1.2.1"
}
},
"node_modules/bare-app-image": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/bare-app-image/-/bare-app-image-0.1.1.tgz",
"integrity": "sha512-FOmSjy+0bx6Qkztl+n7PHhrqxSnu4ccsokjHPwQUFnDNcAetYlxsLQHquq7B/vJeZiowBVJV+ulrTdwuVBfXpA==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"dependencies": {
"bare-fs": "^4.5.1",
"bare-path": "^3.0.0",
"bare-subprocess": "^5.1.5",
"require-asset": "^1.1.0"
}
},
"node_modules/bare-assert": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
"integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-inspect": "^3.1.2"
}
},
"node_modules/bare-buffer": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz",
"integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"bare": ">=1.20.0"
}
},
"node_modules/bare-build": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/bare-build/-/bare-build-0.4.5.tgz",
"integrity": "sha512-fGSNL7HfIEtIvwcbZc4TtC8XRxiXAqBoLAEBKPQhTLHxUudF54NUxQDBkdO+qqmUCWPXavjuCI0ZxJm/z/+zoQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-bundle-id": "^1.0.2",
"bare-fs": "^4.5.1",
"bare-lief": "^0.1.3",
"bare-link": "^3.0.0",
"bare-module-resolve": "^1.12.0",
"bare-module-traverse": "^2.0.0",
"bare-os": "^3.6.2",
"bare-pack": "^2.0.0",
"bare-path": "^3.0.0",
"bare-subprocess": "^5.1.5",
"bare-unpack": "^1.1.3",
"bare-url": "^2.3.2",
"paparam": "^1.8.0",
"require-asset": "^1.1.0"
},
"bin": {
"bare-build": "bin.js"
},
"optionalDependencies": {
"bare-apk": "^0.1.2",
"bare-app-image": "^0.1.0"
}
},
"node_modules/bare-bundle": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
"integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"bare-buffer": "*",
"bare-url": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-url": {
"optional": true
}
}
},
"node_modules/bare-bundle-id": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bare-bundle-id/-/bare-bundle-id-1.0.2.tgz",
"integrity": "sha512-RG/y1J/s6zWmsqUIDtclXh+xxMRTh1jo/10vFL58FKhe9UESchMNkwn0Cz10o5AdA/35WR/KUGoHhIGdyCjQrg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"sodium-native": "^5.0.9"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-bundle": "*"
}
},
"node_modules/bare-env": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
"integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-inspect": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
"integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-ansi-escapes": "^2.1.0",
"bare-type": "^1.0.0"
},
"engines": {
"bare": ">=1.18.0"
}
},
"node_modules/bare-lief": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/bare-lief/-/bare-lief-0.1.6.tgz",
"integrity": "sha512-eNqgZGoHVPGtbkLoDPVyKKEe9uTkXFqOvTevb1iDv3SXv/eU7kMgJoAukSdvFUg7QWNiqd2o/pZwaMy5n3cjNw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"require-addon": "^1.2.0"
}
},
"node_modules/bare-link": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/bare-link/-/bare-link-3.0.2.tgz",
"integrity": "sha512-dtG8B9PFXGMLRb2HJGTsq4jhLPMIq3e8z696f+VIU4eMbWnUOx2TxBvpId/u5yp4oKiapMvyINGEhqEsxtJH4A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-fs": "^4.0.0",
"bare-lief": "^0.2.0",
"bare-module-resolve": "^1.12.0",
"bare-os": "^3.2.0",
"bare-path": "^3.0.0",
"bare-subprocess": "^5.0.2",
"bare-url": "^2.0.9",
"paparam": "^1.5.0"
},
"bin": {
"bare-link": "bin.js"
}
},
"node_modules/bare-link/node_modules/bare-lief": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/bare-lief/-/bare-lief-0.2.0.tgz",
"integrity": "sha512-ya69yPs95HJxSpe5UaNYK2Vzko8DhYS+9cnAM2EhIlzY8J2wWcyMzjABmO4i6ToQwdpvywWgDEs1ud/r5pIjsA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-assert": "^1.2.0",
"require-addon": "^1.2.0"
}
},
"node_modules/bare-module-lexer": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
"integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"require-addon": "^1.0.2"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-module-resolve": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
"integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-semver": "^1.0.0"
},
"peerDependencies": {
"bare-url": "*"
},
"peerDependenciesMeta": {
"bare-url": {
"optional": true
}
}
},
"node_modules/bare-module-traverse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.0.1.tgz",
"integrity": "sha512-1au+Og5p97T9b6Y7xmHZ7KtpW8vEYtz2jC2whmm+YJp46EaHfk26j91MmQhufdkR/8sdK1Q5p+P9A/Y5GrJg7Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-addon-resolve": "^1.5.0",
"bare-module-lexer": "^1.4.0",
"bare-module-resolve": "^1.7.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-url": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-url": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
"integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-pack": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bare-pack/-/bare-pack-2.0.1.tgz",
"integrity": "sha512-zpItExb4Kue1vq97/ZiHNwSBwUGyv3uqhgi1GpT2S7qezkXmc3K/Wx6qkth/7ayS3i+suIn6EGE96EhJxKkCzA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-bundle": "^1.8.3",
"bare-bundle-id": "^1.0.0",
"bare-fs": "^4.2.1",
"bare-module-traverse": "~2.0.0",
"bare-path": "^3.0.0",
"bare-url": "*",
"paparam": "^1.5.0",
"promaphore": "^1.0.0"
},
"bin": {
"bare-pack": "bin.js"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-url": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-url": {
"optional": true
}
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-pipe": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
"integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.0.0",
"bare-stream": "^2.0.0"
},
"engines": {
"bare": ">=1.16.0"
}
},
"node_modules/bare-semver": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
"integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/bare-stream": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz",
"integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.21.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-subprocess": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
"integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-env": "^3.0.0",
"bare-events": "^2.5.4",
"bare-os": "^3.0.1",
"bare-pipe": "^4.0.0",
"bare-url": "^2.2.2"
},
"engines": {
"bare": ">=1.7.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
"integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"bare": ">=1.2.0"
}
},
"node_modules/bare-unpack": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/bare-unpack/-/bare-unpack-1.1.3.tgz",
"integrity": "sha512-b+OFTi74dsMaVQy9w90U6WMqhVBcx5hONJnFbOSfhTefG/9OuY6ZxU3IvybVa5/i3edzboa7rebaMG25K6/kAA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-bundle": "^1.8.3",
"bare-fs": "^4.2.3",
"bare-path": "^3.0.0",
"paparam": "^1.8.5",
"promaphore": "^1.0.0"
},
"bin": {
"bare-unpack": "bin.js"
}
},
"node_modules/bare-url": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"dev": true,
"license": "MIT"
},
"node_modules/paparam": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
"integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/promaphore": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/promaphore/-/promaphore-1.0.0.tgz",
"integrity": "sha512-Eg8401+KJddVvDULkpy8bR964GMX8xMPegL6NdxTeBH2Wa3L86cZlEHizbkFJikr5u+E3wFoR5dLWJ+1OPyEfw==",
"dev": true,
"license": "MIT"
},
"node_modules/require-addon": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
"integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-addon-resolve": "^1.3.0"
},
"engines": {
"bare": ">=1.10.0"
}
},
"node_modules/require-asset": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/require-asset/-/require-asset-1.2.1.tgz",
"integrity": "sha512-wFFxOxJHL/agpiXNDMzDBzC8hnmjJZYW0wDgolojZZkydS76talLlIH+DU3DWjBxQcu0vair30H7V3C4FcLnSg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bare-module-resolve": "^1.6.2"
},
"engines": {
"bare": ">=1.10.0"
}
},
"node_modules/sodium-native": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
"integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
"dev": true,
"license": "MIT",
"dependencies": {
"bare-assert": "^1.2.0",
"require-addon": "^1.1.0",
"which-runtime": "^1.2.1"
},
"engines": {
"bare": ">=1.16.0"
}
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/which-runtime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
"integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
"dev": true,
"license": "Apache-2.0"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "anisail",
"version": "1.0.0",
"description": "Ansible connection plugin for Holesail P2P tunnels",
"private": true,
"license": "MIT",
"scripts": {
"build:tunnel": "node scripts/build-tunnel.js",
"build:tunnel:all": "node scripts/build-tunnel.js --all"
},
"devDependencies": {
"bare-build": "^0.4.3"
}
}
+5
View File
@@ -0,0 +1,5 @@
# For the Ansible connection plugin (control node)
paramiko>=2.11.0
# Optional: to run playbooks
# ansible-core>=2.14
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env node
/**
* Build standalone anisail-tunnel binary using bare-pack + bare-build.
* Output: releases/<host>/anisail-tunnel (or .exe on Windows).
*
* Usage:
* node scripts/build-tunnel.js # current host only
* node scripts/build-tunnel.js --all # all platforms
* node scripts/build-tunnel.js --host darwin-arm64 --host linux-x64
*/
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execSync } = require('child_process');
const { pathToFileURL } = require('url');
const ROOT = path.join(__dirname, '..');
const TUNNEL_DIR = path.join(ROOT, 'tunnel');
const RELEASES_DIR = path.join(ROOT, 'releases');
const ENTRY = path.join(TUNNEL_DIR, 'index.mjs');
const ALL_HOSTS = [
'darwin-arm64',
'darwin-x64',
'linux-arm64',
'linux-x64',
'win32-x64'
];
const BUILTINS = [
'source-map-support',
'dtrace-provider'
];
function getCurrentHost() {
const platform = os.platform();
const arch = os.arch() === 'arm64' ? 'arm64' : 'x64';
return `${platform}-${arch}`;
}
function parseArgs() {
const args = process.argv.slice(2);
const hosts = [];
let all = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--all') {
all = true;
} else if (args[i] === '--host' && args[i + 1]) {
hosts.push(args[++i]);
}
}
if (all) return ALL_HOSTS;
if (hosts.length > 0) return hosts;
return [getCurrentHost()];
}
function getPlatformModule(host) {
const bareBuildDir = path.dirname(require.resolve('bare-build'));
switch (host) {
case 'darwin-arm64':
case 'darwin-x64':
return require(path.join(bareBuildDir, 'lib/platform/apple'));
case 'linux-arm64':
case 'linux-x64':
return require(path.join(bareBuildDir, 'lib/platform/linux'));
case 'win32-x64':
case 'win32-arm64':
return require(path.join(bareBuildDir, 'lib/platform/windows'));
default:
throw new Error(`Unknown host '${host}'`);
}
}
function patchBareBuildSignForLinux() {
if (os.platform() === 'darwin') return;
try {
execSync('which codesign', { stdio: 'ignore' });
return;
} catch (_) {}
const bareBuildDir = path.dirname(require.resolve('bare-build'));
const signPath = path.join(bareBuildDir, 'lib/platform/apple/sign.js');
if (!fs.existsSync(signPath)) return;
const current = fs.readFileSync(signPath, 'utf8');
if (current.includes('PATCHED_NO_CODESIGN')) return;
fs.writeFileSync(signPath, `// PATCHED_NO_CODESIGN: codesign not available on this platform
module.exports = async function sign() {}
`);
console.log(' Patched bare-build/apple/sign.js (codesign not available)');
}
function normalizeBundleKeysToWindows(bundle) {
const Bundle = bundle.constructor;
const next = new Bundle();
next._id = bundle._id;
const keyMap = {};
for (const key of bundle.keys()) {
const newKey = key.replace(/\//g, '\\');
keyMap[key] = newKey;
const content = bundle.read(key);
const mode = bundle.mode(key);
const opts = { mode };
if (key === bundle.main) opts.main = true;
if (bundle.addons && bundle.addons.includes(key)) opts.addon = true;
if (bundle.assets && bundle.assets.includes(key)) opts.asset = true;
const res = bundle.resolutions && bundle.resolutions[key];
if (res) opts.imports = res;
next.write(newKey, content, opts);
}
for (const [alias, key] of Object.entries(bundle.imports || {})) {
next._imports[alias] = keyMap[key] ?? key.replace(/\//g, '\\');
}
return next;
}
function patchBundle(bundle) {
const bundleKeys = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
function resolveJsonDiskPath(keyNorm) {
const inTunnel = path.join(TUNNEL_DIR, keyNorm);
if (fs.existsSync(inTunnel)) return inTunnel;
if (keyNorm.startsWith('node_modules' + path.sep) || keyNorm.startsWith('node_modules/')) {
const inRoot = path.join(ROOT, keyNorm.replace(/\//g, path.sep));
if (fs.existsSync(inRoot)) return inRoot;
}
return null;
}
for (const key of bundleKeys) {
if (!key.endsWith('.json')) continue;
let content = bundle.read(key);
if (!content || content.length === 0) {
const altKey = key.startsWith('/') ? key.slice(1) : '/' + key.replace(/^\/+/, '');
content = bundle.read(altKey);
if (content && content.length > 0) bundle.write(key, content);
}
const isEmpty = !content || content.length === 0;
const invalidJson = content && content.length > 0 && (() => {
try { JSON.parse(content.toString()); return false; } catch (_) { return true; }
})();
if (isEmpty || invalidJson) {
const keyNorm = key.replace(/^\/+/, '').replace(/\//g, path.sep);
const diskPath = resolveJsonDiskPath(keyNorm);
if (diskPath) {
content = fs.readFileSync(diskPath);
bundle.write(key, content);
}
}
if (content && content.length > 0) {
const keyNoLead = key.replace(/^\/+/, '');
const prefixSlash = 'runtime.bundle/' + keyNoLead;
const prefixLead = '/runtime.bundle/' + keyNoLead;
if (prefixSlash !== key) bundle.write(prefixSlash, content);
if (prefixLead !== key) bundle.write(prefixLead, content);
}
}
}
async function build(hosts) {
patchBareBuildSignForLinux();
if (!fs.existsSync(path.join(TUNNEL_DIR, 'node_modules'))) {
console.log('Installing tunnel dependencies...');
execSync('npm install', { cwd: TUNNEL_DIR, stdio: 'inherit' });
}
fs.mkdirSync(RELEASES_DIR, { recursive: true });
const pack = require('bare-pack');
const { readModule, listPrefix } = require('bare-pack/fs');
const traverse = require('bare-module-traverse');
const bundleId = require('bare-bundle-id');
const pkg = require(path.join(TUNNEL_DIR, 'package.json'));
console.log('\nBuilding anisail-tunnel v' + pkg.version);
console.log('Targets: ' + hosts.join(', '));
console.log('Entry: ' + ENTRY);
console.log('Output: ' + RELEASES_DIR + '\n');
const unixHosts = hosts.filter((h) => !h.startsWith('win32'));
const winHosts = hosts.filter((h) => h.startsWith('win32'));
const hasWindows = winHosts.length > 0;
const hasUnix = unixHosts.length > 0;
async function buildAndEmit(bundleHosts, platformHostsList, normalizeForWindows) {
if (bundleHosts.length === 0) return;
console.log(' Bundling module graph' + (normalizeForWindows ? ' (Windows bundle)' : '') + '...');
let bundle = await pack(
pathToFileURL(ENTRY),
{
hosts: bundleHosts,
linked: false,
resolve: traverse.resolve.bare,
builtins: BUILTINS
},
readModule,
listPrefix
);
bundle = bundle.unmount(pathToFileURL(TUNNEL_DIR + '/'));
patchBundle(bundle);
if (normalizeForWindows) {
bundle = normalizeBundleKeysToWindows(bundle);
console.log(' Normalized bundle keys to Windows path form');
}
bundle.id = bundleId(bundle).toString('hex');
const bundleSize = bundle.toBuffer().length;
console.log(' Bundle size: ' + (bundleSize / 1024 / 1024).toFixed(1) + ' MB');
const groups = new Map();
for (const h of platformHostsList) {
const platform = getPlatformModule(h);
if (!groups.has(platform)) groups.set(platform, []);
groups.get(platform).push(h);
}
for (const [platform, platformHosts] of groups) {
console.log(' Building ' + platformHosts.join(', ') + '...');
for await (const file of platform(TUNNEL_DIR, bundle, null, {
name: 'anisail-tunnel',
version: pkg.version,
description: pkg.description,
hosts: platformHosts,
out: RELEASES_DIR,
standalone: true
})) {
console.log(' Built: ' + path.relative(ROOT, file));
}
}
}
if (hasWindows && hasUnix) {
await buildAndEmit(unixHosts, unixHosts, false);
await buildAndEmit(winHosts, winHosts, true);
} else if (hasWindows) {
await buildAndEmit(winHosts, winHosts, true);
} else {
await buildAndEmit(hosts, hosts, false);
}
console.log('\nDone.');
}
const hosts = parseArgs();
build(hosts).catch((err) => {
console.error('\nBuild failed:', err.message || err);
if (err.cause) console.error('Cause:', err.cause);
process.exitCode = 1;
});
+147
View File
@@ -0,0 +1,147 @@
/**
* anisail-tunnel: Bare-compatible Holesail client that binds a local port and
* prints it to stdout for the Ansible connection plugin. Keeps running until
* stdin closes or SIGTERM/SIGINT.
*
* Usage: anisail-tunnel --key <hs://...> [--remote-port 22] [--local-port 0] [--timeout 30000]
* Output: exactly one JSON line to stdout: {"local_port": <number>, "ready": true}
*/
import 'bare-process/global';
const DEFAULT_REMOTE_PORT = 22;
const DEFAULT_LOCAL_PORT = 0;
const DEFAULT_TIMEOUT_MS = 30000;
const LOCAL_HOST = '127.0.0.1';
function parseArgs() {
const args = process.argv.slice(2);
let key = null;
let remotePort = DEFAULT_REMOTE_PORT;
let localPort = DEFAULT_LOCAL_PORT;
let timeoutMs = DEFAULT_TIMEOUT_MS;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--key' && args[i + 1]) {
key = args[++i].trim();
} else if (args[i] === '--remote-port' && args[i + 1]) {
remotePort = parseInt(args[++i], 10) || DEFAULT_REMOTE_PORT;
} else if (args[i] === '--local-port' && args[i + 1]) {
localPort = parseInt(args[++i], 10);
if (isNaN(localPort) || localPort < 0) localPort = DEFAULT_LOCAL_PORT;
} else if (args[i] === '--timeout' && args[i + 1]) {
timeoutMs = parseInt(args[++i], 10) || DEFAULT_TIMEOUT_MS;
}
}
return { key, remotePort, localPort, timeoutMs };
}
function emit(line) {
try {
process.stdout.write(line + '\n');
} catch (_) {}
}
async function run() {
const mod = await import('holesail');
const Holesail = mod.default || mod;
const { key, remotePort, localPort, timeoutMs } = parseArgs();
if (!key || key.length === 0) {
const msg = 'anisail-tunnel: --key <hs://...> is required';
try {
process.stderr.write(msg + '\n');
} catch (_) {}
process.exitCode = 1;
return;
}
const portToUse = (localPort > 0 ? localPort : 19200 + Math.floor(Math.random() * 1000)) || 19200;
const readyPromise = new Promise((resolve, reject) => {
const t = timeoutMs > 0
? setTimeout(() => reject(new Error('Tunnel ready timeout after ' + timeoutMs + 'ms')), timeoutMs)
: null;
const done = (err, port) => {
if (t) clearTimeout(t);
if (err) reject(err);
else resolve(port);
};
const hs = new Holesail({
client: true,
key,
host: LOCAL_HOST,
port: portToUse
});
if (typeof hs.on === 'function') {
hs.on('error', (err) => {
done(err || new Error('Tunnel error'));
});
}
hs.ready()
.then(() => {
const info = typeof hs.info === 'function' ? hs.info() : (hs.info || {});
const boundPort = (info && typeof info.port === 'number')
? info.port
: portToUse;
if (boundPort == null || boundPort < 1 || boundPort > 65535) {
done(new Error('Could not determine local tunnel port'));
return;
}
done(null, boundPort);
return hs;
})
.catch(done);
run._hs = hs;
});
let hs;
try {
const port = await readyPromise;
hs = run._hs;
emit(JSON.stringify({ local_port: port, ready: true }));
} catch (err) {
const msg = (err && err.message) ? err.message : String(err);
try {
process.stderr.write('anisail-tunnel: ' + msg + '\n');
} catch (_) {}
process.exitCode = 1;
return;
}
const close = () => {
if (hs && typeof hs.close === 'function') {
try {
hs.close();
} catch (_) {}
hs = null;
}
process.exitCode = 0;
process.exit(0);
};
process.on('SIGTERM', close);
process.on('SIGINT', close);
process.stdin.on('close', () => {
close();
});
process.stdin.on('end', () => {
close();
});
process.stdin.resume();
}
run().catch((err) => {
try {
process.stderr.write('anisail-tunnel: ' + (err && err.message ? err.message : String(err)) + '\n');
} catch (_) {}
process.exitCode = 1;
});
+1660
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "anisail-tunnel",
"version": "1.0.0",
"description": "Bare-compatible Holesail tunnel for Ansible connection plugin",
"type": "commonjs",
"license": "MIT",
"main": "index.mjs",
"scripts": {
"start": "bare index.mjs"
},
"dependencies": {
"assert": "npm:bare-node-assert@^1.0.0",
"child_process": "npm:bare-subprocess@^5.2.2",
"crypto": "npm:bare-node-crypto@^1.0.0",
"events": "npm:bare-node-events@^1.0.1",
"fs": "npm:bare-node-fs@^1.0.2",
"holesail": "^2.4.1",
"net": "npm:bare-node-net@^1.0.0",
"os": "npm:bare-node-os@^1.0.1",
"path": "npm:bare-node-path@^1.0.1",
"stream": "npm:bare-node-stream@^1.0.0",
"tls": "npm:bare-node-tls@^1.0.0",
"util": "npm:bare-node-util@^1.0.0"
},
"imports": {
"child_process": { "bare": "bare-subprocess", "default": "child_process" },
"assert": { "bare": "assert", "default": "assert" },
"util": { "bare": "util", "default": "util" },
"net": { "bare": "net", "default": "net" },
"tls": { "bare": "tls", "default": "tls" },
"events": { "bare": "events", "default": "events" },
"crypto": { "bare": "crypto", "default": "crypto" },
"fs": { "bare": "fs", "default": "fs" },
"path": { "bare": "path", "default": "path" },
"process": { "bare": "bare-process", "default": "process" },
"os": { "bare": "os", "default": "os" },
"stream": { "bare": "stream", "default": "stream" }
},
"engines": {
"bare": ">=1.0.0"
}
}