@helios-lang/contract-utils 0.3.21 → 0.3.22
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/README.md +46 -0
- package/package.json +9 -4
- package/src/artifacts/ChildArtifactWriter.js +18 -0
- package/src/artifacts/RootArtifact.js +8 -0
- package/src/cli/compile.js +211 -0
- package/src/cli/config.mjs +158 -0
- package/src/cli/login.mjs +117 -0
- package/src/cli/project.mjs +67 -0
- package/src/codegen/LoadedScriptsWriter.js +20 -3
- package/src/context/ContractContextBuilder.js +34 -1
- package/src/context/debugger.js +46 -0
- package/src/helios.mjs +47 -0
- package/src/hl2ts.js +5 -185
- package/src/index.js +10 -1
- package/types/artifacts/ChildArtifactWriter.d.ts.map +1 -1
- package/types/artifacts/RootArtifact.d.ts.map +1 -1
- package/types/cli/compile.d.ts +9 -0
- package/types/cli/compile.d.ts.map +1 -0
- package/types/cli/config.d.mts +4 -0
- package/types/cli/config.d.mts.map +1 -0
- package/types/cli/project.d.mts +11 -0
- package/types/cli/project.d.mts.map +1 -0
- package/types/codegen/LoadedScriptsWriter.d.ts +2 -1
- package/types/codegen/LoadedScriptsWriter.d.ts.map +1 -1
- package/types/context/ContractContextBuilder.d.ts.map +1 -1
- package/types/context/debugger.d.ts +9 -0
- package/types/context/debugger.d.ts.map +1 -0
- package/types/hl2ts.d.ts +1 -6
- package/types/hl2ts.d.ts.map +1 -1
- package/types/index.d.ts +18 -1
- package/types/index.d.ts.map +1 -1
- package/types/cast/PermissiveType.d.ts +0 -4
- package/types/cast/PermissiveType.d.ts.map +0 -1
- package/types/cast/StrictType.d.ts +0 -4
- package/types/cast/StrictType.d.ts.map +0 -1
- package/types/codegen/LoadedModule.d.ts +0 -17
- package/types/codegen/LoadedModule.d.ts.map +0 -1
- package/types/codegen/LoadedValidator.d.ts +0 -38
- package/types/codegen/LoadedValidator.d.ts.map +0 -1
- package/types/codegen/Module.d.ts +0 -24
- package/types/codegen/Module.d.ts.map +0 -1
- package/types/codegen/TypeCheckedModule.d.ts +0 -24
- package/types/codegen/TypeCheckedModule.d.ts.map +0 -1
- package/types/codegen/TypeCheckedValidator.d.ts +0 -9
- package/types/codegen/TypeCheckedValidator.d.ts.map +0 -1
- package/types/codegen/TypescriptWriter.d.ts +0 -50
- package/types/codegen/TypescriptWriter.d.ts.map +0 -1
- package/types/codegen/Validator.d.ts +0 -22
- package/types/codegen/Validator.d.ts.map +0 -1
- package/types/codegen/Writer.d.ts +0 -23
- package/types/codegen/Writer.d.ts.map +0 -1
- package/types/context/ContractContext.d.ts +0 -53
- package/types/context/ContractContext.d.ts.map +0 -1
- package/types/context/writeContractContextArtifacts.d.ts +0 -30
- package/types/context/writeContractContextArtifacts.d.ts.map +0 -1
- package/types/lib/Lib.d.ts +0 -41
- package/types/lib/Lib.d.ts.map +0 -1
- package/types/lib/Libg_v0_16.d.ts +0 -256
- package/types/lib/Libg_v0_16.d.ts.map +0 -1
- package/types/lib/index.d.ts +0 -3
- package/types/lib/index.d.ts.map +0 -1
- package/types/lib/load.d.ts +0 -6
- package/types/lib/load.d.ts.map +0 -1
- package/types/runtime/Cast.d.ts +0 -32
- package/types/runtime/Cast.d.ts.map +0 -1
- package/types/runtime/ContractContext.d.ts +0 -23
- package/types/runtime/ContractContext.d.ts.map +0 -1
- package/types/runtime/ContractContextBuilder.d.ts +0 -53
- package/types/runtime/ContractContextBuilder.d.ts.map +0 -1
- package/types/runtime/DagCompiler.d.ts +0 -64
- package/types/runtime/DagCompiler.d.ts.map +0 -1
- package/types/runtime/PermissiveType.d.ts +0 -3
- package/types/runtime/PermissiveType.d.ts.map +0 -1
- package/types/runtime/StrictType.d.ts +0 -3
- package/types/runtime/StrictType.d.ts.map +0 -1
- package/types/runtime/index.d.ts +0 -2
- package/types/runtime/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,2 +1,48 @@
|
|
|
1
1
|
# contract-utils
|
|
2
2
|
Convenience and type-safety utilities for using Helios validators from within Typescript
|
|
3
|
+
|
|
4
|
+
## Helios CLI
|
|
5
|
+
|
|
6
|
+
Requires Node.js 22 or newer. Install globally with npm:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install -g @helios-lang/contract-utils
|
|
10
|
+
helios --help
|
|
11
|
+
helios compile --format typescript --out-dir ./generated
|
|
12
|
+
helios login
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`helios compile` and `hl2ts` share the same compiler and options. Compilation searches the current directory for `.hl` files and does not require login. `helios compile --help` lists compiler options. The compiler peer dependency is restricted to the supported 0.16/0.17 series.
|
|
16
|
+
|
|
17
|
+
`helios login` opens `https://helios-lang.io/console` with an expiring login request. Connect your wallet, create a project if you have no active projects, compare the code with your terminal, and click **Authorize CLI**. All active project API keys for that wallet are installed locally. The browser displays **You can now close this tab** only after the CLI confirms that the file was saved. Use `helios login --no-browser` to open the printed URL manually, including from another computer. Login expires after ten minutes; cancellation or failure leaves the existing file intact unless it was already saved and only the acknowledgment failed (reported explicitly).
|
|
18
|
+
|
|
19
|
+
Credentials are stored in a versioned `debugger.json` file:
|
|
20
|
+
|
|
21
|
+
| OS | Directory |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Linux | `$XDG_CONFIG_HOME/helios`, or `~/.config/helios` |
|
|
24
|
+
| macOS | `~/Library/Application Support/Helios` |
|
|
25
|
+
| Windows | `%APPDATA%\Helios` |
|
|
26
|
+
|
|
27
|
+
Set `HELIOS_CONFIG_HOME` to override the directory. Unix directories/files use modes 700/600. Windows uses a current-user-only ACL. The JSON has `{version:1,selected,profiles}`, where `selected` is a project ID and `profiles` maps project IDs to `{id,name,created_at,apiKey,wallet,endpoint}`. Names need not be unique. Re-running login refreshes that wallet's snapshot and removes revoked projects while preserving other wallets. It keeps the selected project when available and otherwise selects the oldest returned project. No signatures, browser session cookies, or temporary login tokens are persisted.
|
|
28
|
+
|
|
29
|
+
API keys in this file are secrets: do not commit or share it. They authorize both capture uploads and reads for their project. Login never prints key values. The console's ordinary metadata endpoints do not return them; key recovery uses an explicitly approved, short-lived CLI request.
|
|
30
|
+
|
|
31
|
+
For development, run `pnpm test`. `node scripts/check-global-cli.mjs` packs the package, installs it into an isolated npm global prefix, and checks executable discovery and equivalent compilation. The CLI portability workflow runs these checks on Windows, macOS, and Linux with Node 22 and 24. Publish the package only after the website and Worker with CLI-login support have been deployed.
|
|
32
|
+
|
|
33
|
+
### Compile for a debugger project
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
helios login
|
|
37
|
+
helios compile --project "My project" --out-dir ./generated
|
|
38
|
+
# The compatibility command accepts the same option:
|
|
39
|
+
hl2ts --project "My project" --out-dir ./generated
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--project` selects an exact configured project name and validates its key through `GET /v1/project` before writing output. Unknown names list all configured project names/IDs; an empty configuration explains how to run `helios login`. Duplicate names are rejected as ambiguous. Revoked keys, stale project metadata, and service outages stop compilation rather than generating a bundle with an unchecked key.
|
|
43
|
+
|
|
44
|
+
Generated JS/TS exports `$debugger: {projectId,name,apiKey,endpoint,sources}` and attaches it to each loaded validator/module. `makeContractContextBuilder()` carries that context onto the UPLC programs used by each validator's `$hash.context.program`, including source maps and unoptimized companions. Binding happens after cache lookup: changing projects does not reuse another project's credentials, and compilation caches remain credential-free. Written contract artifacts also preserve the context. Script CBOR and hashes are unchanged.
|
|
45
|
+
|
|
46
|
+
With the corresponding tx-utils release and ledger evaluation-observer support, `spendWithRedeemer()`, minting/staking methods, and attached programs automatically deliver failed builds to that project. No explicit `makeDebuggerService()` call is needed. `makeTxBuilder({isMainnet, debugger:false})` disables automatic capture; an explicit debugger service overrides bundle configuration. Unannotated bundles retain their existing behavior.
|
|
47
|
+
|
|
48
|
+
The API key is intentionally embedded in generated bundles, including browser bundles, and grants upload **and read** access to that project's feed. Use project-specific keys appropriate for that distribution. The key is sent in the authorization header, not in capture payloads. Omitting `--project` embeds no debugger credentials.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helios-lang/contract-utils",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"description": "Convenience and type-safety utilities for using Helios validators from within Typescript",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
19
|
"bin": {
|
|
20
|
-
"hl2ts": "src/hl2ts.js"
|
|
20
|
+
"hl2ts": "src/hl2ts.js",
|
|
21
|
+
"helios": "src/helios.mjs"
|
|
21
22
|
},
|
|
22
23
|
"author": "Christian Schmitz",
|
|
23
24
|
"license": "BSD-3-Clause",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
"singleQuote": false
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
|
-
"@helios-lang/compiler": "
|
|
37
|
+
"@helios-lang/compiler": "^0.16.0 || ^0.17.0"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@helios-lang/cli-utils": "^0.1.10",
|
|
@@ -42,7 +43,11 @@
|
|
|
42
43
|
"@helios-lang/crypto": "0.2.3",
|
|
43
44
|
"@helios-lang/ledger": "^0.7.8",
|
|
44
45
|
"@helios-lang/type-utils": "^0.3.0",
|
|
45
|
-
"@helios-lang/uplc": "^0.7.15"
|
|
46
|
+
"@helios-lang/uplc": "^0.7.15",
|
|
47
|
+
"open": "^10.2.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
46
51
|
},
|
|
47
52
|
"scripts": {
|
|
48
53
|
"build": "pnpm run prettify && pnpm run build:types && pnpm run test:suite",
|
|
@@ -99,6 +99,24 @@ export class ChildArtifactWriter extends ArtifactWriter {
|
|
|
99
99
|
throw new Error("unhandled Plutus version")
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const debuggerContext =
|
|
103
|
+
/** @type {UplcProgram & {$debugger?: import("../index.js").DebuggerContext}} */ (
|
|
104
|
+
program
|
|
105
|
+
).$debugger
|
|
106
|
+
if (debuggerContext && includeDef) {
|
|
107
|
+
this.writeDefLine(
|
|
108
|
+
`export const $debugger = ${JSON.stringify(debuggerContext)}`
|
|
109
|
+
)
|
|
110
|
+
this.writeDeclLine(
|
|
111
|
+
`export declare const $debugger: import("@helios-lang/contract-utils").DebuggerContext`
|
|
112
|
+
)
|
|
113
|
+
this.writeDefLine(
|
|
114
|
+
`Object.defineProperty(${name}, "$debugger", {value:$debugger})`
|
|
115
|
+
)
|
|
116
|
+
this.writeDefLine(
|
|
117
|
+
`if (${name}.alt) Object.defineProperty(${name}.alt, "$debugger", {value:$debugger})`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
102
120
|
return this
|
|
103
121
|
}
|
|
104
122
|
}
|
|
@@ -43,6 +43,14 @@ export function writeContractContextArtifacts(context, options) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
if (context.$debugger) {
|
|
47
|
+
root.writeDefLine(
|
|
48
|
+
`export const $debugger = ${JSON.stringify(context.$debugger)}`
|
|
49
|
+
)
|
|
50
|
+
root.writeDeclLine(
|
|
51
|
+
`export declare const $debugger: import("@helios-lang/contract-utils").DebuggerContext`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
46
54
|
root.writeValidatorNameUtils(validatorNames)
|
|
47
55
|
root.save()
|
|
48
56
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
promises,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync
|
|
7
|
+
} from "node:fs"
|
|
8
|
+
import { dirname, join, resolve } from "node:path"
|
|
9
|
+
import { Cli, EnumOpt, StringOpt } from "@helios-lang/cli-utils"
|
|
10
|
+
import { makeLoadedScriptsWriter } from "../codegen/index.js"
|
|
11
|
+
import { loadCompilerLib } from "../compiler/index.js"
|
|
12
|
+
import { typeCheckFiles } from "../compiler/ops.js"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* name: string
|
|
17
|
+
* purpose: string
|
|
18
|
+
* sourceCode: string
|
|
19
|
+
* }} SourceDetails
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {"javascript" | "typescript"} FormatKind
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const NAME = "hl2ts"
|
|
27
|
+
|
|
28
|
+
/** @param {string[]} args */
|
|
29
|
+
export async function compile(args) {
|
|
30
|
+
const projectIndex = args.indexOf("--project")
|
|
31
|
+
if (projectIndex >= 0 && !args[projectIndex + 1]?.trim())
|
|
32
|
+
throw new Error(
|
|
33
|
+
"--project requires a project name; run helios login to configure projects"
|
|
34
|
+
)
|
|
35
|
+
const cli = new Cli(NAME, {
|
|
36
|
+
minArgs: 0,
|
|
37
|
+
maxArgs: 0,
|
|
38
|
+
options: {
|
|
39
|
+
project: new StringOpt({
|
|
40
|
+
long: "--project",
|
|
41
|
+
description: "Debugger project name (from helios login)",
|
|
42
|
+
default: ""
|
|
43
|
+
}),
|
|
44
|
+
outDir: new StringOpt({
|
|
45
|
+
long: "--out-dir",
|
|
46
|
+
short: "-o",
|
|
47
|
+
default: () => process.cwd()
|
|
48
|
+
}),
|
|
49
|
+
format: new EnumOpt({
|
|
50
|
+
long: "--format",
|
|
51
|
+
short: "-f",
|
|
52
|
+
variants: /** @type {FormatKind[]} */ ([
|
|
53
|
+
"javascript",
|
|
54
|
+
"typescript"
|
|
55
|
+
]),
|
|
56
|
+
default: () => {
|
|
57
|
+
const path = findPackageJson()
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
path &&
|
|
61
|
+
existsSync(join(dirname(path), "tsconfig.json"))
|
|
62
|
+
) {
|
|
63
|
+
return "typescript"
|
|
64
|
+
} else {
|
|
65
|
+
return "javascript"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
action: mainInternal
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await cli.run(["node", "hl2ts", ...args])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string[]} _args
|
|
78
|
+
* @param {{outDir: string, format: FormatKind, project: string}} options
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
async function mainInternal(_args, { outDir, format, project }) {
|
|
82
|
+
const debugProject = project
|
|
83
|
+
? await (await import("./project.mjs")).resolveProject(project)
|
|
84
|
+
: undefined
|
|
85
|
+
const lib = loadCompilerLib()
|
|
86
|
+
|
|
87
|
+
const filePaths = await listFiles(process.cwd(), ".hl")
|
|
88
|
+
const files = readFiles(filePaths)
|
|
89
|
+
|
|
90
|
+
console.log(`${NAME} using Helios compiler v${lib.version}`)
|
|
91
|
+
console.log(` Transpiling ${Object.keys(files).length} inputs:`)
|
|
92
|
+
filePaths.forEach((p) => console.log(` ${p}`))
|
|
93
|
+
|
|
94
|
+
const { modules, validators } = typeCheckFiles(lib, files, (current, rel) =>
|
|
95
|
+
resolve(join(dirname(current), rel))
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const sources = debugProject
|
|
99
|
+
? {
|
|
100
|
+
...files,
|
|
101
|
+
...Object.fromEntries(
|
|
102
|
+
[...Object.values(modules), ...Object.values(validators)].map(
|
|
103
|
+
(m) => [m.name, m.sourceCode]
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
: undefined
|
|
108
|
+
const [js, dts, ts] = makeLoadedScriptsWriter(
|
|
109
|
+
debugProject ? { ...debugProject, sources } : undefined
|
|
110
|
+
)
|
|
111
|
+
.writeModules(modules)
|
|
112
|
+
.writeValidators(validators)
|
|
113
|
+
.finalize()
|
|
114
|
+
|
|
115
|
+
if (format == "typescript") {
|
|
116
|
+
mkdirIfNotExists(resolve(outDir))
|
|
117
|
+
const tsPath = join(outDir, "index.ts")
|
|
118
|
+
|
|
119
|
+
writeFileSync(tsPath, ts)
|
|
120
|
+
|
|
121
|
+
console.log(` Output:`)
|
|
122
|
+
console.log(` ${tsPath}`)
|
|
123
|
+
} else {
|
|
124
|
+
const resolvedOutDir = resolve(outDir)
|
|
125
|
+
|
|
126
|
+
mkdirIfNotExists(resolvedOutDir)
|
|
127
|
+
|
|
128
|
+
const jsPath = join(resolvedOutDir, "index.js")
|
|
129
|
+
const dtsPath = join(resolvedOutDir, "index.d.ts")
|
|
130
|
+
|
|
131
|
+
writeFileSync(jsPath, js)
|
|
132
|
+
writeFileSync(dtsPath, dts)
|
|
133
|
+
|
|
134
|
+
console.log(` Output:`)
|
|
135
|
+
console.log(` ${jsPath}`)
|
|
136
|
+
console.log(` ${dtsPath}`)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} dir
|
|
142
|
+
* @param {string} ext
|
|
143
|
+
* @returns {Promise<string[]>}
|
|
144
|
+
*/
|
|
145
|
+
async function listFiles(dir, ext) {
|
|
146
|
+
const entries = await promises.readdir(dir, { withFileTypes: true })
|
|
147
|
+
|
|
148
|
+
const files = await Promise.all(
|
|
149
|
+
entries.map((entry) => {
|
|
150
|
+
const res = resolve(dir, entry.name)
|
|
151
|
+
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
if (entry.name.endsWith("node_modules")) {
|
|
154
|
+
return []
|
|
155
|
+
} else {
|
|
156
|
+
return listFiles(res, ext)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
return res
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return files.flat().filter((name) => name.endsWith(ext))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string[]} filePaths
|
|
169
|
+
* @returns {{[path: string]: string}}
|
|
170
|
+
*/
|
|
171
|
+
function readFiles(filePaths) {
|
|
172
|
+
return Object.fromEntries(
|
|
173
|
+
filePaths.map((f) => {
|
|
174
|
+
return [f, readFileSync(f).toString()]
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} fileName
|
|
181
|
+
* @returns {string | undefined}
|
|
182
|
+
*/
|
|
183
|
+
function findPackageJson(fileName = "package.json") {
|
|
184
|
+
let dir = process.cwd()
|
|
185
|
+
|
|
186
|
+
let path = join(dir, fileName)
|
|
187
|
+
let found = existsSync(path)
|
|
188
|
+
|
|
189
|
+
while (!found) {
|
|
190
|
+
const parent = dirname(dir)
|
|
191
|
+
if (parent === dir) return undefined
|
|
192
|
+
dir = parent
|
|
193
|
+
path = join(dir, fileName)
|
|
194
|
+
found = existsSync(path)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (found) {
|
|
198
|
+
return path
|
|
199
|
+
} else {
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {string} dir
|
|
206
|
+
*/
|
|
207
|
+
function mkdirIfNotExists(dir) {
|
|
208
|
+
if (!existsSync(dir)) {
|
|
209
|
+
mkdirSync(dir, { recursive: true })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import { join, dirname, win32, posix } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
readFile,
|
|
7
|
+
writeFile,
|
|
8
|
+
rename,
|
|
9
|
+
chmod,
|
|
10
|
+
unlink
|
|
11
|
+
} from "node:fs/promises"
|
|
12
|
+
import { randomUUID } from "node:crypto"
|
|
13
|
+
import { execFile } from "node:child_process"
|
|
14
|
+
import { promisify } from "node:util"
|
|
15
|
+
const exec = promisify(execFile)
|
|
16
|
+
async function windowsAcl(path, directory, set = false) {
|
|
17
|
+
// Pass paths through the environment, never interpolate them into PowerShell.
|
|
18
|
+
const script = `
|
|
19
|
+
$ErrorActionPreference = 'Stop'
|
|
20
|
+
$path = $env:HELIOS_CONFIG_ACL_PATH
|
|
21
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
22
|
+
if (${set ? "$true" : "$false"}) {
|
|
23
|
+
$acl = New-Object System.Security.AccessControl.${directory ? "DirectorySecurity" : "FileSecurity"}
|
|
24
|
+
$acl.SetOwner($sid)
|
|
25
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
26
|
+
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', '${directory ? "ContainerInherit, ObjectInherit" : "None"}', 'None', 'Allow')
|
|
27
|
+
$acl.AddAccessRule($rule)
|
|
28
|
+
Set-Acl -LiteralPath $path -AclObject $acl
|
|
29
|
+
} else {
|
|
30
|
+
$acl = Get-Acl -LiteralPath $path
|
|
31
|
+
if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Configuration must be owned by the current user' }
|
|
32
|
+
foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
|
|
33
|
+
if ($rule.AccessControlType -eq 'Allow' -and $rule.IdentityReference.Value -ne $sid.Value) { throw 'Configuration must be accessible only by the current user' }
|
|
34
|
+
}
|
|
35
|
+
}`
|
|
36
|
+
await exec(
|
|
37
|
+
"powershell.exe",
|
|
38
|
+
[
|
|
39
|
+
"-NoProfile",
|
|
40
|
+
"-NonInteractive",
|
|
41
|
+
"-EncodedCommand",
|
|
42
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
43
|
+
],
|
|
44
|
+
{
|
|
45
|
+
windowsHide: true,
|
|
46
|
+
env: { ...process.env, HELIOS_CONFIG_ACL_PATH: path }
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
export function configPath(
|
|
51
|
+
platform = process.platform,
|
|
52
|
+
env = process.env,
|
|
53
|
+
home = homedir()
|
|
54
|
+
) {
|
|
55
|
+
const join = platform === "win32" ? win32.join : posix.join
|
|
56
|
+
const directory =
|
|
57
|
+
env.HELIOS_CONFIG_HOME ||
|
|
58
|
+
(platform === "win32"
|
|
59
|
+
? join(env.APPDATA || join(home, "AppData", "Roaming"), "Helios")
|
|
60
|
+
: platform === "darwin"
|
|
61
|
+
? join(home, "Library", "Application Support", "Helios")
|
|
62
|
+
: join(env.XDG_CONFIG_HOME || join(home, ".config"), "helios"))
|
|
63
|
+
return join(directory, "debugger.json")
|
|
64
|
+
}
|
|
65
|
+
async function inspect(path, directory = false) {
|
|
66
|
+
try {
|
|
67
|
+
const stat = await lstat(path)
|
|
68
|
+
if (
|
|
69
|
+
stat.isSymbolicLink() ||
|
|
70
|
+
!(directory ? stat.isDirectory() : stat.isFile())
|
|
71
|
+
)
|
|
72
|
+
throw new Error(`Unsafe configuration path: ${path}`)
|
|
73
|
+
if (
|
|
74
|
+
process.platform !== "win32" &&
|
|
75
|
+
(stat.mode & 0o077 || stat.uid !== process.getuid?.())
|
|
76
|
+
)
|
|
77
|
+
throw new Error(`Configuration must be owner-only: ${path}`)
|
|
78
|
+
if (process.platform === "win32") await windowsAcl(path, directory)
|
|
79
|
+
return true
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.code === "ENOENT") return false
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function readConfig(path = configPath()) {
|
|
86
|
+
await inspect(dirname(path), true)
|
|
87
|
+
if (!(await inspect(path)))
|
|
88
|
+
return { version: 1, selected: null, profiles: {} }
|
|
89
|
+
const data = JSON.parse(await readFile(path, "utf8"))
|
|
90
|
+
if (
|
|
91
|
+
data.version !== 1 ||
|
|
92
|
+
!data.profiles ||
|
|
93
|
+
typeof data.profiles !== "object" ||
|
|
94
|
+
Array.isArray(data.profiles)
|
|
95
|
+
)
|
|
96
|
+
throw new Error("Unsupported or invalid Helios configuration")
|
|
97
|
+
return data
|
|
98
|
+
}
|
|
99
|
+
export async function installProjects(
|
|
100
|
+
wallet,
|
|
101
|
+
projects,
|
|
102
|
+
endpoint,
|
|
103
|
+
path = configPath()
|
|
104
|
+
) {
|
|
105
|
+
if (
|
|
106
|
+
!/^[a-f0-9]{56}$/.test(wallet) ||
|
|
107
|
+
!Array.isArray(projects) ||
|
|
108
|
+
!projects.length
|
|
109
|
+
)
|
|
110
|
+
throw new Error("Invalid or empty project response")
|
|
111
|
+
const ids = new Set()
|
|
112
|
+
for (const p of projects) {
|
|
113
|
+
if (
|
|
114
|
+
!/^[a-f0-9-]{36}$/.test(p.id) ||
|
|
115
|
+
ids.has(p.id) ||
|
|
116
|
+
typeof p.name !== "string" ||
|
|
117
|
+
!p.name.trim() ||
|
|
118
|
+
!Number.isSafeInteger(p.created_at) ||
|
|
119
|
+
p.created_at < 0 ||
|
|
120
|
+
!/^hdbg_[a-f0-9]{64}$/.test(p.apiKey)
|
|
121
|
+
)
|
|
122
|
+
throw new Error("Invalid project response")
|
|
123
|
+
ids.add(p.id)
|
|
124
|
+
}
|
|
125
|
+
const data = await readConfig(path)
|
|
126
|
+
for (const [id, profile] of Object.entries(data.profiles))
|
|
127
|
+
if (profile.wallet === wallet && profile.endpoint === endpoint)
|
|
128
|
+
delete data.profiles[id]
|
|
129
|
+
for (const project of projects)
|
|
130
|
+
data.profiles[project.id] = { ...project, wallet, endpoint }
|
|
131
|
+
if (!data.profiles[data.selected])
|
|
132
|
+
data.selected = [...projects].sort(
|
|
133
|
+
(a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id)
|
|
134
|
+
)[0].id
|
|
135
|
+
const dir = dirname(path)
|
|
136
|
+
const existed = await inspect(dir, true)
|
|
137
|
+
await mkdir(dir, { recursive: true, mode: 0o700 })
|
|
138
|
+
if (!existed && process.platform === "win32")
|
|
139
|
+
await windowsAcl(dir, true, true)
|
|
140
|
+
await inspect(dir, true)
|
|
141
|
+
const temporary = join(dir, `.debugger-${randomUUID()}.tmp`)
|
|
142
|
+
try {
|
|
143
|
+
await writeFile(temporary, JSON.stringify(data, null, 2) + "\n", {
|
|
144
|
+
flag: "wx",
|
|
145
|
+
mode: 0o600
|
|
146
|
+
})
|
|
147
|
+
if (process.platform === "win32")
|
|
148
|
+
await windowsAcl(temporary, false, true)
|
|
149
|
+
else await chmod(temporary, 0o600)
|
|
150
|
+
await inspect(path)
|
|
151
|
+
await rename(temporary, path)
|
|
152
|
+
} finally {
|
|
153
|
+
await unlink(temporary).catch((error) => {
|
|
154
|
+
if (error.code !== "ENOENT") throw error
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
return path
|
|
158
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { installProjects } from "./config.mjs"
|
|
2
|
+
const sleep = (ms, signal) =>
|
|
3
|
+
new Promise((resolve, reject) => {
|
|
4
|
+
const onAbort = () => {
|
|
5
|
+
clearTimeout(timer)
|
|
6
|
+
reject(signal.reason)
|
|
7
|
+
}
|
|
8
|
+
const timer = setTimeout(() => {
|
|
9
|
+
signal.removeEventListener("abort", onAbort)
|
|
10
|
+
resolve()
|
|
11
|
+
}, ms)
|
|
12
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
13
|
+
if (signal.aborted) onAbort()
|
|
14
|
+
})
|
|
15
|
+
export async function login({
|
|
16
|
+
noBrowser = false,
|
|
17
|
+
endpoint = "https://debugger.helios-lang.io",
|
|
18
|
+
fetchImpl = fetch,
|
|
19
|
+
openBrowser,
|
|
20
|
+
save = installProjects,
|
|
21
|
+
log = console.log,
|
|
22
|
+
wait = sleep,
|
|
23
|
+
signal = AbortSignal.timeout(600000)
|
|
24
|
+
} = {}) {
|
|
25
|
+
async function request(path, method = "GET", token) {
|
|
26
|
+
const response = await fetchImpl(endpoint + "/v1/" + path, {
|
|
27
|
+
method,
|
|
28
|
+
redirect: "error",
|
|
29
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]),
|
|
30
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
|
31
|
+
})
|
|
32
|
+
const data = await response.json()
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
throw Object.assign(
|
|
35
|
+
new Error(data.error || `HTTP ${response.status}`),
|
|
36
|
+
{ status: response.status }
|
|
37
|
+
)
|
|
38
|
+
return data
|
|
39
|
+
}
|
|
40
|
+
const flow = await request("cli/logins", "POST")
|
|
41
|
+
if (
|
|
42
|
+
!/^[a-f0-9-]{36}$/.test(flow.id) ||
|
|
43
|
+
!/^hcli_[a-f0-9]{64}$/.test(flow.token) ||
|
|
44
|
+
!/^[A-F0-9]{8}$/.test(flow.code) ||
|
|
45
|
+
!Number.isFinite(flow.expires)
|
|
46
|
+
)
|
|
47
|
+
throw new Error("Invalid login response")
|
|
48
|
+
const url = new URL(flow.verificationUri)
|
|
49
|
+
if (
|
|
50
|
+
url.origin !== "https://helios-lang.io" ||
|
|
51
|
+
url.pathname !== "/console" ||
|
|
52
|
+
url.searchParams.get("cli_login") !== flow.id
|
|
53
|
+
)
|
|
54
|
+
throw new Error("Invalid Console login URL")
|
|
55
|
+
log(
|
|
56
|
+
`Open ${url.href}\nVerify code: ${flow.code}\nConnect your wallet, then authorize the CLI.`
|
|
57
|
+
)
|
|
58
|
+
if (!noBrowser) {
|
|
59
|
+
try {
|
|
60
|
+
await (openBrowser ?? (await import("open")).default)(url.href)
|
|
61
|
+
} catch {
|
|
62
|
+
log("Could not open your browser. Open the URL above manually.")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let failures = 0
|
|
66
|
+
while (!signal.aborted && Date.now() / 1000 < flow.expires) {
|
|
67
|
+
let result
|
|
68
|
+
try {
|
|
69
|
+
result = await request(`cli/logins/${flow.id}`, "GET", flow.token)
|
|
70
|
+
failures = 0
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (
|
|
73
|
+
signal.aborted ||
|
|
74
|
+
(error.status && error.status < 500 && error.status !== 429) ||
|
|
75
|
+
++failures > 5
|
|
76
|
+
)
|
|
77
|
+
throw error
|
|
78
|
+
await wait(Math.min(5000 * 2 ** failures, 30000), signal)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (result.state === "approved") {
|
|
82
|
+
const path = await save(result.wallet, result.projects, endpoint)
|
|
83
|
+
for (let attempt = 0; ; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
await request(
|
|
86
|
+
`cli/logins/${flow.id}/complete`,
|
|
87
|
+
"POST",
|
|
88
|
+
flow.token
|
|
89
|
+
)
|
|
90
|
+
break
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (
|
|
93
|
+
signal.aborted ||
|
|
94
|
+
attempt === 4 ||
|
|
95
|
+
(error.status &&
|
|
96
|
+
error.status < 500 &&
|
|
97
|
+
error.status !== 429)
|
|
98
|
+
)
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Keys saved to ${path}, but completion could not be confirmed. ${error.message}`
|
|
101
|
+
)
|
|
102
|
+
await wait(5000, signal)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
log(`Installed ${result.projects.length} project(s) in ${path}:`)
|
|
106
|
+
for (const project of result.projects)
|
|
107
|
+
log(` ${JSON.stringify(project.name)}`)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (result.state === "cancelled")
|
|
111
|
+
throw new Error("Login cancelled in browser")
|
|
112
|
+
if (result.state !== "pending")
|
|
113
|
+
throw new Error("Login already completed; run helios login again")
|
|
114
|
+
await wait(5000, signal)
|
|
115
|
+
}
|
|
116
|
+
throw new Error("Login expired or cancelled; run helios login again")
|
|
117
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readConfig } from "./config.mjs"
|
|
2
|
+
export async function resolveProject(
|
|
3
|
+
name,
|
|
4
|
+
{ read = readConfig, fetchImpl = fetch } = {}
|
|
5
|
+
) {
|
|
6
|
+
const config = await read()
|
|
7
|
+
const projects = Object.values(config.profiles)
|
|
8
|
+
const available = projects.length
|
|
9
|
+
? "Configured projects: " +
|
|
10
|
+
projects.map((p) => `${JSON.stringify(p.name)} (${p.id})`).join(", ")
|
|
11
|
+
: "No projects are configured. Run `helios login` to connect your wallet and install project keys."
|
|
12
|
+
const matches = projects.filter((project) => project.name === name)
|
|
13
|
+
if (!matches.length)
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Project ${JSON.stringify(name)} does not exist. ${available}`
|
|
16
|
+
)
|
|
17
|
+
if (matches.length > 1)
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Project name ${JSON.stringify(name)} is ambiguous. Give projects distinct names before compiling. ${available}`
|
|
20
|
+
)
|
|
21
|
+
const project = matches[0]
|
|
22
|
+
if (!/^hdbg_[a-f0-9]{64}$/.test(project.apiKey))
|
|
23
|
+
throw new Error("Invalid configured API key. Run `helios login` again.")
|
|
24
|
+
const endpoint = new URL(project.endpoint)
|
|
25
|
+
if (
|
|
26
|
+
endpoint.protocol !== "https:" ||
|
|
27
|
+
endpoint.username ||
|
|
28
|
+
endpoint.password ||
|
|
29
|
+
endpoint.pathname !== "/" ||
|
|
30
|
+
endpoint.search ||
|
|
31
|
+
endpoint.hash
|
|
32
|
+
)
|
|
33
|
+
throw new Error(
|
|
34
|
+
"Project endpoint must be an HTTPS origin. Run `helios login` again."
|
|
35
|
+
)
|
|
36
|
+
let response
|
|
37
|
+
try {
|
|
38
|
+
response = await fetchImpl(`${endpoint.origin}/v1/project`, {
|
|
39
|
+
headers: { Authorization: `Bearer ${project.apiKey}` },
|
|
40
|
+
signal: AbortSignal.timeout(10000),
|
|
41
|
+
redirect: "error"
|
|
42
|
+
})
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"Could not validate the project API key with the debugger service. Check your connection and try again."
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
if (response.status === 401)
|
|
49
|
+
throw new Error(
|
|
50
|
+
`The API key for ${JSON.stringify(name)} is invalid or revoked. Run \`helios login\` again.`
|
|
51
|
+
)
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Debugger project validation failed (HTTP ${response.status}). Try again later.`
|
|
55
|
+
)
|
|
56
|
+
const remote = await response.json()
|
|
57
|
+
if (remote.id !== project.id || remote.name !== project.name)
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Configured project metadata is outdated. Run `helios login` again."
|
|
60
|
+
)
|
|
61
|
+
return {
|
|
62
|
+
projectId: remote.id,
|
|
63
|
+
name: remote.name,
|
|
64
|
+
apiKey: project.apiKey,
|
|
65
|
+
endpoint: endpoint.origin
|
|
66
|
+
}
|
|
67
|
+
}
|