sealcode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/bin/sealcode.js +11 -0
- package/package.json +53 -0
- package/src/api.js +162 -0
- package/src/bundle.js +133 -0
- package/src/cli-auth.js +233 -0
- package/src/cli-grants.js +185 -0
- package/src/cli-link.js +90 -0
- package/src/cli.js +683 -0
- package/src/config.js +76 -0
- package/src/crypto.js +151 -0
- package/src/errors.js +111 -0
- package/src/hooks.js +127 -0
- package/src/index.js +19 -0
- package/src/init.js +180 -0
- package/src/kdf.js +83 -0
- package/src/keystore.js +249 -0
- package/src/link-state.js +60 -0
- package/src/manifest.js +25 -0
- package/src/open.js +43 -0
- package/src/presets.js +338 -0
- package/src/prompt.js +108 -0
- package/src/recovery.js +154 -0
- package/src/seal.js +174 -0
- package/src/status.js +207 -0
- package/src/ui.js +270 -0
- package/src/util.js +59 -0
- package/src/verify.js +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sealcode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# sealcode
|
|
2
|
+
|
|
3
|
+
> Lock your source code inside your own git repo. Stop AI agents, scrapers, and curious eyes from reading what's yours.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g sealcode
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
> **Renamed from `vaultline`.** Existing vaults keep working unchanged — the on-disk format is unchanged, `.vaultlinerc.json` is still read, and `VAULTLINE_PASSPHRASE` is still accepted as a fallback.
|
|
10
|
+
|
|
11
|
+
## What problem does this solve?
|
|
12
|
+
|
|
13
|
+
You push code to a private repo. You think it's safe. But anyone (or any AI) with read access to that repo — a contractor, a teammate's compromised laptop, a future Copilot indexing run, a leak — can read every line.
|
|
14
|
+
|
|
15
|
+
**sealcode** wraps your source files in AES-256-GCM ciphertext before they touch git. Your repo becomes a folder of opaque binary blobs with a stub `package.json`. To anyone without your passphrase, your codebase is uniformly random bytes.
|
|
16
|
+
|
|
17
|
+
To you, one command brings it all back:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sealcode unlock # readable code is restored
|
|
21
|
+
# ... edit ...
|
|
22
|
+
sealcode lock # back to ciphertext
|
|
23
|
+
git commit && git push
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart (60 seconds)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd your-project
|
|
30
|
+
sealcode init # interactive wizard
|
|
31
|
+
# 1. Auto-detects your stack (Node, Python, Go, Rust, Ruby, PHP, Java, ...)
|
|
32
|
+
# 2. Picks safe include/exclude defaults
|
|
33
|
+
# 3. Asks for a passphrase
|
|
34
|
+
# 4. Shows a recovery code — write it down ONCE
|
|
35
|
+
# 5. Locks every source file immediately
|
|
36
|
+
|
|
37
|
+
git add . && git commit -m "vault initialized" && git push
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
When you want to work on it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
sealcode unlock
|
|
44
|
+
npm install
|
|
45
|
+
npm run dev
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When you're done:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sealcode lock
|
|
52
|
+
git commit && git push
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why this beats `git-crypt`, `git-secret`, `sops`, etc.
|
|
56
|
+
|
|
57
|
+
| | sealcode | git-crypt | git-secret | sops |
|
|
58
|
+
|---|---|---|---|---|
|
|
59
|
+
| Encrypts whole files (not just diffs) | ✅ | partial | ✅ | ❌ (values only) |
|
|
60
|
+
| Filenames hidden | ✅ | ❌ | ❌ | ❌ |
|
|
61
|
+
| Repo metadata hidden (package.json, deps) | ✅ | ❌ | ❌ | ❌ |
|
|
62
|
+
| Passphrase-based (no GPG keyring) | ✅ | ❌ | ❌ | partial |
|
|
63
|
+
| Recovery code if you lose passphrase | ✅ | ❌ | ❌ | ❌ |
|
|
64
|
+
| Works on any language/stack | ✅ | ✅ | ✅ | ✅ |
|
|
65
|
+
| Auto-detects your ecosystem | ✅ | ❌ | ❌ | ❌ |
|
|
66
|
+
| Built-in drift detection | ✅ | ❌ | ❌ | ❌ |
|
|
67
|
+
|
|
68
|
+
The big one: **sealcode hides that your code exists at all**. Other tools encrypt the files but leave the folder structure, the filenames, and the `package.json` (with every telltale dependency) in plain view.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
sealcode init # one-time setup; interactive wizard
|
|
74
|
+
sealcode lock # encrypt source → vendor/ blobs (removes plaintext)
|
|
75
|
+
sealcode unlock # decrypt blobs → restore source (removes stubs)
|
|
76
|
+
sealcode verify # confirm every blob decrypts & matches its hash
|
|
77
|
+
sealcode status # show locked/unlocked + drift since last lock
|
|
78
|
+
sealcode status --check # exit 1 if unlocked with drift (used by git hook)
|
|
79
|
+
sealcode status --json # machine-readable state (editors / scripts)
|
|
80
|
+
sealcode rotate # change passphrase (blobs unchanged); env: SEALCODE_OLD_PASSPHRASE / SEALCODE_NEW_PASSPHRASE
|
|
81
|
+
sealcode backup <dir> # copy locked vault + config snapshot to a new folder
|
|
82
|
+
sealcode restore <dir> # restore from backup (use --force if locked dir exists)
|
|
83
|
+
sealcode install-hook # git pre-commit: block commits when unlocked + drift
|
|
84
|
+
sealcode uninstall-hook # remove hook block
|
|
85
|
+
sealcode panic # immediate re-lock + session wipe
|
|
86
|
+
sealcode logout # clear cached session, force passphrase next time
|
|
87
|
+
sealcode presets # list supported ecosystems
|
|
88
|
+
sealcode pro # Pro tier info
|
|
89
|
+
sealcode where # debug: print paths and state (no secrets)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Aliases: `sc`, `sealcode seal` = `lock`, `sealcode open` = `unlock`.
|
|
93
|
+
|
|
94
|
+
### Editor support
|
|
95
|
+
|
|
96
|
+
A VS Code companion lives in `tools/vaultline-vscode/` — it shows the current lock state in the status bar when the CLI is on your `PATH`. (Rename in flight; the extension itself works against either `sealcode` or `vaultline` on `PATH`.)
|
|
97
|
+
|
|
98
|
+
### CI (GitHub Actions)
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- run: npm install -g sealcode
|
|
102
|
+
- env:
|
|
103
|
+
SEALCODE_PASSPHRASE: ${{ secrets.SEALCODE_PASSPHRASE }}
|
|
104
|
+
run: |
|
|
105
|
+
sealcode unlock
|
|
106
|
+
npm install --omit=dev
|
|
107
|
+
npm run build
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Supported stacks (auto-detected)
|
|
111
|
+
|
|
112
|
+
| Marker file | Preset | Locked dir |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `package.json` (no Next config) | Node.js / TypeScript | `vendor/` |
|
|
115
|
+
| `next.config.*` | Next.js | `vendor/` |
|
|
116
|
+
| `requirements.txt` / `pyproject.toml` / `setup.py` | Python | `_site_packages/` |
|
|
117
|
+
| `manage.py` | Django | `_site_packages/` |
|
|
118
|
+
| `go.mod` | Go | `internal/sealed/` |
|
|
119
|
+
| `Cargo.toml` | Rust | `target/.cache/` |
|
|
120
|
+
| `Gemfile` | Ruby on Rails | `vendor/sealed/` |
|
|
121
|
+
| `composer.json` | PHP / Laravel | `storage/sealed/` |
|
|
122
|
+
| `pom.xml` / `build.gradle*` | Java / JVM | `.sealed/` |
|
|
123
|
+
| (none of the above) | generic | `vendor/` |
|
|
124
|
+
|
|
125
|
+
Each preset uses a `lockedDir` name that looks native to its ecosystem — Go projects don't get a suspicious folder called `locked/`, they get `internal/sealed/`. Stealth by camouflage.
|
|
126
|
+
|
|
127
|
+
## How the crypto works (the short version)
|
|
128
|
+
|
|
129
|
+
- **Algorithm:** AES-256-GCM with a random 12-byte IV per blob; gzip before encrypt.
|
|
130
|
+
- **Key derivation:** scrypt(passphrase, salt) → 32-byte wrapping key, N=2^17, r=8, p=1 (~1s, ~128MB).
|
|
131
|
+
- **Master data key:** randomly generated, stored on disk wrapped twice — once by your passphrase, once by a 128-bit recovery seed (the 30-char recovery code you wrote down at init).
|
|
132
|
+
- **File-on-disk format:** `[12B IV][16B GCM tag][ciphertext]` with no header bytes. Indistinguishable from random data.
|
|
133
|
+
- **Filenames inside `vendor/`:** HMAC-SHA256 of the path, sharded by first 2 hex chars, truncated to 12. Stable across re-locks, indistinguishable across files.
|
|
134
|
+
- **No `# CV1` or other text signatures** that would let `file(1)` / scrapers / AI identify the format.
|
|
135
|
+
|
|
136
|
+
## Where does the passphrase live?
|
|
137
|
+
|
|
138
|
+
Wherever you put it. The tool checks:
|
|
139
|
+
|
|
140
|
+
1. Cached session (~8h after last unlock — stored at `~/.sealcode/sessions/<id>`, encrypted with a host-binding key)
|
|
141
|
+
2. `SEALCODE_PASSPHRASE` env var (use in CI). `VAULTLINE_PASSPHRASE` is accepted as a fallback for projects upgrading from vaultline 1.x.
|
|
142
|
+
3. Interactive prompt
|
|
143
|
+
|
|
144
|
+
**sealcode never writes your passphrase to disk in any recoverable form.** Lose your passphrase AND your recovery code, and your code is gone. We can't help you. That's not a bug — it's the entire point.
|
|
145
|
+
|
|
146
|
+
Back up your recovery code in:
|
|
147
|
+
- a password manager (1Password / Bitwarden as a secure note)
|
|
148
|
+
- a printed sheet
|
|
149
|
+
- an encrypted USB / hardware key
|
|
150
|
+
|
|
151
|
+
## CI / deploy
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
# GitHub Actions
|
|
155
|
+
- run: npm install -g sealcode
|
|
156
|
+
- env:
|
|
157
|
+
SEALCODE_PASSPHRASE: ${{ secrets.SEALCODE_PASSPHRASE }}
|
|
158
|
+
run: |
|
|
159
|
+
sealcode unlock
|
|
160
|
+
npm install --omit=dev
|
|
161
|
+
npm run build # produces dist/main.js
|
|
162
|
+
- run: rsync dist/ user@server:/app/dist/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Your production server never sees the passphrase or the vault. It only runs the built `dist/`.
|
|
166
|
+
|
|
167
|
+
## Pro features (coming soon)
|
|
168
|
+
|
|
169
|
+
The free CLI handles lock / unlock / verify forever. **sealcode Pro** adds:
|
|
170
|
+
|
|
171
|
+
- **Team key sharing** — invite teammates, no passphrase exchange needed
|
|
172
|
+
- **Key rotation across N projects** with one command
|
|
173
|
+
- **Audit log** — see who unlocked what, when
|
|
174
|
+
- **Cloud key escrow** — passphrase recovery via account auth
|
|
175
|
+
- **Temporary access codes** — time-boxed, revocable read access to a project
|
|
176
|
+
- **CI/CD tokens** — short-lived deploy keys, no long-lived secrets in Actions
|
|
177
|
+
|
|
178
|
+
Start a 14-day trial at [sealcode.dev/pro](https://sealcode.dev/pro).
|
|
179
|
+
|
|
180
|
+
## FAQ
|
|
181
|
+
|
|
182
|
+
**Does this affect git diffs?**
|
|
183
|
+
Yes — `vendor/` blobs change every lock (random IV). For human review, diff your *local* unlocked tree before locking. A `sealcode diff <commit>` command is on the Pro roadmap.
|
|
184
|
+
|
|
185
|
+
**Can I encrypt only some files?**
|
|
186
|
+
Yes — edit `.sealcoderc.json`'s `include` array.
|
|
187
|
+
|
|
188
|
+
**Does it work on Windows?**
|
|
189
|
+
Yes. Forward-slash normalization in opaque names; tested on macOS, Linux, Windows.
|
|
190
|
+
|
|
191
|
+
**What's the perf cost?**
|
|
192
|
+
First unlock with passphrase: ~1s (scrypt). Subsequent commands within 8h: <100ms (cached session). Lock/unlock of a 1000-file repo: ~2s.
|
|
193
|
+
|
|
194
|
+
**Is this audited?**
|
|
195
|
+
Not formally. The crypto is standard primitives (AES-256-GCM, scrypt) used in their textbook configurations. Audit yourself at the repo. We'll fund a formal audit once revenue supports it.
|
|
196
|
+
|
|
197
|
+
**I had `vaultline` installed before — do I lose my vault?**
|
|
198
|
+
No. The on-disk format is unchanged. `sealcode` reads any existing `.vaultlinerc.json`, your old `~/.vaultline/sessions/` cache, and `VAULTLINE_PASSPHRASE` env var. On the next `sealcode lock` the project will start writing the new config name (`.sealcoderc.json`), but the encrypted blobs themselves never need to be re-encrypted.
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT. See [LICENSE](./LICENSE).
|
package/bin/sealcode.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { run } = require('../src/cli');
|
|
5
|
+
|
|
6
|
+
run(process.argv).catch((err) => {
|
|
7
|
+
// Last-resort error path; cli.js handles known errors with friendly output.
|
|
8
|
+
process.stderr.write((err && err.stack) || String(err));
|
|
9
|
+
process.stderr.write('\n');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sealcode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lock your source code in your own git repo. Stop AI agents, scrapers, and curious eyes from reading what's yours.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"encryption",
|
|
7
|
+
"source-code",
|
|
8
|
+
"privacy",
|
|
9
|
+
"git",
|
|
10
|
+
"ai-privacy",
|
|
11
|
+
"obfuscation",
|
|
12
|
+
"vault",
|
|
13
|
+
"secrets",
|
|
14
|
+
"code-protection",
|
|
15
|
+
"intellectual-property",
|
|
16
|
+
"encrypt",
|
|
17
|
+
"lock",
|
|
18
|
+
"seal",
|
|
19
|
+
"sealcode"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://sealcode.dev",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/sealcode/sealcode/issues"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/sealcode/sealcode.git"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"author": "sealcode",
|
|
31
|
+
"main": "src/index.js",
|
|
32
|
+
"bin": {
|
|
33
|
+
"sealcode": "bin/sealcode.js",
|
|
34
|
+
"sc": "bin/sealcode.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"bin",
|
|
38
|
+
"src",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node test/roundtrip.js",
|
|
44
|
+
"selftest": "node test/roundtrip.js"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"commander": "^12.1.0",
|
|
51
|
+
"fast-glob": "^3.3.2"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sealcode CLI ↔ web app glue.
|
|
5
|
+
*
|
|
6
|
+
* - Credentials live at ~/.sealcode/credentials.json (mode 0600).
|
|
7
|
+
* - Bearer token is sent on every authenticated request.
|
|
8
|
+
* - `apiUrl` is read from credentials, then SEALCODE_API_URL, then a default.
|
|
9
|
+
*
|
|
10
|
+
* The credentials file is intentionally minimal: it stores who is logged in
|
|
11
|
+
* (so `sealcode whoami` works offline) and the long-lived token. We never
|
|
12
|
+
* cache project lists or grants there.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const pkg = require('../package.json');
|
|
19
|
+
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.sealcode');
|
|
21
|
+
const CREDS_PATH = path.join(CONFIG_DIR, 'credentials.json');
|
|
22
|
+
const DEFAULT_API_URL = 'https://sealcode.dev';
|
|
23
|
+
|
|
24
|
+
function defaultApiUrl() {
|
|
25
|
+
return process.env.SEALCODE_API_URL || DEFAULT_API_URL;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readCreds() {
|
|
29
|
+
try {
|
|
30
|
+
if (!fs.existsSync(CREDS_PATH)) return null;
|
|
31
|
+
const raw = fs.readFileSync(CREDS_PATH, 'utf8');
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
34
|
+
return parsed;
|
|
35
|
+
} catch (_) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeCreds(creds) {
|
|
41
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
42
|
+
// Write to a temp file then rename so a partial write can't leave a corrupt
|
|
43
|
+
// credentials file behind.
|
|
44
|
+
const tmp = CREDS_PATH + '.tmp';
|
|
45
|
+
fs.writeFileSync(tmp, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
|
|
46
|
+
try {
|
|
47
|
+
fs.chmodSync(tmp, 0o600);
|
|
48
|
+
} catch (_) { /* ignore on Windows */ }
|
|
49
|
+
fs.renameSync(tmp, CREDS_PATH);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clearCreds() {
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(CREDS_PATH);
|
|
55
|
+
} catch (_) { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getApiUrl() {
|
|
59
|
+
// Env var wins so test setups can point at a local dev server without
|
|
60
|
+
// editing credentials.
|
|
61
|
+
if (process.env.SEALCODE_API_URL) return process.env.SEALCODE_API_URL;
|
|
62
|
+
const c = readCreds();
|
|
63
|
+
return (c && c.apiUrl) || DEFAULT_API_URL;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getToken() {
|
|
67
|
+
if (process.env.SEALCODE_TOKEN) return process.env.SEALCODE_TOKEN;
|
|
68
|
+
const c = readCreds();
|
|
69
|
+
return c && c.token ? c.token : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clientInfo() {
|
|
73
|
+
return {
|
|
74
|
+
hostname: os.hostname(),
|
|
75
|
+
platform: `${os.platform()}-${os.arch()}`,
|
|
76
|
+
cliVersion: pkg.version,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class ApiError extends Error {
|
|
81
|
+
constructor(status, code, message, raw) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.status = status;
|
|
84
|
+
this.apiCode = code;
|
|
85
|
+
this.raw = raw;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function request(method, urlPath, { body, auth = false, apiUrl } = {}) {
|
|
90
|
+
const base = apiUrl || getApiUrl();
|
|
91
|
+
const url = new URL(urlPath, base).toString();
|
|
92
|
+
const headers = {
|
|
93
|
+
'content-type': 'application/json',
|
|
94
|
+
'user-agent': `sealcode/${pkg.version}`,
|
|
95
|
+
accept: 'application/json',
|
|
96
|
+
};
|
|
97
|
+
if (auth) {
|
|
98
|
+
const token = getToken();
|
|
99
|
+
if (!token) {
|
|
100
|
+
throw new ApiError(
|
|
101
|
+
401,
|
|
102
|
+
'unauthenticated',
|
|
103
|
+
'You are not logged in. Run `sealcode login` first.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
headers.authorization = `Bearer ${token}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let res;
|
|
110
|
+
try {
|
|
111
|
+
res = await fetch(url, {
|
|
112
|
+
method,
|
|
113
|
+
headers,
|
|
114
|
+
body: body == null ? undefined : JSON.stringify(body),
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new ApiError(
|
|
118
|
+
0,
|
|
119
|
+
'network',
|
|
120
|
+
`Could not reach ${base}: ${err.message || err}.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const ct = res.headers.get('content-type') || '';
|
|
125
|
+
let parsed = null;
|
|
126
|
+
if (ct.includes('application/json')) {
|
|
127
|
+
try {
|
|
128
|
+
parsed = await res.json();
|
|
129
|
+
} catch {
|
|
130
|
+
parsed = null;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
try {
|
|
134
|
+
parsed = { text: await res.text() };
|
|
135
|
+
} catch {
|
|
136
|
+
parsed = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const code = (parsed && parsed.error && parsed.error.code) || 'http_error';
|
|
142
|
+
const message =
|
|
143
|
+
(parsed && parsed.error && parsed.error.message) ||
|
|
144
|
+
`${method} ${urlPath} → HTTP ${res.status}`;
|
|
145
|
+
throw new ApiError(res.status, code, message, parsed);
|
|
146
|
+
}
|
|
147
|
+
return parsed || {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
ApiError,
|
|
152
|
+
CONFIG_DIR,
|
|
153
|
+
CREDS_PATH,
|
|
154
|
+
defaultApiUrl,
|
|
155
|
+
readCreds,
|
|
156
|
+
writeCreds,
|
|
157
|
+
clearCreds,
|
|
158
|
+
getApiUrl,
|
|
159
|
+
getToken,
|
|
160
|
+
clientInfo,
|
|
161
|
+
request,
|
|
162
|
+
};
|
package/src/bundle.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Portable backup of a sealcode project's locked directory + config snapshot.
|
|
5
|
+
*
|
|
6
|
+
* `export` writes a directory:
|
|
7
|
+
* <dest>/
|
|
8
|
+
* manifest.json — metadata (sealcode version, lockedDir, paths)
|
|
9
|
+
* locked/ — recursive copy of the locked dir on disk
|
|
10
|
+
*
|
|
11
|
+
* `import` copies `locked/` from a bundle back into the project and optionally
|
|
12
|
+
* restores `.sealcoderc.json` if present in the bundle.
|
|
13
|
+
*
|
|
14
|
+
* Backward-compat: bundles produced by vaultline 1.x carry a
|
|
15
|
+
* `vaultlinerc.json.bak` snapshot under the same scheme — we read that too
|
|
16
|
+
* and restore it as `.sealcoderc.json` on import.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const pkg = require('../package.json');
|
|
22
|
+
const { CONFIG_FILE, LEGACY_CONFIG_FILE } = require('./config');
|
|
23
|
+
|
|
24
|
+
const CONFIG_BAK_NAME = 'sealcoderc.json.bak';
|
|
25
|
+
const LEGACY_CONFIG_BAK_NAME = 'vaultlinerc.json.bak';
|
|
26
|
+
|
|
27
|
+
function copyDirRecursive(src, dest) {
|
|
28
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
29
|
+
for (const name of fs.readdirSync(src)) {
|
|
30
|
+
const s = path.join(src, name);
|
|
31
|
+
const d = path.join(dest, name);
|
|
32
|
+
const st = fs.statSync(s);
|
|
33
|
+
if (st.isDirectory()) copyDirRecursive(s, d);
|
|
34
|
+
else fs.copyFileSync(s, d);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rmDir(dir) {
|
|
39
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} projectRoot
|
|
44
|
+
* @param {object} config — active config with lockedDir
|
|
45
|
+
* @param {string} dest — path to output folder (created)
|
|
46
|
+
*/
|
|
47
|
+
async function exportBundle(projectRoot, config, dest) {
|
|
48
|
+
const absDest = path.resolve(dest);
|
|
49
|
+
const lockedDir = config.lockedDir;
|
|
50
|
+
const lockedSrc = path.join(projectRoot, lockedDir);
|
|
51
|
+
if (!fs.existsSync(lockedSrc)) {
|
|
52
|
+
throw new Error(`sealcode: locked directory not found: ${lockedSrc}`);
|
|
53
|
+
}
|
|
54
|
+
if (fs.existsSync(absDest)) {
|
|
55
|
+
throw new Error(`sealcode: destination already exists: ${absDest}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.mkdirSync(absDest, { recursive: true });
|
|
59
|
+
const bundleLocked = path.join(absDest, 'locked');
|
|
60
|
+
copyDirRecursive(lockedSrc, bundleLocked);
|
|
61
|
+
|
|
62
|
+
// Prefer the new config; fall back to a legacy file from a not-yet-migrated
|
|
63
|
+
// project so a backup taken right after install still captures the config.
|
|
64
|
+
let cfgPath = path.join(projectRoot, CONFIG_FILE);
|
|
65
|
+
if (!fs.existsSync(cfgPath)) cfgPath = path.join(projectRoot, LEGACY_CONFIG_FILE);
|
|
66
|
+
|
|
67
|
+
let configSnapshot = null;
|
|
68
|
+
if (fs.existsSync(cfgPath)) {
|
|
69
|
+
configSnapshot = fs.readFileSync(cfgPath, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const meta = {
|
|
73
|
+
v: 1,
|
|
74
|
+
sealcode: pkg.version,
|
|
75
|
+
lockedDir,
|
|
76
|
+
createdAt: new Date().toISOString(),
|
|
77
|
+
hasSealcoderc: configSnapshot != null,
|
|
78
|
+
};
|
|
79
|
+
fs.writeFileSync(path.join(absDest, 'manifest.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
|
80
|
+
if (configSnapshot) {
|
|
81
|
+
fs.writeFileSync(path.join(absDest, CONFIG_BAK_NAME), configSnapshot, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { path: absDest, meta };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} projectRoot
|
|
89
|
+
* @param {string} bundlePath — folder produced by exportBundle
|
|
90
|
+
* @param {{ force?: boolean }} opts
|
|
91
|
+
*/
|
|
92
|
+
async function importBundle(projectRoot, bundlePath, opts = {}) {
|
|
93
|
+
const abs = path.resolve(bundlePath);
|
|
94
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
95
|
+
throw new Error(`sealcode: bundle not found: ${abs}`);
|
|
96
|
+
}
|
|
97
|
+
const manPath = path.join(abs, 'manifest.json');
|
|
98
|
+
if (!fs.existsSync(manPath)) {
|
|
99
|
+
throw new Error('sealcode: bundle missing manifest.json');
|
|
100
|
+
}
|
|
101
|
+
const meta = JSON.parse(fs.readFileSync(manPath, 'utf8'));
|
|
102
|
+
const lockedDir = meta.lockedDir;
|
|
103
|
+
if (!lockedDir || typeof lockedDir !== 'string') {
|
|
104
|
+
throw new Error('sealcode: bundle manifest missing lockedDir');
|
|
105
|
+
}
|
|
106
|
+
const bundleLocked = path.join(abs, 'locked');
|
|
107
|
+
if (!fs.existsSync(bundleLocked)) {
|
|
108
|
+
throw new Error('sealcode: bundle missing locked/ directory');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const destLocked = path.join(projectRoot, lockedDir);
|
|
112
|
+
if (fs.existsSync(destLocked)) {
|
|
113
|
+
if (!opts.force) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`sealcode: ${destLocked} already exists. Pass --force to replace (DANGER).`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
rmDir(destLocked);
|
|
119
|
+
}
|
|
120
|
+
copyDirRecursive(bundleLocked, destLocked);
|
|
121
|
+
|
|
122
|
+
// Prefer the new bak name; fall back to the legacy one. Either way we
|
|
123
|
+
// restore as the new .sealcoderc.json on the project side.
|
|
124
|
+
let bak = path.join(abs, CONFIG_BAK_NAME);
|
|
125
|
+
if (!fs.existsSync(bak)) bak = path.join(abs, LEGACY_CONFIG_BAK_NAME);
|
|
126
|
+
if (fs.existsSync(bak)) {
|
|
127
|
+
fs.copyFileSync(bak, path.join(projectRoot, CONFIG_FILE));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { lockedDir, meta };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { exportBundle, importBundle };
|