Compare commits

...
7 Commits
Author SHA1 Message Date
BillyOutlast fa785e4848 fix: require owner authorization for network gameId retagging and handle query params in mock
Closes #15, closes #16
2026-09-17 20:54:03 -04:00
BillyOutlast 0861452c63 feat(zerotier): game-scoped mesh networks and production-safe client slot
- Pass LaunchContext through pre-launch/post-exit hooks and tag stored joins
  with gameId so launching one game never revokes membership of another's mesh
- Add optional gameId to MeshNetwork, POST /networks and GET /networks/active
- Replace raw template string with a render-function slot component
- Fix manifest conformance: top-level capabilities carry the server set; drop
  the unused client:ws declaration
- Add manifest conformance test asserting the shipped bundle capabilities
2026-09-17 16:23:41 -04:00
BillyOutlast 62cf8157b6 style: prettier-format switch-sdk-scope script 2026-09-17 15:38:18 -04:00
BillyOutlast fd9250dd4a chore(deps): pin @droposs sdk/cli to 0.6.1 (fixes cross-scope import in plugin-cli) 2026-09-17 15:37:26 -04:00
BillyOutlast 63ec069a26 chore(deps): follow configurable SDK scope onto @droposs 0.6.x mirror 2026-09-17 15:22:55 -04:00
BillyOutlast 0069a37a78 chore: ignore local build artifacts 2026-09-17 14:54:33 -04:00
BillyOutlast 5320da59b9 chore: configurable SDK scope (@droposs cutover) and .dropplugin release workflow 2026-09-17 14:48:57 -04:00
15 changed files with 1085 additions and 76 deletions

No files matched your search

+63
View File
@@ -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
+2
View File
@@ -8,3 +8,5 @@ plugin-bundle/dist/
plugin-bundle/dist-package/
dist-packages/
*.tsbuildinfo
--out/
dist-package/
+5
View File
@@ -0,0 +1,5 @@
{
"sdk": "@droposs",
"sdkVersion": "^0.6.1",
"cliVersion": "^0.6.1"
}
+562 -28
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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",
+70 -16
View File
@@ -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(() => {});
}
}
}
+98 -4
View File
@@ -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",
],
});
});
});
+1 -1
View File
@@ -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",
+39 -8
View File
@@ -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),
+21 -5
View File
@@ -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);
+17
View File
@@ -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"]);
});
+6 -12
View File
@@ -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"
}
}
+169
View File
@@ -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");
}