@hasna/hooks 0.6.5 → 0.6.6
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 +1 -1
- package/bin/index.js +589 -168
- package/bin/serve.js +115 -35
- package/dist/config.d.ts +8 -1
- package/dist/db/remote-storage.d.ts +22 -0
- package/dist/index.js +412 -115
- package/dist/lib/hook-env.d.ts +42 -0
- package/dist/lib/manifest.d.ts +24 -2
- package/dist/lib/redact.d.ts +40 -0
- package/dist/lib/semver.d.ts +19 -0
- package/dist/lib/store.d.ts +16 -0
- package/dist/lib/sync.d.ts +55 -4
- package/dist/serve.d.ts +2 -1
- package/dist/storage.js +104 -7
- package/package.json +2 -2
package/bin/serve.js
CHANGED
|
@@ -527,7 +527,7 @@ function getHook(name) {
|
|
|
527
527
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
|
|
528
528
|
import { basename, dirname, isAbsolute, join as join2, normalize, resolve, sep } from "path";
|
|
529
529
|
|
|
530
|
-
//
|
|
530
|
+
// node_modules/zod/v3/external.js
|
|
531
531
|
var exports_external = {};
|
|
532
532
|
__export(exports_external, {
|
|
533
533
|
void: () => voidType,
|
|
@@ -639,7 +639,7 @@ __export(exports_external, {
|
|
|
639
639
|
BRAND: () => BRAND
|
|
640
640
|
});
|
|
641
641
|
|
|
642
|
-
//
|
|
642
|
+
// node_modules/zod/v3/helpers/util.js
|
|
643
643
|
var util;
|
|
644
644
|
(function(util2) {
|
|
645
645
|
util2.assertEqual = (_) => {};
|
|
@@ -770,7 +770,7 @@ var getParsedType = (data) => {
|
|
|
770
770
|
}
|
|
771
771
|
};
|
|
772
772
|
|
|
773
|
-
//
|
|
773
|
+
// node_modules/zod/v3/ZodError.js
|
|
774
774
|
var ZodIssueCode = util.arrayToEnum([
|
|
775
775
|
"invalid_type",
|
|
776
776
|
"invalid_literal",
|
|
@@ -889,7 +889,7 @@ ZodError.create = (issues) => {
|
|
|
889
889
|
return error;
|
|
890
890
|
};
|
|
891
891
|
|
|
892
|
-
//
|
|
892
|
+
// node_modules/zod/v3/locales/en.js
|
|
893
893
|
var errorMap = (issue, _ctx) => {
|
|
894
894
|
let message;
|
|
895
895
|
switch (issue.code) {
|
|
@@ -992,7 +992,7 @@ var errorMap = (issue, _ctx) => {
|
|
|
992
992
|
};
|
|
993
993
|
var en_default = errorMap;
|
|
994
994
|
|
|
995
|
-
//
|
|
995
|
+
// node_modules/zod/v3/errors.js
|
|
996
996
|
var overrideErrorMap = en_default;
|
|
997
997
|
function setErrorMap(map) {
|
|
998
998
|
overrideErrorMap = map;
|
|
@@ -1000,7 +1000,7 @@ function setErrorMap(map) {
|
|
|
1000
1000
|
function getErrorMap() {
|
|
1001
1001
|
return overrideErrorMap;
|
|
1002
1002
|
}
|
|
1003
|
-
//
|
|
1003
|
+
// node_modules/zod/v3/helpers/parseUtil.js
|
|
1004
1004
|
var makeIssue = (params) => {
|
|
1005
1005
|
const { data, path, errorMaps, issueData } = params;
|
|
1006
1006
|
const fullPath = [...path, ...issueData.path || []];
|
|
@@ -1106,14 +1106,14 @@ var isAborted = (x) => x.status === "aborted";
|
|
|
1106
1106
|
var isDirty = (x) => x.status === "dirty";
|
|
1107
1107
|
var isValid = (x) => x.status === "valid";
|
|
1108
1108
|
var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
|
|
1109
|
-
//
|
|
1109
|
+
// node_modules/zod/v3/helpers/errorUtil.js
|
|
1110
1110
|
var errorUtil;
|
|
1111
1111
|
(function(errorUtil2) {
|
|
1112
1112
|
errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
|
|
1113
1113
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
|
|
1114
1114
|
})(errorUtil || (errorUtil = {}));
|
|
1115
1115
|
|
|
1116
|
-
//
|
|
1116
|
+
// node_modules/zod/v3/types.js
|
|
1117
1117
|
class ParseInputLazyPath {
|
|
1118
1118
|
constructor(parent, value, path, key) {
|
|
1119
1119
|
this._cachedPath = [];
|
|
@@ -4518,20 +4518,22 @@ function getLockPath() {
|
|
|
4518
4518
|
return explicit;
|
|
4519
4519
|
return join(getHooksDataDir(), "hooks.lock");
|
|
4520
4520
|
}
|
|
4521
|
-
function resolveApiKey(
|
|
4522
|
-
if (flagKey && flagKey.length > 0)
|
|
4523
|
-
return flagKey;
|
|
4521
|
+
function resolveApiKey() {
|
|
4524
4522
|
return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
|
|
4525
4523
|
}
|
|
4526
4524
|
|
|
4525
|
+
// src/lib/semver.ts
|
|
4526
|
+
var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
4527
|
+
|
|
4527
4528
|
// src/lib/manifest.ts
|
|
4528
4529
|
var HOOK_NAME_RE = /^[\w-]+$/;
|
|
4529
4530
|
var manifestSchema = exports_external.object({
|
|
4530
4531
|
name: exports_external.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
|
|
4531
|
-
version: exports_external.string().regex(
|
|
4532
|
+
version: exports_external.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
|
|
4532
4533
|
description: exports_external.string().optional(),
|
|
4533
4534
|
events: exports_external.array(exports_external.string()).min(1, "at least one event is required"),
|
|
4534
4535
|
script: exports_external.string().min(1, "script must name a relative path or inline content"),
|
|
4536
|
+
script_kind: exports_external.enum(["inline", "file"]).optional(),
|
|
4535
4537
|
args: exports_external.array(exports_external.string()).optional(),
|
|
4536
4538
|
timeout_ms: exports_external.number().int().positive().optional()
|
|
4537
4539
|
});
|
|
@@ -4584,16 +4586,25 @@ function assertContained(target, root, raw) {
|
|
|
4584
4586
|
assertContainedReal(root, lexical, raw);
|
|
4585
4587
|
return lexical;
|
|
4586
4588
|
}
|
|
4587
|
-
function
|
|
4588
|
-
|
|
4589
|
-
`)
|
|
4589
|
+
function isInlineScript(manifest) {
|
|
4590
|
+
return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
|
|
4591
|
+
`);
|
|
4592
|
+
}
|
|
4593
|
+
function scriptRelFor(manifest) {
|
|
4594
|
+
if (isInlineScript(manifest)) {
|
|
4590
4595
|
const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
|
|
4591
|
-
return
|
|
4596
|
+
return `script${ext}`;
|
|
4592
4597
|
}
|
|
4593
4598
|
const rel = normalize(manifest.script);
|
|
4594
4599
|
if (isAbsolute(rel)) {
|
|
4595
4600
|
throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
|
|
4596
4601
|
}
|
|
4602
|
+
return rel;
|
|
4603
|
+
}
|
|
4604
|
+
function resolveScript(manifest, manifestDir) {
|
|
4605
|
+
if (isInlineScript(manifest))
|
|
4606
|
+
return { path: scriptRelFor(manifest), content: manifest.script };
|
|
4607
|
+
const rel = scriptRelFor(manifest);
|
|
4597
4608
|
const resolved = assertContained(join2(manifestDir, rel), manifestDir, manifest.script);
|
|
4598
4609
|
if (!existsSync(resolved)) {
|
|
4599
4610
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
@@ -4611,8 +4622,8 @@ function readCustomManifest(name) {
|
|
|
4611
4622
|
try {
|
|
4612
4623
|
const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
|
|
4613
4624
|
const script = resolveScript(manifest, dir);
|
|
4614
|
-
|
|
4615
|
-
|
|
4625
|
+
const scriptIsInline = isInlineScript(manifest);
|
|
4626
|
+
return { manifest, scriptPath: join2(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
4616
4627
|
} catch (err) {
|
|
4617
4628
|
if (err instanceof ScriptContainmentError)
|
|
4618
4629
|
throw err;
|
|
@@ -4641,7 +4652,7 @@ function readdirSafe(dir) {
|
|
|
4641
4652
|
|
|
4642
4653
|
// src/lib/store.ts
|
|
4643
4654
|
import { createHash } from "crypto";
|
|
4644
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4655
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
4645
4656
|
import { readFile } from "fs/promises";
|
|
4646
4657
|
|
|
4647
4658
|
// src/db/index.ts
|
|
@@ -5002,6 +5013,7 @@ function getDb() {
|
|
|
5002
5013
|
}
|
|
5003
5014
|
|
|
5004
5015
|
// src/lib/store.ts
|
|
5016
|
+
import { randomBytes } from "crypto";
|
|
5005
5017
|
function sha256Of(input) {
|
|
5006
5018
|
return createHash("sha256").update(input).digest("hex");
|
|
5007
5019
|
}
|
|
@@ -5037,16 +5049,45 @@ function upsertHookRecord(db, record) {
|
|
|
5037
5049
|
record.last_verified_at ?? null
|
|
5038
5050
|
]);
|
|
5039
5051
|
}
|
|
5052
|
+
class LockFileError extends Error {
|
|
5053
|
+
constructor(message) {
|
|
5054
|
+
super(message);
|
|
5055
|
+
this.name = "LockFileError";
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
function assertLockShape(parsed, path) {
|
|
5059
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5060
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: expected a JSON object with a "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
5061
|
+
}
|
|
5062
|
+
const hooks = parsed.hooks;
|
|
5063
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
5064
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: missing or invalid "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
5065
|
+
}
|
|
5066
|
+
for (const [name, entry] of Object.entries(hooks)) {
|
|
5067
|
+
const e = entry;
|
|
5068
|
+
if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
|
|
5069
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: entry for '${name}' is not a valid pin (version and sha256 strings required). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it.`);
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5040
5073
|
function readLock() {
|
|
5074
|
+
const path = getLockPath();
|
|
5075
|
+
if (!existsSync4(path))
|
|
5076
|
+
return { hooks: {} };
|
|
5077
|
+
let raw;
|
|
5041
5078
|
try {
|
|
5042
|
-
|
|
5043
|
-
if (!existsSync4(path))
|
|
5044
|
-
return { hooks: {} };
|
|
5045
|
-
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
5046
|
-
return { hooks: parsed.hooks ?? {} };
|
|
5079
|
+
raw = readFileSync3(path, "utf-8");
|
|
5047
5080
|
} catch {
|
|
5048
|
-
|
|
5081
|
+
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5082
|
+
}
|
|
5083
|
+
let parsed;
|
|
5084
|
+
try {
|
|
5085
|
+
parsed = JSON.parse(raw);
|
|
5086
|
+
} catch {
|
|
5087
|
+
throw new LockFileError(`hooks.lock (${path}) is malformed: not valid JSON (${String((raw ?? "").slice(0, 80))}). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
|
|
5049
5088
|
}
|
|
5089
|
+
assertLockShape(parsed, path);
|
|
5090
|
+
return parsed;
|
|
5050
5091
|
}
|
|
5051
5092
|
function writeLock(lock) {
|
|
5052
5093
|
const path = getLockPath();
|
|
@@ -5055,8 +5096,10 @@ function writeLock(lock) {
|
|
|
5055
5096
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5056
5097
|
sorted[name] = lock.hooks[name];
|
|
5057
5098
|
}
|
|
5058
|
-
|
|
5099
|
+
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
5100
|
+
writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5059
5101
|
`, "utf-8");
|
|
5102
|
+
renameSync(tmp, path);
|
|
5060
5103
|
return path;
|
|
5061
5104
|
}
|
|
5062
5105
|
function setPinnedHook(name, entry) {
|
|
@@ -5185,24 +5228,29 @@ async function buildCatalog() {
|
|
|
5185
5228
|
const scriptPath = resolveHook(meta.name)?.scriptPath;
|
|
5186
5229
|
if (!scriptPath)
|
|
5187
5230
|
continue;
|
|
5231
|
+
const sha = await sha256File(scriptPath);
|
|
5188
5232
|
byName.set(meta.name, {
|
|
5189
5233
|
name: meta.name,
|
|
5190
5234
|
version: meta.version,
|
|
5191
|
-
sha256:
|
|
5235
|
+
sha256: sha,
|
|
5192
5236
|
events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
|
|
5193
5237
|
description: meta.description,
|
|
5194
|
-
source: "bundled"
|
|
5238
|
+
source: "bundled",
|
|
5239
|
+
versions: [meta.version]
|
|
5195
5240
|
});
|
|
5196
5241
|
}
|
|
5197
5242
|
for (const custom2 of listCustomHooks()) {
|
|
5198
5243
|
const name = shortManifestName(custom2.manifest.name);
|
|
5244
|
+
const sha = await sha256File(custom2.scriptPath);
|
|
5245
|
+
const existing = byName.get(name);
|
|
5199
5246
|
byName.set(name, {
|
|
5200
5247
|
name,
|
|
5201
5248
|
version: custom2.manifest.version,
|
|
5202
|
-
sha256:
|
|
5249
|
+
sha256: sha,
|
|
5203
5250
|
events: custom2.manifest.events,
|
|
5204
5251
|
description: custom2.manifest.description ?? "Custom hook",
|
|
5205
|
-
source:
|
|
5252
|
+
source: existing ? "custom-overrides-bundled" : "custom",
|
|
5253
|
+
versions: existing ? [...new Set([...existing.versions, custom2.manifest.version])] : [custom2.manifest.version]
|
|
5206
5254
|
});
|
|
5207
5255
|
}
|
|
5208
5256
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -5220,6 +5268,7 @@ async function artifactFor(name, version) {
|
|
|
5220
5268
|
description: resolved.description,
|
|
5221
5269
|
events: resolved.events,
|
|
5222
5270
|
script: custom2 ? custom2.manifest.script : `src/hook.ts`,
|
|
5271
|
+
script_kind: custom2?.manifest.script_kind,
|
|
5223
5272
|
args: custom2?.manifest.args,
|
|
5224
5273
|
timeout_ms: custom2?.manifest.timeout_ms
|
|
5225
5274
|
};
|
|
@@ -5249,11 +5298,32 @@ function handleServeRequest(req, apiKey) {
|
|
|
5249
5298
|
return buildCatalog().then((catalog) => json({ hooks: catalog }));
|
|
5250
5299
|
}
|
|
5251
5300
|
if (url.pathname === "/api/v1/lock" && req.method === "GET") {
|
|
5252
|
-
return
|
|
5301
|
+
return buildCatalog().then((catalog) => {
|
|
5302
|
+
const lock = readLock();
|
|
5303
|
+
const byVersion = {};
|
|
5304
|
+
for (const entry of catalog)
|
|
5305
|
+
byVersion[entry.name] = entry.versions;
|
|
5306
|
+
const hooks = {};
|
|
5307
|
+
for (const [name, pin] of Object.entries(lock.hooks)) {
|
|
5308
|
+
hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
|
|
5309
|
+
}
|
|
5310
|
+
return json({ hooks });
|
|
5311
|
+
});
|
|
5253
5312
|
}
|
|
5254
|
-
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(
|
|
5313
|
+
const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
|
|
5255
5314
|
if (artifactMatch && req.method === "GET") {
|
|
5256
|
-
const [,
|
|
5315
|
+
const [, rawName, rawVersion] = artifactMatch;
|
|
5316
|
+
let name;
|
|
5317
|
+
let version;
|
|
5318
|
+
try {
|
|
5319
|
+
name = decodeURIComponent(rawName);
|
|
5320
|
+
version = decodeURIComponent(rawVersion);
|
|
5321
|
+
} catch {
|
|
5322
|
+
return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
|
|
5323
|
+
}
|
|
5324
|
+
if (!SEMVER_PATTERN.test(version)) {
|
|
5325
|
+
return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
|
|
5326
|
+
}
|
|
5257
5327
|
return artifactFor(name, version).then(async (artifact) => {
|
|
5258
5328
|
if (!artifact)
|
|
5259
5329
|
return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
|
|
@@ -5284,7 +5354,7 @@ function handleServeRequest(req, apiKey) {
|
|
|
5284
5354
|
function startServeServer(options) {
|
|
5285
5355
|
const port = options.port ?? DEFAULT_SERVE_PORT;
|
|
5286
5356
|
const host = options.host ?? SERVE_HOST;
|
|
5287
|
-
const apiKey = resolveApiKey(
|
|
5357
|
+
const apiKey = resolveApiKey();
|
|
5288
5358
|
const server = Bun.serve({
|
|
5289
5359
|
hostname: host,
|
|
5290
5360
|
port,
|
|
@@ -5296,7 +5366,17 @@ function startServeServer(options) {
|
|
|
5296
5366
|
return server;
|
|
5297
5367
|
}
|
|
5298
5368
|
if (import.meta.main) {
|
|
5299
|
-
|
|
5369
|
+
const argv = process.argv.slice(2);
|
|
5370
|
+
const flagValue = (name) => {
|
|
5371
|
+
const idx = argv.indexOf(name);
|
|
5372
|
+
return idx >= 0 && argv[idx + 1] ? argv[idx + 1] : undefined;
|
|
5373
|
+
};
|
|
5374
|
+
const portArg = flagValue("--port");
|
|
5375
|
+
const hostArg = flagValue("--host");
|
|
5376
|
+
startServeServer({
|
|
5377
|
+
port: portArg ? parseInt(portArg, 10) : undefined,
|
|
5378
|
+
host: hostArg
|
|
5379
|
+
});
|
|
5300
5380
|
}
|
|
5301
5381
|
export {
|
|
5302
5382
|
startServeServer,
|
package/dist/config.d.ts
CHANGED
|
@@ -15,4 +15,11 @@ export interface HooksConfig {
|
|
|
15
15
|
export declare function readConfig(): HooksConfig;
|
|
16
16
|
export declare function writeConfig(config: HooksConfig): string;
|
|
17
17
|
export declare function resolveApiUrl(): string | undefined;
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* P1-8: the registry API key resolves from the environment ONLY —
|
|
20
|
+
* HASNA_HOOKS_API_KEY / HOOKS_API_KEY. A secret-valued CLI flag is removed
|
|
21
|
+
* (a flag value is visible in process listings and shell history); the
|
|
22
|
+
* vault-key-NAME reference (config api_key_ref) is a name, not a value, and
|
|
23
|
+
* is untouched.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveApiKey(): string | undefined;
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P1-5: verified TLS by default. `sslmode=require` / `ssl=true` previously
|
|
3
|
+
* produced `rejectUnauthorized: false` — TLS that verified nothing, so the
|
|
4
|
+
* "encrypted" channel was open to a MITM. The default is now
|
|
5
|
+
* `rejectUnauthorized: true`.
|
|
6
|
+
*
|
|
7
|
+
* Insecure verification exists ONLY through the explicit dev-only config key
|
|
8
|
+
* HASNA_HOOKS_PG_INSECURE_TLS=1 (or HOOKS_PG_INSECURE_TLS) and is refused
|
|
9
|
+
* under a production-shaped environment (NODE_ENV=production) — a dev key
|
|
10
|
+
* silently disabling verification in prod would be the exact failure this
|
|
11
|
+
* finding corrects.
|
|
12
|
+
*
|
|
13
|
+
* P3-12: sslmode/ssl are parsed from the connection-string QUERY, not by
|
|
14
|
+
* substring scan — `sslmode = require` (spaces around the separator), other
|
|
15
|
+
* casings and non-URL DSN forms are all resolved from the parameter value.
|
|
16
|
+
*
|
|
17
|
+
* Exported for tests; the connection-string forms are the documented
|
|
18
|
+
* sslmode values the pg client understands.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sslConfigFor(connectionString: string): {
|
|
21
|
+
rejectUnauthorized: boolean;
|
|
22
|
+
} | undefined;
|
|
1
23
|
export declare class PgAdapterAsync {
|
|
2
24
|
private readonly pool;
|
|
3
25
|
constructor(connectionString: string);
|