mirror of
https://github.com/Heretek-Games/drop-zerotier.git
synced 2026-11-07 10:05:57 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa785e4848 | ||
|
|
0861452c63 | ||
|
|
62cf8157b6 | ||
|
|
fd9250dd4a | ||
|
|
63ec069a26 | ||
|
|
0069a37a78 | ||
|
|
5320da59b9 |
No files matched your search
@@ -0,0 +1,63 @@
|
||||
name: Release Drop Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build, validate, and publish .dropplugin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # actions/[email protected]
|
||||
|
||||
- name: Set up Node.js 22
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # actions/[email protected]
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Assemble plugin bundle
|
||||
run: npm run bundle:build
|
||||
|
||||
- name: Validate manifest
|
||||
run: npm run validate
|
||||
|
||||
- name: Pack plugin archive
|
||||
run: npm run pack
|
||||
|
||||
- name: Generate checksums
|
||||
id: pkg
|
||||
run: |
|
||||
FILE=$(find dist-packages -name "*.dropplugin" -print -quit)
|
||||
echo "path=$FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "name=$(basename "$FILE")" >> "$GITHUB_OUTPUT"
|
||||
sha256sum "$FILE" > "$FILE.sha256"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ steps.pkg.outputs.path }}
|
||||
${{ steps.pkg.outputs.path }}.sha256
|
||||
name: ${{ steps.pkg.outputs.name }}
|
||||
prerelease: ${{ contains(github.ref_name, '-') }}
|
||||
generate_release_notes: true
|
||||
@@ -8,3 +8,5 @@ plugin-bundle/dist/
|
||||
plugin-bundle/dist-package/
|
||||
dist-packages/
|
||||
*.tsbuildinfo
|
||||
--out/
|
||||
dist-package/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"sdk": "@droposs",
|
||||
"sdkVersion": "^0.6.1",
|
||||
"cliVersion": "^0.6.1"
|
||||
}
|
||||
Generated
+562
-28
@@ -12,7 +12,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@droposs/plugin-cli": "file:../drop-plugin-sdk/packages/plugin-cli",
|
||||
"@droposs/plugin-cli": "^0.6.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"eslint": "^10.9.1",
|
||||
@@ -24,30 +24,10 @@
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"../drop-plugin-sdk/packages/plugin-cli": {
|
||||
"name": "@drop-oss/plugin-cli",
|
||||
"version": "0.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@drop-oss/plugin-sdk": "workspace:*",
|
||||
"ajv": "^8.20.0",
|
||||
"esbuild": "^0.28.2"
|
||||
},
|
||||
"bin": {
|
||||
"drop-plugin": "bin/drop-plugin.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.5.1",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"../drop-plugin-sdk/packages/plugin-sdk": {
|
||||
"name": "@drop-oss/plugin-sdk",
|
||||
"version": "0.6.0",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.5.1",
|
||||
@@ -90,12 +70,497 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@droposs/plugin-cli": {
|
||||
"resolved": "../drop-plugin-sdk/packages/plugin-cli",
|
||||
"link": true
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@droposs/plugin-cli/-/plugin-cli-0.6.1.tgz",
|
||||
"integrity": "sha512-K+kZn8MGW+D6tP49oCD7uWaZQ9l4MFrC8kftsSjxwg+EW9GMaYlu2ZMma5ET2Im5vLFibOaZyHHAS/JA/UKoUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@droposs/plugin-sdk": "0.6.1",
|
||||
"ajv": "^8.20.0",
|
||||
"esbuild": "^0.28.2"
|
||||
},
|
||||
"bin": {
|
||||
"drop-plugin": "bin/drop-plugin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@droposs/plugin-cli/node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@droposs/plugin-cli/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@droposs/plugin-sdk": {
|
||||
"resolved": "../drop-plugin-sdk/packages/plugin-sdk",
|
||||
"link": true
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@droposs/plugin-sdk/-/plugin-sdk-0.6.1.tgz",
|
||||
"integrity": "sha512-t/GEmBI3PiakjgJcvpK8k2YVQWUy8bTznPvsScW9+ANhhOQMT2NdMGiJb4RwsI/CvTK1UoAEgSy0OjoGHOGAVA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
||||
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
||||
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
||||
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
||||
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
||||
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
||||
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
||||
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
||||
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
||||
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.10.1",
|
||||
@@ -728,6 +1193,48 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
||||
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.2",
|
||||
"@esbuild/android-arm": "0.28.2",
|
||||
"@esbuild/android-arm64": "0.28.2",
|
||||
"@esbuild/android-x64": "0.28.2",
|
||||
"@esbuild/darwin-arm64": "0.28.2",
|
||||
"@esbuild/darwin-x64": "0.28.2",
|
||||
"@esbuild/freebsd-arm64": "0.28.2",
|
||||
"@esbuild/freebsd-x64": "0.28.2",
|
||||
"@esbuild/linux-arm": "0.28.2",
|
||||
"@esbuild/linux-arm64": "0.28.2",
|
||||
"@esbuild/linux-ia32": "0.28.2",
|
||||
"@esbuild/linux-loong64": "0.28.2",
|
||||
"@esbuild/linux-mips64el": "0.28.2",
|
||||
"@esbuild/linux-ppc64": "0.28.2",
|
||||
"@esbuild/linux-riscv64": "0.28.2",
|
||||
"@esbuild/linux-s390x": "0.28.2",
|
||||
"@esbuild/linux-x64": "0.28.2",
|
||||
"@esbuild/netbsd-arm64": "0.28.2",
|
||||
"@esbuild/netbsd-x64": "0.28.2",
|
||||
"@esbuild/openbsd-arm64": "0.28.2",
|
||||
"@esbuild/openbsd-x64": "0.28.2",
|
||||
"@esbuild/openharmony-arm64": "0.28.2",
|
||||
"@esbuild/sunos-x64": "0.28.2",
|
||||
"@esbuild/win32-arm64": "0.28.2",
|
||||
"@esbuild/win32-ia32": "0.28.2",
|
||||
"@esbuild/win32-x64": "0.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -917,6 +1424,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -1329,6 +1853,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
@@ -1521,7 +2055,7 @@
|
||||
"version": "0.3.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@droposs/plugin-sdk": "file:../../../drop-plugin-sdk/packages/plugin-sdk"
|
||||
"@droposs/plugin-sdk": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
@@ -1533,7 +2067,7 @@
|
||||
"version": "0.3.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@droposs/plugin-sdk": "file:../../../drop-plugin-sdk/packages/plugin-sdk",
|
||||
"@droposs/plugin-sdk": "^0.6.1",
|
||||
"@heretek-games/zerotier-mesh": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@droposs/plugin-cli": "file:../drop-plugin-sdk/packages/plugin-cli",
|
||||
"@droposs/plugin-cli": "^0.6.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"prettier": "^3.5.0",
|
||||
"typescript": "^5.7.2",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"test": "node --test dist/test/*.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@droposs/plugin-sdk": "file:../../../drop-plugin-sdk/packages/plugin-sdk"
|
||||
"@droposs/plugin-sdk": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
* it leaves the networks again (fail-closed revocation happens server-side).
|
||||
*/
|
||||
|
||||
import type { ClientPlugin, ClientPluginContext } from "@droposs/plugin-sdk";
|
||||
import type {
|
||||
ClientPlugin,
|
||||
ClientPluginContext,
|
||||
LaunchContext,
|
||||
} from "@droposs/plugin-sdk";
|
||||
import { joinNetwork, leaveNetwork } from "./zerotier-cli.js";
|
||||
|
||||
export {
|
||||
@@ -33,11 +37,13 @@ export interface NetworkView {
|
||||
members: Array<{ userId: string; address?: string; joinedAt: number }>;
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
gameId?: string;
|
||||
}
|
||||
|
||||
interface StoredJoin {
|
||||
key: string;
|
||||
networkId: string;
|
||||
gameId?: string;
|
||||
}
|
||||
|
||||
export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
@@ -53,7 +59,6 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
capabilities: [
|
||||
"game:launch-hook" as const,
|
||||
"client:storage" as const,
|
||||
"client:ws" as const,
|
||||
"system:command" as const,
|
||||
"ui:slot" as const,
|
||||
],
|
||||
@@ -63,19 +68,33 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
ctx.registerLaunchHook({
|
||||
stage: "pre-launch:network",
|
||||
order: 10,
|
||||
execute: () => this.joinActiveNetworks(ctx),
|
||||
execute: (launch?: LaunchContext) => this.joinActiveNetworks(ctx, launch),
|
||||
});
|
||||
ctx.registerLaunchHook({
|
||||
stage: "post-exit:cleanup",
|
||||
order: 10,
|
||||
execute: () => this.leaveActiveNetworks(ctx),
|
||||
execute: (launch?: LaunchContext) => this.leaveActiveNetworks(ctx, launch),
|
||||
});
|
||||
|
||||
ctx.registerSlot(
|
||||
"game-detail:badges",
|
||||
{
|
||||
template:
|
||||
'<span class="text-xs text-zinc-400">Mesh network available</span>',
|
||||
render() {
|
||||
const g = globalThis as Record<string, any>;
|
||||
const vue = g.Vue ?? g.window?.Vue;
|
||||
if (typeof vue?.h === "function") {
|
||||
return vue.h(
|
||||
"span",
|
||||
{ class: "text-xs text-zinc-400" },
|
||||
"Mesh network available",
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: "span",
|
||||
props: { class: "text-xs text-zinc-400" },
|
||||
children: "Mesh network available",
|
||||
};
|
||||
},
|
||||
},
|
||||
{ label: "ZeroTier", order: 40 },
|
||||
);
|
||||
@@ -87,13 +106,15 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
*/
|
||||
private async activeNetworks(
|
||||
ctx: ClientPluginContext,
|
||||
gameId?: string,
|
||||
attempts = 3,
|
||||
): Promise<NetworkView[]> {
|
||||
const query = gameId ? `?gameId=${encodeURIComponent(gameId)}` : "";
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
try {
|
||||
const res = await ctx.serverRequest<{ networks: NetworkView[] }>(
|
||||
"GET",
|
||||
"/networks/active",
|
||||
`/networks/active${query}`,
|
||||
);
|
||||
const networks = res?.networks ?? [];
|
||||
if (networks.length > 0 || attempt === attempts - 1) return networks;
|
||||
@@ -111,9 +132,15 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
}
|
||||
|
||||
/** Join every active ZeroTier network and report this node's id. */
|
||||
private async joinActiveNetworks(ctx: ClientPluginContext): Promise<void> {
|
||||
const networks = await this.activeNetworks(ctx);
|
||||
const joined: StoredJoin[] = [];
|
||||
private async joinActiveNetworks(
|
||||
ctx: ClientPluginContext,
|
||||
launch?: LaunchContext,
|
||||
): Promise<void> {
|
||||
const gameId = launch?.gameId;
|
||||
const networks = await this.activeNetworks(ctx, gameId);
|
||||
const previouslyJoined =
|
||||
(await ctx.storage.get<StoredJoin[]>(ACTIVE_NETWORKS_KEY)) ?? [];
|
||||
const newlyJoined: StoredJoin[] = [];
|
||||
|
||||
for (const network of networks) {
|
||||
if (network.mesh.backend !== "zerotier") {
|
||||
@@ -129,7 +156,11 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
`/networks/${encodeURIComponent(network.key)}/member`,
|
||||
{ memberId: id },
|
||||
);
|
||||
joined.push({ key: network.key, networkId: network.mesh.networkId });
|
||||
newlyJoined.push({
|
||||
key: network.key,
|
||||
networkId: network.mesh.networkId,
|
||||
...(gameId ? { gameId } : {}),
|
||||
});
|
||||
ctx.logger.info(`Joined mesh network ${network.key} as ${id}`);
|
||||
} catch (err) {
|
||||
ctx.logger.warn(
|
||||
@@ -138,14 +169,32 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.storage.set(ACTIVE_NETWORKS_KEY, joined);
|
||||
const merged = [
|
||||
...previouslyJoined.filter(
|
||||
(prev) => !newlyJoined.some((curr) => curr.key === prev.key),
|
||||
),
|
||||
...newlyJoined,
|
||||
];
|
||||
await ctx.storage.set(ACTIVE_NETWORKS_KEY, merged);
|
||||
}
|
||||
|
||||
/** Leave everything joined for this session. */
|
||||
private async leaveActiveNetworks(ctx: ClientPluginContext): Promise<void> {
|
||||
/** Leave networks joined for this game session. */
|
||||
private async leaveActiveNetworks(
|
||||
ctx: ClientPluginContext,
|
||||
launch?: LaunchContext,
|
||||
): Promise<void> {
|
||||
const gameId = launch?.gameId;
|
||||
const joined =
|
||||
(await ctx.storage.get<StoredJoin[]>(ACTIVE_NETWORKS_KEY)) ?? [];
|
||||
for (const entry of joined) {
|
||||
|
||||
const toLeave = gameId
|
||||
? joined.filter((entry) => entry.gameId === gameId || !entry.gameId)
|
||||
: joined;
|
||||
const toKeep = gameId
|
||||
? joined.filter((entry) => entry.gameId && entry.gameId !== gameId)
|
||||
: [];
|
||||
|
||||
for (const entry of toLeave) {
|
||||
try {
|
||||
await leaveNetwork(ctx, entry.networkId);
|
||||
} catch (err) {
|
||||
@@ -154,7 +203,12 @@ export class DropZeroTierClientPlugin implements ClientPlugin {
|
||||
);
|
||||
}
|
||||
}
|
||||
await ctx.storage.delete(ACTIVE_NETWORKS_KEY).catch(() => {});
|
||||
|
||||
if (toKeep.length > 0) {
|
||||
await ctx.storage.set(ACTIVE_NETWORKS_KEY, toKeep);
|
||||
} else {
|
||||
await ctx.storage.delete(ACTIVE_NETWORKS_KEY).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ function makeContext() {
|
||||
"system:command",
|
||||
"ui:slot",
|
||||
]);
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active", {
|
||||
const activeNetworks = {
|
||||
networks: [
|
||||
{
|
||||
key: "room-1",
|
||||
@@ -29,7 +29,9 @@ function makeContext() {
|
||||
expiresAt: Date.now() + 60_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active", activeNetworks);
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active?gameId=g1", activeNetworks);
|
||||
ctx.serverRequestLog.setResponse("POST", "/networks/room-1/member", {
|
||||
address: "10.242.1.20",
|
||||
});
|
||||
@@ -111,7 +113,7 @@ test("missing system:command capability is rejected", async () => {
|
||||
"client:storage",
|
||||
"ui:slot",
|
||||
]);
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active", {
|
||||
const activeNetworks = {
|
||||
networks: [
|
||||
{
|
||||
key: "room-1",
|
||||
@@ -126,7 +128,9 @@ test("missing system:command capability is rejected", async () => {
|
||||
expiresAt: Date.now() + 60_000,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active", activeNetworks);
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active?gameId=g1", activeNetworks);
|
||||
plugin.init(ctx);
|
||||
|
||||
const hook = ctx.launchHooks.find((h) => h.stage === "pre-launch:network");
|
||||
@@ -134,3 +138,93 @@ test("missing system:command capability is rejected", async () => {
|
||||
await hook!.execute({ gameId: "g1", gameTitle: "Game", gameDir: "/g" });
|
||||
assert.equal(ctx.systemCommand.calls.length, 0);
|
||||
});
|
||||
|
||||
test("launch hooks isolate joined networks by gameId", async () => {
|
||||
const plugin = new DropZeroTierClientPlugin();
|
||||
const ctx = new MockClientPluginContext("drop-zerotier", [
|
||||
"game:launch-hook",
|
||||
"client:storage",
|
||||
"system:command",
|
||||
"ui:slot",
|
||||
]);
|
||||
|
||||
const NET_A = "8056c2e21c00000a";
|
||||
const NET_B = "8056c2e21c00000b";
|
||||
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active?gameId=game-a", {
|
||||
networks: [
|
||||
{
|
||||
key: "room-a",
|
||||
mesh: { backend: "zerotier", cidr: "10.242.1.0/24", networkId: NET_A, expiresAt: Date.now() + 60_000 },
|
||||
members: [],
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + 60_000,
|
||||
gameId: "game-a",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.serverRequestLog.setResponse("GET", "/networks/active?gameId=game-b", {
|
||||
networks: [
|
||||
{
|
||||
key: "room-b",
|
||||
mesh: { backend: "zerotier", cidr: "10.242.2.0/24", networkId: NET_B, expiresAt: Date.now() + 60_000 },
|
||||
members: [],
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + 60_000,
|
||||
gameId: "game-b",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.serverRequestLog.setResponse("POST", "/networks/room-a/member", { address: "10.242.1.20" });
|
||||
ctx.serverRequestLog.setResponse("POST", "/networks/room-b/member", { address: "10.242.2.20" });
|
||||
|
||||
ctx.systemCommand.setResponse("zerotier-cli", ["info"], {
|
||||
code: 0,
|
||||
stdout: "200 info abcdef0123 1.12.2 ONLINE",
|
||||
stderr: "",
|
||||
});
|
||||
ctx.systemCommand.setResponse("zerotier-cli", ["join", NET_A], { code: 0, stdout: "200 join OK", stderr: "" });
|
||||
ctx.systemCommand.setResponse("zerotier-cli", ["join", NET_B], { code: 0, stdout: "200 join OK", stderr: "" });
|
||||
ctx.systemCommand.setResponse("zerotier-cli", ["leave", NET_A], { code: 0, stdout: "200 leave OK", stderr: "" });
|
||||
ctx.systemCommand.setResponse("zerotier-cli", ["leave", NET_B], { code: 0, stdout: "200 leave OK", stderr: "" });
|
||||
|
||||
plugin.init(ctx);
|
||||
const pre = ctx.launchHooks.find((h) => h.stage === "pre-launch:network")!;
|
||||
const post = ctx.launchHooks.find((h) => h.stage === "post-exit:cleanup")!;
|
||||
|
||||
// 1. Launch Game A
|
||||
await pre.execute({ gameId: "game-a", gameTitle: "Game A", gameDir: "/a" });
|
||||
let stored = (await ctx.storage.get<any[]>("zerotier:activeNetworks")) ?? [];
|
||||
assert.equal(stored.length, 1);
|
||||
assert.equal(stored[0].key, "room-a");
|
||||
assert.equal(stored[0].gameId, "game-a");
|
||||
|
||||
// 2. Launch Game B concurrently
|
||||
await pre.execute({ gameId: "game-b", gameTitle: "Game B", gameDir: "/b" });
|
||||
stored = (await ctx.storage.get<any[]>("zerotier:activeNetworks")) ?? [];
|
||||
assert.equal(stored.length, 2);
|
||||
|
||||
// 3. Exit Game B -> leaves only room-b
|
||||
await post.execute({ gameId: "game-b", gameTitle: "Game B", gameDir: "/b" });
|
||||
stored = (await ctx.storage.get<any[]>("zerotier:activeNetworks")) ?? [];
|
||||
assert.equal(stored.length, 1);
|
||||
assert.equal(stored[0].key, "room-a");
|
||||
|
||||
// 4. Exit Game A -> leaves room-a
|
||||
await post.execute({ gameId: "game-a", gameTitle: "Game A", gameDir: "/a" });
|
||||
assert.equal(await ctx.storage.get("zerotier:activeNetworks"), null);
|
||||
});
|
||||
|
||||
test("slot component renders without template compiler", () => {
|
||||
const plugin = new DropZeroTierClientPlugin();
|
||||
const ctx = makeContext();
|
||||
plugin.init(ctx);
|
||||
|
||||
const slots = ctx.registeredSlots.get("game-detail:badges");
|
||||
assert.ok(slots && slots.length > 0, "badge slot should be registered");
|
||||
const comp = slots[0].component as { render?: () => unknown; template?: string };
|
||||
assert.ok(typeof comp.render === "function", "component should provide a render function");
|
||||
const rendered = comp.render();
|
||||
assert.ok(rendered, "render() should return a vnode or node descriptor");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { assertManifestSupports } from "@droposs/plugin-sdk";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const bundlePath = path.resolve(
|
||||
__dirname,
|
||||
"../../../..",
|
||||
"plugin-bundle",
|
||||
"drop-plugin.json",
|
||||
);
|
||||
|
||||
test("drop-zerotier manifest conforms to server and client requirements", () => {
|
||||
const manifest = JSON.parse(readFileSync(bundlePath, "utf-8"));
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
assertManifestSupports(manifest, {
|
||||
server: ["routes", "storage", "websocket", "events", "network"],
|
||||
client: [
|
||||
"game:launch-hook",
|
||||
"client:storage",
|
||||
"system:command",
|
||||
"ui:slot",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heretek-games/zerotier-mesh": "^0.3.0",
|
||||
"@droposs/plugin-sdk": "file:../../../drop-plugin-sdk/packages/plugin-sdk"
|
||||
"@droposs/plugin-sdk": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -36,6 +36,7 @@ export const MESH_EVENT_MEMBER = "mesh:member";
|
||||
interface MemberJoinPayload {
|
||||
key?: unknown;
|
||||
userId?: unknown;
|
||||
gameId?: unknown;
|
||||
}
|
||||
|
||||
async function getRequestBody<T = any>(event: any): Promise<T | undefined> {
|
||||
@@ -65,6 +66,7 @@ async function getRequestBody<T = any>(event: any): Promise<T | undefined> {
|
||||
function readKeyedMembership(payload: unknown): {
|
||||
key: string;
|
||||
userId: string;
|
||||
gameId?: string;
|
||||
} | null {
|
||||
const data = (payload ?? {}) as MemberJoinPayload;
|
||||
if (
|
||||
@@ -75,7 +77,14 @@ function readKeyedMembership(payload: unknown): {
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { key: data.key, userId: data.userId };
|
||||
return {
|
||||
key: data.key,
|
||||
userId: data.userId,
|
||||
gameId:
|
||||
typeof data.gameId === "string" && data.gameId.length > 0
|
||||
? data.gameId
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** Member-visible view: peer node ids are revocation handles, so redact them. */
|
||||
@@ -90,6 +99,7 @@ function toNetworkView(network: MeshNetwork) {
|
||||
})),
|
||||
createdAt: network.createdAt,
|
||||
expiresAt: network.expiresAt,
|
||||
...(network.gameId ? { gameId: network.gameId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,13 +156,14 @@ export class DropZeroTierServerPlugin implements ServerPlugin {
|
||||
const membership = readKeyedMembership(payload);
|
||||
if (!membership) return;
|
||||
void this.store
|
||||
.addMember(membership.key, membership.userId)
|
||||
.addMember(membership.key, membership.userId, membership.gameId)
|
||||
.then((network) => {
|
||||
ctx.broadcast(MESH_EVENT_NETWORK, {
|
||||
type: "member_joined",
|
||||
key: membership.key,
|
||||
userId: membership.userId,
|
||||
mesh: network.mesh,
|
||||
...(network.gameId ? { gameId: network.gameId } : {}),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -201,7 +212,12 @@ export class DropZeroTierServerPlugin implements ServerPlugin {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const networks = await this.store.activeForUser(wsCtx.userId);
|
||||
const gameId =
|
||||
typeof (message as any)?.gameId === "string" &&
|
||||
(message as any).gameId.length > 0
|
||||
? (message as any).gameId
|
||||
: undefined;
|
||||
const networks = await this.store.activeForUser(wsCtx.userId, gameId);
|
||||
wsCtx.send({
|
||||
ok: true,
|
||||
networks: networks.map(toNetworkView),
|
||||
@@ -249,7 +265,12 @@ export class DropZeroTierServerPlugin implements ServerPlugin {
|
||||
});
|
||||
}
|
||||
await this.store.pruneExpired();
|
||||
const networks = await this.store.activeForUser(context.userId);
|
||||
const gameId =
|
||||
typeof context.query?.gameId === "string" &&
|
||||
context.query.gameId.length > 0
|
||||
? context.query.gameId
|
||||
: undefined;
|
||||
const networks = await this.store.activeForUser(context.userId, gameId);
|
||||
return { networks: networks.map(toNetworkView) };
|
||||
});
|
||||
|
||||
@@ -261,9 +282,11 @@ export class DropZeroTierServerPlugin implements ServerPlugin {
|
||||
statusMessage: "Authentication required",
|
||||
});
|
||||
}
|
||||
const body = await getRequestBody<{ key?: string; ttlMs?: number }>(
|
||||
event,
|
||||
);
|
||||
const body = await getRequestBody<{
|
||||
key?: string;
|
||||
ttlMs?: number;
|
||||
gameId?: string;
|
||||
}>(event);
|
||||
if (typeof body?.key !== "string" || body.key.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
@@ -284,11 +307,15 @@ export class DropZeroTierServerPlugin implements ServerPlugin {
|
||||
body.key,
|
||||
body.ttlMs ?? NETWORK_TTL_MS,
|
||||
context.userId,
|
||||
typeof body.gameId === "string" && body.gameId.length > 0
|
||||
? body.gameId
|
||||
: undefined,
|
||||
);
|
||||
ctx.broadcast(MESH_EVENT_NETWORK, {
|
||||
type: "network_created",
|
||||
key: network.key,
|
||||
mesh: network.mesh,
|
||||
...(network.gameId ? { gameId: network.gameId } : {}),
|
||||
});
|
||||
return { network: toNetworkView(network) };
|
||||
} catch (err) {
|
||||
@@ -322,17 +349,21 @@ export class DropZeroTierServerPlugin implements ServerPlugin {
|
||||
ctx.registerRoute(
|
||||
"POST",
|
||||
"/networks/:key/join",
|
||||
async (_event, context) => {
|
||||
async (event, context) => {
|
||||
if (!context.userId) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Authentication required",
|
||||
});
|
||||
}
|
||||
const body = await getRequestBody<{ gameId?: string }>(event);
|
||||
try {
|
||||
const network = await this.store.addMember(
|
||||
context.params.key,
|
||||
context.userId,
|
||||
typeof body?.gameId === "string" && body.gameId.length > 0
|
||||
? body.gameId
|
||||
: undefined,
|
||||
);
|
||||
return {
|
||||
network: toNetworkView(network),
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface MeshNetwork {
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
ownerId?: string;
|
||||
gameId?: string;
|
||||
}
|
||||
|
||||
const STATE_KEY = "networks";
|
||||
@@ -46,6 +47,7 @@ function isNetwork(value: unknown): value is MeshNetwork {
|
||||
typeof network.createdAt === "number" &&
|
||||
typeof network.expiresAt === "number" &&
|
||||
(network.ownerId === undefined || typeof network.ownerId === "string") &&
|
||||
(network.gameId === undefined || typeof network.gameId === "string") &&
|
||||
Array.isArray(network.members) &&
|
||||
network.members.every(
|
||||
(member) => !!member && typeof member.userId === "string",
|
||||
@@ -113,11 +115,18 @@ export class NetworkStore {
|
||||
key: string,
|
||||
ttlMs = NETWORK_TTL_MS,
|
||||
ownerId?: string,
|
||||
gameId?: string,
|
||||
): Promise<MeshNetwork> {
|
||||
return this.withLock(async () => {
|
||||
const state = await this.load();
|
||||
const existing = this.liveNetwork(state, key);
|
||||
if (existing) return existing;
|
||||
if (existing) {
|
||||
if (gameId && !existing.gameId && (!existing.ownerId || existing.ownerId === ownerId)) {
|
||||
existing.gameId = gameId;
|
||||
await this.save(state);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
const createdAt = this.now();
|
||||
const expiresAt = createdAt + ttlMs;
|
||||
@@ -129,6 +138,7 @@ export class NetworkStore {
|
||||
createdAt,
|
||||
expiresAt,
|
||||
...(ownerId ? { ownerId } : {}),
|
||||
...(gameId ? { gameId } : {}),
|
||||
};
|
||||
state.networks[key] = network;
|
||||
await this.save(state);
|
||||
@@ -150,7 +160,7 @@ export class NetworkStore {
|
||||
}
|
||||
|
||||
/** Networks the user has been added to that are currently active and unexpired. */
|
||||
async activeForUser(userId: string): Promise<MeshNetwork[]> {
|
||||
async activeForUser(userId: string, gameId?: string): Promise<MeshNetwork[]> {
|
||||
return this.withLock(async () => {
|
||||
const state = await this.load();
|
||||
const keys = state.membership[userId] ?? [];
|
||||
@@ -162,8 +172,10 @@ export class NetworkStore {
|
||||
network &&
|
||||
network.members.some((member) => member.userId === userId)
|
||||
) {
|
||||
networks.push(network);
|
||||
activeKeys.push(key);
|
||||
if (!gameId || network.gameId === gameId || !network.gameId) {
|
||||
networks.push(network);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activeKeys.length !== keys.length) {
|
||||
@@ -175,8 +187,12 @@ export class NetworkStore {
|
||||
}
|
||||
|
||||
/** Add a user to a network, provisioning it if necessary. */
|
||||
async addMember(key: string, userId: string): Promise<MeshNetwork> {
|
||||
await this.ensure(key);
|
||||
async addMember(
|
||||
key: string,
|
||||
userId: string,
|
||||
gameId?: string,
|
||||
): Promise<MeshNetwork> {
|
||||
await this.ensure(key, NETWORK_TTL_MS, undefined, gameId);
|
||||
return this.withLock(async () => {
|
||||
const state = await this.load();
|
||||
const network = this.liveNetwork(state, key);
|
||||
|
||||
@@ -298,3 +298,20 @@ test("plugin enforces auth on GET /networks and authorization on DELETE /network
|
||||
|
||||
plugin.teardown();
|
||||
});
|
||||
|
||||
test("NetworkStore isolates active networks by gameId", async () => {
|
||||
const store = makeStore();
|
||||
await store.addMember("game1-net", "user-1", "game-alpha");
|
||||
await store.addMember("game2-net", "user-1", "game-beta");
|
||||
await store.addMember("global-net", "user-1");
|
||||
|
||||
const alphaActive = await store.activeForUser("user-1", "game-alpha");
|
||||
assert.deepEqual(alphaActive.map((n) => n.key).sort(), ["game1-net", "global-net"]);
|
||||
|
||||
const betaActive = await store.activeForUser("user-1", "game-beta");
|
||||
assert.deepEqual(betaActive.map((n) => n.key).sort(), ["game2-net", "global-net"]);
|
||||
|
||||
const allActive = await store.activeForUser("user-1");
|
||||
assert.deepEqual(allActive.map((n) => n.key).sort(), ["game1-net", "game2-net", "global-net"]);
|
||||
});
|
||||
|
||||
@@ -19,12 +19,7 @@
|
||||
"storage",
|
||||
"websocket",
|
||||
"events",
|
||||
"network",
|
||||
"game:launch-hook",
|
||||
"client:storage",
|
||||
"client:ws",
|
||||
"system:command",
|
||||
"ui:slot"
|
||||
"network"
|
||||
],
|
||||
"server": {
|
||||
"source": "../packages/addon-server/dist/src/index.js",
|
||||
@@ -44,7 +39,6 @@
|
||||
"capabilities": [
|
||||
"game:launch-hook",
|
||||
"client:storage",
|
||||
"client:ws",
|
||||
"system:command",
|
||||
"ui:slot"
|
||||
],
|
||||
@@ -52,11 +46,11 @@
|
||||
"zerotier-cli"
|
||||
]
|
||||
},
|
||||
"checksum": "b93eb41af3caaebc14fa48d0ed5523fe48a57a34ddbe2c11439ec9566241fbce",
|
||||
"checksum": "f9e58102c725e3fd3f4e5cf4251c97f98de809693d26bb353eb825f62782fd77",
|
||||
"files": {
|
||||
"dist/client.js": "37eb2a6c6879de6640adcb07b36f74b1b4e2cfd555a58caf3fa5359f153e7440",
|
||||
"dist/client.js.map": "ab8d9e886ddd8ab579951501e9a0b73088e2e8695210e02cab9b260b9646a790",
|
||||
"dist/server.js": "b93eb41af3caaebc14fa48d0ed5523fe48a57a34ddbe2c11439ec9566241fbce",
|
||||
"dist/server.js.map": "36689044b9419e7ca510285c503503fda8a15594b336c5626cb82c3141162b54"
|
||||
"dist/client.js": "58c83f09822585f326743c96e65f865f9a23d532f8945ec99478e2cc4aad81b3",
|
||||
"dist/client.js.map": "76ea501feb5b9f1e618da9f06e6cd5c0bf6f9920162caf4507ba84ac742f689f",
|
||||
"dist/server.js": "f9e58102c725e3fd3f4e5cf4251c97f98de809693d26bb353eb825f62782fd77",
|
||||
"dist/server.js.map": "e8a73b196b264fcb0606c4ba4c20fe0ec11b9f40aa1e7ec63de07efa024ee65e"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
/* global process, console */
|
||||
|
||||
import {
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
} from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const LEGACY_SCOPE = "@droposs";
|
||||
const NEW_SCOPE = "@drop-oss";
|
||||
const SKIP_DIRS = new Set([
|
||||
"node_modules",
|
||||
".git",
|
||||
"dist-package",
|
||||
"dist-packages",
|
||||
]);
|
||||
const LOCKFILES = new Set([
|
||||
"package-lock.json",
|
||||
"pnpm-lock.yaml",
|
||||
"yarn.lock",
|
||||
"bun.lockb",
|
||||
"deno.lock",
|
||||
]);
|
||||
const TEXT_EXT = new Set([
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".json",
|
||||
".md",
|
||||
".yml",
|
||||
".yaml",
|
||||
]);
|
||||
|
||||
const configPath = path.resolve(process.cwd(), ".sdk-scope.json");
|
||||
if (!existsSync(configPath)) {
|
||||
console.error("Missing .sdk-scope.json in repo root");
|
||||
process.exit(1);
|
||||
}
|
||||
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
||||
const scope = config.sdk;
|
||||
if (scope !== NEW_SCOPE && scope !== LEGACY_SCOPE) {
|
||||
console.error(
|
||||
`Unknown scope '${scope}'; .sdk-scope.json sdk must be '${NEW_SCOPE}' or '${LEGACY_SCOPE}'`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const sdkVersion = config["sdkVersion"];
|
||||
const cliVersion = config["cliVersion"];
|
||||
if (typeof sdkVersion !== "string" || typeof cliVersion !== "string") {
|
||||
console.error(".sdk-scope.json must define sdkVersion and cliVersion");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const OTHER_SCOPES = scope === NEW_SCOPE ? [LEGACY_SCOPE] : [NEW_SCOPE];
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (SKIP_DIRS.has(entry.name)) continue;
|
||||
if (statSync(full).isDirectory()) walk(full, files);
|
||||
else files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function normalizeSpecifiers(text) {
|
||||
let out = text;
|
||||
for (const other of OTHER_SCOPES) {
|
||||
out = out
|
||||
.replaceAll(`${other}/plugin-sdk`, `${scope}/plugin-sdk`)
|
||||
.replaceAll(`${other}/plugin-cli`, `${scope}/plugin-cli`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function rewritePackageJson(file, text) {
|
||||
const indent = detectIndent(text);
|
||||
let pkg;
|
||||
try {
|
||||
pkg = JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
for (const sectionName of [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
]) {
|
||||
const section = pkg[sectionName];
|
||||
if (!section) continue;
|
||||
const rebuilt = {};
|
||||
for (const dep of Object.keys(section)) {
|
||||
const kind = parseSdkDep(dep);
|
||||
if (!kind) {
|
||||
rebuilt[dep] = section[dep];
|
||||
continue;
|
||||
}
|
||||
const target = `${scope}/${kind}`;
|
||||
rebuilt[target] = kind === "plugin-sdk" ? sdkVersion : cliVersion;
|
||||
}
|
||||
pkg[sectionName] = rebuilt;
|
||||
}
|
||||
return JSON.stringify(pkg, null, indent) + (text.endsWith("\n") ? "\n" : "");
|
||||
}
|
||||
|
||||
function parseSdkDep(dep) {
|
||||
for (const scopeName of [LEGACY_SCOPE, NEW_SCOPE, "@drop"]) {
|
||||
if (dep === `${scopeName}/plugin-sdk`) return "plugin-sdk";
|
||||
if (dep === `${scopeName}/plugin-cli`) return "plugin-cli";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectIndent(text) {
|
||||
const match = text.match(/\n([ \t]+)"/);
|
||||
return match ? match[1] : "\t";
|
||||
}
|
||||
|
||||
const edited = [];
|
||||
|
||||
const files = walk(process.cwd());
|
||||
for (const file of files) {
|
||||
if (LOCKFILES.has(path.basename(file))) continue;
|
||||
if (!TEXT_EXT.has(path.extname(file))) continue;
|
||||
const before = readFileSync(file, "utf-8");
|
||||
let after = normalizeSpecifiers(before);
|
||||
if (path.basename(file) === "package.json") {
|
||||
after = rewritePackageJson(file, after);
|
||||
}
|
||||
if (after !== before) {
|
||||
writeFileSync(file, after);
|
||||
edited.push(path.relative(process.cwd(), file));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Scope set to '${scope}' (sdk ${sdkVersion}, cli ${cliVersion})`);
|
||||
if (edited.length) {
|
||||
for (const f of edited) console.log(` rewrote ${f}`);
|
||||
} else {
|
||||
console.log(" no specifier changes needed");
|
||||
}
|
||||
|
||||
const installDir = process.cwd();
|
||||
|
||||
if (existsSync(path.join(installDir, "package-lock.json"))) {
|
||||
try {
|
||||
execSync("npm install --no-audit --no-fund", {
|
||||
cwd: installDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
} catch {
|
||||
execSync("npm install --no-audit --no-fund --legacy-peer-deps", {
|
||||
cwd: installDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
} else if (existsSync(path.join(installDir, "pnpm-lock.yaml"))) {
|
||||
execSync("pnpm install", { cwd: installDir, stdio: "inherit" });
|
||||
} else {
|
||||
console.warn("no lockfile found; skipping dependency install");
|
||||
}
|
||||
Reference in new issue
Block a user