@wrongstack/plugin-sdk 0.319.1 → 1.0.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.
@@ -0,0 +1,136 @@
1
+ // src/runtime/bounded-map.ts
2
+ var BoundedMap = class {
3
+ map = /* @__PURE__ */ new Map();
4
+ max;
5
+ ttlMs;
6
+ now;
7
+ /** Entries dropped to stay under `max`. Surfaced by plugin health(). */
8
+ evictions = 0;
9
+ constructor(options) {
10
+ const normalizedMax = Math.floor(options.max);
11
+ this.max = Number.isSafeInteger(normalizedMax) ? Math.max(1, normalizedMax) : 1;
12
+ this.ttlMs = options.ttlMs;
13
+ this.now = options.now ?? Date.now;
14
+ }
15
+ expired(entry) {
16
+ return this.ttlMs !== void 0 && this.now() - entry.storedAt > this.ttlMs;
17
+ }
18
+ get(key) {
19
+ const entry = this.map.get(key);
20
+ if (entry === void 0) return void 0;
21
+ if (this.expired(entry)) {
22
+ this.map.delete(key);
23
+ return void 0;
24
+ }
25
+ this.map.delete(key);
26
+ this.map.set(key, entry);
27
+ return entry.value;
28
+ }
29
+ /**
30
+ * Read without promoting the key to most-recently-used. Use for
31
+ * diagnostics that must not perturb the eviction order.
32
+ */
33
+ peek(key) {
34
+ const entry = this.map.get(key);
35
+ if (entry === void 0 || this.expired(entry)) return void 0;
36
+ return entry.value;
37
+ }
38
+ has(key) {
39
+ const entry = this.map.get(key);
40
+ if (entry === void 0) return false;
41
+ if (this.expired(entry)) {
42
+ this.map.delete(key);
43
+ return false;
44
+ }
45
+ return true;
46
+ }
47
+ set(key, value) {
48
+ this.map.delete(key);
49
+ this.map.set(key, { value, storedAt: this.now() });
50
+ if (this.map.size > this.max && this.ttlMs !== void 0) {
51
+ this.prune();
52
+ }
53
+ while (this.map.size > this.max) {
54
+ const coldest = this.map.keys().next().value;
55
+ if (coldest === void 0) break;
56
+ this.map.delete(coldest);
57
+ this.evictions += 1;
58
+ }
59
+ return this;
60
+ }
61
+ delete(key) {
62
+ return this.map.delete(key);
63
+ }
64
+ clear() {
65
+ this.map.clear();
66
+ this.evictions = 0;
67
+ }
68
+ get size() {
69
+ if (this.ttlMs !== void 0) {
70
+ this.prune();
71
+ }
72
+ return this.map.size;
73
+ }
74
+ /** How many entries have been dropped to respect `max`, since the last clear. */
75
+ get evictionCount() {
76
+ return this.evictions;
77
+ }
78
+ /** Drop every expired entry. Cheap enough to call from a status tool. */
79
+ prune() {
80
+ if (this.ttlMs === void 0) return 0;
81
+ let removed = 0;
82
+ for (const [key, entry] of this.map) {
83
+ if (this.expired(entry)) {
84
+ this.map.delete(key);
85
+ removed += 1;
86
+ }
87
+ }
88
+ return removed;
89
+ }
90
+ /** Live (non-expired) entries, coldest first. */
91
+ *entries() {
92
+ for (const [key, entry] of this.map) {
93
+ if (!this.expired(entry)) yield [key, entry.value];
94
+ }
95
+ }
96
+ [Symbol.iterator]() {
97
+ return this.entries();
98
+ }
99
+ };
100
+ var BoundedSet = class {
101
+ inner;
102
+ constructor(options) {
103
+ this.inner = new BoundedMap(options);
104
+ }
105
+ has(value) {
106
+ return this.inner.has(value);
107
+ }
108
+ add(value) {
109
+ this.inner.set(value, true);
110
+ return this;
111
+ }
112
+ delete(value) {
113
+ return this.inner.delete(value);
114
+ }
115
+ clear() {
116
+ this.inner.clear();
117
+ }
118
+ get size() {
119
+ return this.inner.size;
120
+ }
121
+ /** How many entries have been dropped to respect `max`, since the last clear. */
122
+ get evictionCount() {
123
+ return this.inner.evictionCount;
124
+ }
125
+ *values() {
126
+ for (const [key] of this.inner) yield key;
127
+ }
128
+ [Symbol.iterator]() {
129
+ return this.values();
130
+ }
131
+ };
132
+ export {
133
+ BoundedMap,
134
+ BoundedSet
135
+ };
136
+ //# sourceMappingURL=bounded-map.js.map
@@ -0,0 +1,154 @@
1
+ // src/runtime/credential-patterns.ts
2
+ var CREDENTIAL_PATTERNS = [
3
+ // LLM provider keys
4
+ {
5
+ type: "anthropic_key",
6
+ regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
7
+ },
8
+ {
9
+ type: "openai_key",
10
+ regex: /(?<![A-Za-z0-9])sk-(?!ant)(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g
11
+ },
12
+ // GitHub. `ghp_` is only the personal-access-token prefix — the OAuth
13
+ // (`gho_`), user-to-server (`ghu_`), server-to-server (`ghs_`) and
14
+ // refresh (`ghr_`) tokens grant the same or broader access and were
15
+ // previously not detected at all.
16
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
17
+ {
18
+ type: "github_oauth_token",
19
+ regex: /(?<![A-Za-z0-9])gh[ousr]_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g
20
+ },
21
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
22
+ // GitLab
23
+ { type: "gitlab_pat", regex: /(?<![A-Za-z0-9])glpat-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
24
+ {
25
+ type: "gitlab_runner_token",
26
+ regex: /(?<![A-Za-z0-9])glrt-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
27
+ },
28
+ // npm — a leaked publish token is a supply-chain compromise.
29
+ { type: "npm_token", regex: /(?<![A-Za-z0-9])npm_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g },
30
+ // AWS
31
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
32
+ // GCP
33
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
34
+ // Slack. `xoxe` (token-rotation) and `xapp` (app-level) were missing;
35
+ // both are as sensitive as the bot/user tokens already covered.
36
+ {
37
+ type: "slack_token",
38
+ regex: /(?<![A-Za-z0-9-])xox[abposer]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g
39
+ },
40
+ { type: "slack_app_token", regex: /(?<![A-Za-z0-9-])xapp-\d-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
41
+ {
42
+ type: "slack_webhook",
43
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_-]+\/B[A-Za-z0-9_-]+\/[A-Za-z0-9]{16,}/g
44
+ },
45
+ // Stripe
46
+ {
47
+ type: "stripe_key",
48
+ regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g
49
+ },
50
+ // Twilio
51
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
52
+ // Telegram
53
+ {
54
+ type: "telegram_bot_token",
55
+ regex: /(?:(?<![A-Za-z0-9_])|(?<=(?:^|[^A-Za-z0-9_])bot))\d+:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
56
+ },
57
+ // JWT
58
+ {
59
+ type: "jwt",
60
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
61
+ },
62
+ // Private keys
63
+ {
64
+ type: "private_key",
65
+ regex: /(?:^|\n)(?:-----BEGIN (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----[\s\S]*?-----END (?:RSA|EC|OPENSSH|DSA)? ?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----)(?!\S)/g
66
+ },
67
+ // AI/ML provider tokens
68
+ { type: "huggingface_token", regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g },
69
+ { type: "replicate_token", regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
70
+ { type: "perplexity_key", regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
71
+ { type: "groq_key", regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
72
+ // SaaS / infrastructure tokens — each grants API access on the user's
73
+ // account, and each was previously invisible to this gate.
74
+ {
75
+ type: "sendgrid_key",
76
+ regex: /(?<![A-Za-z0-9])SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
77
+ },
78
+ { type: "digitalocean_token", regex: /(?<![A-Za-z0-9])dop_v1_[a-f0-9]{64}(?![A-Za-z0-9])/g },
79
+ {
80
+ type: "doppler_token",
81
+ regex: /(?<![A-Za-z0-9])dp\.(?:pt|st|sa|scim|audit)\.[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
82
+ },
83
+ {
84
+ type: "shopify_token",
85
+ regex: /(?<![A-Za-z0-9])shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}(?![A-Za-z0-9])/g
86
+ },
87
+ { type: "docker_pat", regex: /(?<![A-Za-z0-9])dckr_pat_[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g },
88
+ { type: "linear_key", regex: /(?<![A-Za-z0-9])lin_api_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g },
89
+ {
90
+ type: "atlassian_token",
91
+ regex: /(?<![A-Za-z0-9])ATATT3[A-Za-z0-9_\-=]{40,}(?![A-Za-z0-9_\-=])/g
92
+ },
93
+ {
94
+ type: "square_token",
95
+ regex: /(?<![A-Za-z0-9])(?:sq0(?:atp|csp)-|EAAA)[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
96
+ },
97
+ {
98
+ type: "azure_storage_key",
99
+ regex: /AccountKey=[A-Za-z0-9+/]{80,}={0,2}/g
100
+ },
101
+ {
102
+ type: "google_oauth_client_secret",
103
+ regex: /(?<![A-Za-z0-9_-])GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g
104
+ },
105
+ // Bearer tokens
106
+ {
107
+ type: "bearer_token",
108
+ regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{12,512}=*(?![A-Za-z0-9._~+/-])/g
109
+ },
110
+ // Database URIs. Require password-bearing user-info; credential-free values stay scannable.
111
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
112
+ {
113
+ type: "postgres_uri",
114
+ // Query parsers decode percent-encoded parameter names. Recognize each
115
+ // encoded character in `password` so mixed forms such as `pass%77ord`
116
+ // cannot bypass detection while keeping the scan strictly bounded.
117
+ regex: /postgres(?:ql)?:\/\/(?:[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+|[^&\s?"'`#]{1,2048}\?(?:(?!(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=)[^&\s#"'`]{1,256}&){0,32}(?:p|%70)(?:a|%61)(?:s|%73)(?:s|%73)(?:w|%77)(?:o|%6[fF])(?:r|%72)(?:d|%64)=[^&\s#"'`]{1,4096})/g
118
+ },
119
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
120
+ { type: "redis_uri", regex: /redis:\/\/[^\s:/@"'`]*:[^\s/@"'`]+@[^\s"'`]+/g },
121
+ {
122
+ // Credentials serialised as JSON, keyed rather than prefixed. Every other
123
+ // entry in this table recognises a credential by its SHAPE (`ghp_`, `sk-`,
124
+ // `eyJ`), which means a key with no distinctive prefix — Azure, a
125
+ // self-hosted gateway, an Anthropic/Codex OAuth token — was invisible to
126
+ // both surfaces. `prompt-firewall` guards the outgoing provider request, so
127
+ // this is what stops a JSON-shaped tool result carrying such a value to a
128
+ // third party.
129
+ //
130
+ // The key is matched in a LOOKBEHIND, so the reported match is the secret
131
+ // itself and the pattern keeps zero capturing groups — `secret-scanner`
132
+ // maps a combined-regex group index back to the pattern that fired, and an
133
+ // inner group would shift that mapping (see the style note above, enforced
134
+ // by credential-pattern-parity.test.ts).
135
+ //
136
+ // Mirrors `json_credential_key` in
137
+ // `@wrongstack/core` → `src/security/secret-scrubber.ts`. Keep the two key
138
+ // lists in step; the core side additionally preserves the key name when it
139
+ // rewrites, which is why it is written with capture groups instead.
140
+ type: "json_credential_key",
141
+ regex: /(?<="[A-Za-z0-9_]{0,64}(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s{0,8}:\s{0,8}")[^"\\]{8,512}(?=")/gi
142
+ }
143
+ ];
144
+ function cloneCredentialPatterns() {
145
+ return CREDENTIAL_PATTERNS.map((p) => ({
146
+ type: p.type,
147
+ regex: new RegExp(p.regex.source, p.regex.flags)
148
+ }));
149
+ }
150
+ export {
151
+ CREDENTIAL_PATTERNS,
152
+ cloneCredentialPatterns
153
+ };
154
+ //# sourceMappingURL=credential-patterns.js.map
@@ -0,0 +1,45 @@
1
+ // src/runtime/h1-state.ts
2
+ function createH1State(initial) {
3
+ const handles = /* @__PURE__ */ new Map();
4
+ const safeRelease = (unregister) => {
5
+ try {
6
+ unregister();
7
+ } catch {
8
+ }
9
+ };
10
+ return {
11
+ state: initial,
12
+ register(key, unregister) {
13
+ const prior = handles.get(key);
14
+ if (prior) {
15
+ safeRelease(prior);
16
+ handles.delete(key);
17
+ }
18
+ if (unregister) {
19
+ handles.set(key, unregister);
20
+ }
21
+ },
22
+ release(key) {
23
+ const prior = handles.get(key);
24
+ if (!prior) return;
25
+ handles.delete(key);
26
+ safeRelease(prior);
27
+ },
28
+ releaseAll() {
29
+ for (const unregister of handles.values()) {
30
+ safeRelease(unregister);
31
+ }
32
+ handles.clear();
33
+ },
34
+ size() {
35
+ return handles.size;
36
+ },
37
+ keys() {
38
+ return [...handles.keys()];
39
+ }
40
+ };
41
+ }
42
+ export {
43
+ createH1State
44
+ };
45
+ //# sourceMappingURL=h1-state.js.map
@@ -0,0 +1,20 @@
1
+ // src/runtime/handles.ts
2
+ function releaseHandle(off) {
3
+ if (off) {
4
+ try {
5
+ off();
6
+ } catch {
7
+ }
8
+ }
9
+ return null;
10
+ }
11
+ function releaseHandles(state, keys) {
12
+ for (const key of keys) {
13
+ state[key] = releaseHandle(state[key]);
14
+ }
15
+ }
16
+ export {
17
+ releaseHandle,
18
+ releaseHandles
19
+ };
20
+ //# sourceMappingURL=handles.js.map
@@ -0,0 +1,91 @@
1
+ // src/runtime/llm.ts
2
+ function stripOuterMarkdownFence(text) {
3
+ const trimmed = text.trim();
4
+ const match = trimmed.match(/^```(?:[a-z0-9_-]+)?\s*\r?\n([\s\S]*?)\r?\n```$/i);
5
+ return (match?.[1] ?? trimmed).trim();
6
+ }
7
+ function parseLlmJsonObject(text) {
8
+ const candidate = stripOuterMarkdownFence(text);
9
+ try {
10
+ const parsed = JSON.parse(candidate);
11
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ async function runOptionalPluginLlm(request) {
17
+ if (!request.requested) {
18
+ return { used: false, value: null, fallbackReason: "not-requested" };
19
+ }
20
+ if (!request.api.llm) {
21
+ return { used: false, value: null, fallbackReason: "unavailable" };
22
+ }
23
+ if (request.options?.signal?.aborted) {
24
+ return { used: false, value: null, fallbackReason: "cancelled" };
25
+ }
26
+ try {
27
+ const response = await request.api.llm.complete(request.prompt, request.options);
28
+ const parsed = request.parse(response.text);
29
+ if (parsed === null) {
30
+ request.api.log.warn(`${request.label}: ignored invalid LLM response`);
31
+ return { used: false, value: null, fallbackReason: "invalid-response" };
32
+ }
33
+ return { used: true, value: parsed, fallbackReason: null };
34
+ } catch (error) {
35
+ const cancelled = request.options?.signal?.aborted === true;
36
+ request.api.log.warn(`${request.label}: LLM enrichment failed; using deterministic fallback`, {
37
+ error: error instanceof Error ? error.message : String(error)
38
+ });
39
+ return {
40
+ used: false,
41
+ value: null,
42
+ fallbackReason: cancelled ? "cancelled" : "provider-error"
43
+ };
44
+ }
45
+ }
46
+ async function runOptionalPluginCouncil(request) {
47
+ if (!request.requested) {
48
+ return { used: false, value: null, fallbackReason: "not-requested" };
49
+ }
50
+ if (request.options?.signal?.aborted) {
51
+ return { used: false, value: null, fallbackReason: "cancelled" };
52
+ }
53
+ const council = request.api.llm?.council;
54
+ if (council) {
55
+ try {
56
+ const result = await council(request.prompt, {
57
+ ...request.context ? { context: request.context } : {},
58
+ ...request.profile ? { profile: request.profile } : {},
59
+ ...request.councilOptions ? { options: request.councilOptions } : {},
60
+ ...request.options?.signal ? { signal: request.options.signal } : {}
61
+ });
62
+ if (result.status === "cancelled") {
63
+ return { used: false, value: null, fallbackReason: "cancelled" };
64
+ }
65
+ const parsed = result.status === "decided" ? request.parse(result.answer ?? "") : null;
66
+ if (parsed !== null) return { used: true, value: parsed, fallbackReason: null };
67
+ request.api.log.warn(
68
+ `${request.label}: Council did not return a valid answer; trying One Shot`,
69
+ {
70
+ status: result.status,
71
+ resolution: result.resolution
72
+ }
73
+ );
74
+ } catch (error) {
75
+ if (request.options?.signal?.aborted) {
76
+ return { used: false, value: null, fallbackReason: "cancelled" };
77
+ }
78
+ request.api.log.warn(`${request.label}: Council failed; trying One Shot`, {
79
+ error: error instanceof Error ? error.message : String(error)
80
+ });
81
+ }
82
+ }
83
+ return runOptionalPluginLlm(request);
84
+ }
85
+ export {
86
+ parseLlmJsonObject,
87
+ runOptionalPluginCouncil,
88
+ runOptionalPluginLlm,
89
+ stripOuterMarkdownFence
90
+ };
91
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1,102 @@
1
+ // src/runtime/local-bin.ts
2
+ import { createRequire } from "node:module";
3
+ import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { accessSync, constants, readFileSync } from "node:fs";
5
+ import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
6
+ function resolveExecInvocation(command, args = []) {
7
+ const resolved = resolveWin32Command(command);
8
+ const normalizedResolved = resolved.toLowerCase();
9
+ const needsShell = process.platform === "win32" && (normalizedResolved.endsWith(".cmd") || normalizedResolved.endsWith(".bat"));
10
+ if (needsShell) {
11
+ const shim = buildWin32CmdShimInvocation(resolved, args);
12
+ return { cmd: shim.command, args: shim.args, windowsVerbatimArguments: true };
13
+ }
14
+ return { cmd: resolved, args: [...args], windowsVerbatimArguments: false };
15
+ }
16
+ function findOnPath(cmd) {
17
+ if (!cmd) return null;
18
+ const exists = (p) => {
19
+ try {
20
+ accessSync(p, constants.X_OK);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ };
26
+ if (cmd.includes("/") || cmd.includes("\\")) {
27
+ return exists(cmd) ? resolve(cmd) : null;
28
+ }
29
+ const suffixes = process.platform === "win32" && extname(cmd) === "" ? (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
30
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
31
+ if (!dir) continue;
32
+ const base = join(dir, cmd);
33
+ for (const suffix of suffixes) {
34
+ const candidate = `${base}${suffix}`;
35
+ if (exists(candidate)) return candidate;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ function isInside(parent, candidate) {
41
+ const rel = relative(parent, candidate);
42
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
43
+ }
44
+ var binCache = /* @__PURE__ */ new Map();
45
+ var BIN_CACHE_MAX = 64;
46
+ var NEGATIVE_BIN_CACHE_TTL_MS = 5e3;
47
+ function cachePut(key, value) {
48
+ while (binCache.size >= BIN_CACHE_MAX) {
49
+ const oldest = binCache.keys().next().value;
50
+ if (oldest === void 0) break;
51
+ binCache.delete(oldest);
52
+ }
53
+ binCache.set(key, { value, cachedAt: Date.now() });
54
+ return value;
55
+ }
56
+ function clearLocalBinCache() {
57
+ binCache.clear();
58
+ }
59
+ function resolveNodeBin(packageName, binName, cwd, extraArgs = []) {
60
+ const key = `${packageName}|${binName}|${cwd}`;
61
+ const cached = binCache.get(key);
62
+ if (cached !== void 0) {
63
+ if (cached.value !== null || Date.now() - cached.cachedAt < NEGATIVE_BIN_CACHE_TTL_MS) {
64
+ return cached.value === null ? null : { ...cached.value, args: [cached.value.entry, ...extraArgs] };
65
+ }
66
+ binCache.delete(key);
67
+ }
68
+ let resolved = null;
69
+ try {
70
+ const requireFromProject = createRequire(resolve(cwd, "package.json"));
71
+ const packagePath = requireFromProject.resolve(`${packageName}/package.json`);
72
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
73
+ const relativeBin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.[binName] ?? Object.values(packageJson.bin ?? {})[0];
74
+ if (relativeBin && !isAbsolute(relativeBin)) {
75
+ const packageDir = dirname(packagePath);
76
+ const entry = resolve(packageDir, relativeBin);
77
+ if (isInside(packageDir, entry)) {
78
+ resolved = { cmd: process.execPath, args: [entry], entry };
79
+ }
80
+ }
81
+ } catch {
82
+ resolved = null;
83
+ }
84
+ cachePut(key, resolved);
85
+ return resolved === null ? null : { ...resolved, args: [resolved.entry, ...extraArgs] };
86
+ }
87
+ function resolveFirstNodeBin(candidates, cwd) {
88
+ for (const c of candidates) {
89
+ const hit = resolveNodeBin(c.packageName, c.binName, cwd, c.args ?? []);
90
+ if (hit) return { ...hit, packageName: c.packageName, binName: c.binName };
91
+ }
92
+ return null;
93
+ }
94
+ export {
95
+ clearLocalBinCache,
96
+ findOnPath,
97
+ resolveExecInvocation,
98
+ resolveFirstNodeBin,
99
+ resolveNodeBin,
100
+ resolveWin32Command
101
+ };
102
+ //# sourceMappingURL=local-bin.js.map
@@ -0,0 +1,96 @@
1
+ // src/runtime/redos-guard.ts
2
+ import { Worker } from "node:worker_threads";
3
+ var warm = null;
4
+ var callSeq = 0;
5
+ function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
6
+ const opts = { budgetMs, ...options };
7
+ const start = Date.now();
8
+ const id = ++callSeq;
9
+ let worker;
10
+ if (warm !== null) {
11
+ worker = warm;
12
+ } else {
13
+ worker = spawnPoolWorker();
14
+ }
15
+ warm = null;
16
+ worker.ref();
17
+ return new Promise((resolve) => {
18
+ let settled = false;
19
+ const settle = (result) => {
20
+ if (settled) return;
21
+ settled = true;
22
+ clearTimeout(timer);
23
+ worker.off("message", onMessage);
24
+ worker.off("error", onError);
25
+ resolve(result);
26
+ };
27
+ const onMessage = (msg) => {
28
+ if (settled || msg.id !== id) return;
29
+ worker.unref();
30
+ if (warm === null) {
31
+ warm = worker;
32
+ } else {
33
+ worker.terminate().catch(() => {
34
+ });
35
+ }
36
+ if (!msg.ok) {
37
+ settle({ timedOut: true, match: null });
38
+ return;
39
+ }
40
+ settle({ timedOut: false, match: msg.match });
41
+ };
42
+ const onError = () => {
43
+ if (settled) return;
44
+ settle({ timedOut: true, match: null });
45
+ };
46
+ const timer = setTimeout(() => {
47
+ if (settled) return;
48
+ const elapsedMs = Date.now() - start;
49
+ worker.terminate().catch(() => {
50
+ });
51
+ try {
52
+ opts.onTimeout?.({
53
+ regex: re,
54
+ input,
55
+ budgetMs: opts.budgetMs,
56
+ elapsedMs
57
+ });
58
+ } catch {
59
+ }
60
+ settle({ timedOut: true, match: null });
61
+ }, opts.budgetMs);
62
+ timer.unref?.();
63
+ worker.once("message", onMessage);
64
+ worker.once("error", onError);
65
+ worker.postMessage({ id, source: re.source, flags: re.flags, input });
66
+ });
67
+ }
68
+ var POOLED_WORKER_SOURCE = `
69
+ const { parentPort } = require('node:worker_threads');
70
+ parentPort.on('message', (msg) => {
71
+ const { id, source, flags, input } = msg;
72
+ try {
73
+ const re = new RegExp(source, flags);
74
+ const match = re.exec(input);
75
+ parentPort.postMessage({ id, ok: true, match });
76
+ } catch (err) {
77
+ parentPort.postMessage({ id, ok: false, error: err && err.message ? err.message : String(err) });
78
+ }
79
+ });
80
+ `;
81
+ function spawnPoolWorker() {
82
+ const worker = new Worker(POOLED_WORKER_SOURCE, {
83
+ eval: true,
84
+ name: "redos-guard:pool"
85
+ });
86
+ worker.unref();
87
+ return worker;
88
+ }
89
+ function guardedMatcher(re, budgetMs = 50, onTimeout) {
90
+ return (input) => withReDoSGuard(re, input, budgetMs, onTimeout ? { onTimeout } : {});
91
+ }
92
+ export {
93
+ guardedMatcher,
94
+ withReDoSGuard
95
+ };
96
+ //# sourceMappingURL=redos-guard.js.map
@@ -0,0 +1,27 @@
1
+ // src/runtime/safe-json.ts
2
+ var UNSERIALIZABLE = "[unserializable]";
3
+ function safeJsonStringify(value, indent) {
4
+ try {
5
+ const stack = [];
6
+ const out = JSON.stringify(
7
+ value,
8
+ function replacer(_key, val) {
9
+ if (typeof val === "bigint") return `${val.toString()}n`;
10
+ if (val === null || typeof val !== "object") return val;
11
+ while (stack.length > 0 && stack[stack.length - 1] !== this) stack.pop();
12
+ if (stack.includes(val)) return "[circular]";
13
+ stack.push(val);
14
+ return val;
15
+ },
16
+ indent
17
+ );
18
+ return out ?? String(value);
19
+ } catch {
20
+ return UNSERIALIZABLE;
21
+ }
22
+ }
23
+ export {
24
+ UNSERIALIZABLE,
25
+ safeJsonStringify
26
+ };
27
+ //# sourceMappingURL=safe-json.js.map
@@ -0,0 +1,50 @@
1
+ // src/runtime/sandbox.ts
2
+ import { realpathSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ var MAX_PATH_BYTES = 4096;
5
+ function safePath(input, options = {}) {
6
+ if (typeof input !== "string") return null;
7
+ if (input.length === 0 || input.length > MAX_PATH_BYTES) return null;
8
+ if (input.startsWith("-")) return null;
9
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
10
+ const lexical = isAbsolute(input) ? resolve(input) : resolve(projectRoot, input);
11
+ if (!withinLexical(projectRoot, lexical)) return null;
12
+ if (options.followSymlinks !== false) {
13
+ const real = realpathWithMissingLeaf(lexical);
14
+ if (real === null || !withinLexical(projectRoot, real)) return null;
15
+ return real;
16
+ }
17
+ return lexical;
18
+ }
19
+ function realpathWithMissingLeaf(candidate) {
20
+ let current = candidate;
21
+ const missing = [];
22
+ while (true) {
23
+ try {
24
+ const resolved = realpathSync(current);
25
+ return missing.reduceRight((parent, part) => join(parent, part), resolved);
26
+ } catch (error) {
27
+ const code = error.code;
28
+ if (code !== "ENOENT" && code !== "ENOTDIR") return null;
29
+ const parent = dirname(current);
30
+ if (parent === current) return null;
31
+ missing.push(basename(current));
32
+ current = parent;
33
+ }
34
+ }
35
+ }
36
+ function withinLexical(projectRoot, candidate) {
37
+ const rel = relative(projectRoot, candidate);
38
+ if (rel === "" || rel === ".") return true;
39
+ if (rel.startsWith("..")) return false;
40
+ if (isAbsolute(rel)) return false;
41
+ return true;
42
+ }
43
+ function isInsideProject(input, options = {}) {
44
+ return safePath(input, options) !== null;
45
+ }
46
+ export {
47
+ isInsideProject,
48
+ safePath
49
+ };
50
+ //# sourceMappingURL=sandbox.js.map
package/dist/runtime.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/runtime/index.ts
2
2
  import { execFile } from "node:child_process";
3
3
  import { existsSync, readdirSync, statSync } from "node:fs";
4
- import { basename, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
5
- import { buildChildEnv } from "@wrongstack/core/utils";
4
+ import { basename as basename2, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
5
+ import { buildChildEnv } from "@wrongstack/core/utils/child-env";
6
6
 
7
7
  // src/runtime/llm.ts
8
8
  function stripOuterMarkdownFence(text) {
@@ -506,39 +506,51 @@ function releaseHandles(state, keys) {
506
506
 
507
507
  // src/runtime/redos-guard.ts
508
508
  import { Worker } from "node:worker_threads";
509
+ var warm = null;
510
+ var callSeq = 0;
509
511
  function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
510
512
  const opts = { budgetMs, ...options };
511
513
  const start = Date.now();
512
- const workerSource = buildWorkerSource(re.source, input, re.flags);
513
- const worker = new Worker(workerSource, {
514
- eval: true,
515
- name: `redos-guard:${re.source.slice(0, 32)}`
516
- });
514
+ const id = ++callSeq;
515
+ let worker;
516
+ if (warm !== null) {
517
+ worker = warm;
518
+ } else {
519
+ worker = spawnPoolWorker();
520
+ }
521
+ warm = null;
522
+ worker.ref();
517
523
  return new Promise((resolve4) => {
518
524
  let settled = false;
519
- const onMessage = (msg) => {
525
+ const settle = (result) => {
520
526
  if (settled) return;
521
527
  settled = true;
522
528
  clearTimeout(timer);
523
- worker.terminate().catch(() => {
524
- });
529
+ worker.off("message", onMessage);
530
+ worker.off("error", onError);
531
+ resolve4(result);
532
+ };
533
+ const onMessage = (msg) => {
534
+ if (settled || msg.id !== id) return;
535
+ worker.unref();
536
+ if (warm === null) {
537
+ warm = worker;
538
+ } else {
539
+ worker.terminate().catch(() => {
540
+ });
541
+ }
525
542
  if (!msg.ok) {
526
- resolve4({ timedOut: true, match: null });
543
+ settle({ timedOut: true, match: null });
527
544
  return;
528
545
  }
529
- resolve4({ timedOut: false, match: msg.match });
546
+ settle({ timedOut: false, match: msg.match });
530
547
  };
531
548
  const onError = () => {
532
549
  if (settled) return;
533
- settled = true;
534
- clearTimeout(timer);
535
- worker.terminate().catch(() => {
536
- });
537
- resolve4({ timedOut: true, match: null });
550
+ settle({ timedOut: true, match: null });
538
551
  };
539
552
  const timer = setTimeout(() => {
540
553
  if (settled) return;
541
- settled = true;
542
554
  const elapsedMs = Date.now() - start;
543
555
  worker.terminate().catch(() => {
544
556
  });
@@ -551,34 +563,34 @@ function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
551
563
  });
552
564
  } catch {
553
565
  }
554
- resolve4({ timedOut: true, match: null });
566
+ settle({ timedOut: true, match: null });
555
567
  }, opts.budgetMs);
556
568
  timer.unref?.();
557
- worker.on("message", onMessage);
558
- worker.on("error", onError);
569
+ worker.once("message", onMessage);
570
+ worker.once("error", onError);
571
+ worker.postMessage({ id, source: re.source, flags: re.flags, input });
559
572
  });
560
573
  }
561
- function buildWorkerSource(source, input, flags) {
562
- const S = JSON.stringify(source);
563
- const I = JSON.stringify(input);
564
- const F = JSON.stringify(flags);
565
- return `
574
+ var POOLED_WORKER_SOURCE = `
566
575
  const { parentPort } = require('node:worker_threads');
567
- const source = ${S};
568
- const input = ${I};
569
- const flags = ${F};
570
- try {
571
- const re = new RegExp(source, flags);
572
- const match = re.exec(input);
573
- // parentPort.postMessage, NOT bare postMessage: with eval:true
574
- // workers this Node version does not expose the bare postMessage
575
- // global \u2014 the worker throws ReferenceError at startup and the
576
- // host misreads it as a timeout (positive-path regression).
577
- parentPort.postMessage({ ok: true, match });
578
- } catch (err) {
579
- parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
580
- }
576
+ parentPort.on('message', (msg) => {
577
+ const { id, source, flags, input } = msg;
578
+ try {
579
+ const re = new RegExp(source, flags);
580
+ const match = re.exec(input);
581
+ parentPort.postMessage({ id, ok: true, match });
582
+ } catch (err) {
583
+ parentPort.postMessage({ id, ok: false, error: err && err.message ? err.message : String(err) });
584
+ }
585
+ });
581
586
  `;
587
+ function spawnPoolWorker() {
588
+ const worker = new Worker(POOLED_WORKER_SOURCE, {
589
+ eval: true,
590
+ name: "redos-guard:pool"
591
+ });
592
+ worker.unref();
593
+ return worker;
582
594
  }
583
595
  function guardedMatcher(re, budgetMs = 50, onTimeout) {
584
596
  return (input) => withReDoSGuard(re, input, budgetMs, onTimeout ? { onTimeout } : {});
@@ -586,7 +598,7 @@ function guardedMatcher(re, budgetMs = 50, onTimeout) {
586
598
 
587
599
  // src/runtime/sandbox.ts
588
600
  import { realpathSync } from "node:fs";
589
- import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
601
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
590
602
  var MAX_PATH_BYTES = 4096;
591
603
  function safePath(input, options = {}) {
592
604
  if (typeof input !== "string") return null;
@@ -596,17 +608,29 @@ function safePath(input, options = {}) {
596
608
  const lexical = isAbsolute2(input) ? resolve2(input) : resolve2(projectRoot, input);
597
609
  if (!withinLexical(projectRoot, lexical)) return null;
598
610
  if (options.followSymlinks !== false) {
599
- let real;
600
- try {
601
- real = realpathSync(lexical);
602
- } catch {
603
- return null;
604
- }
605
- if (!withinLexical(projectRoot, real)) return null;
611
+ const real = realpathWithMissingLeaf(lexical);
612
+ if (real === null || !withinLexical(projectRoot, real)) return null;
606
613
  return real;
607
614
  }
608
615
  return lexical;
609
616
  }
617
+ function realpathWithMissingLeaf(candidate) {
618
+ let current = candidate;
619
+ const missing = [];
620
+ while (true) {
621
+ try {
622
+ const resolved = realpathSync(current);
623
+ return missing.reduceRight((parent, part) => join2(parent, part), resolved);
624
+ } catch (error) {
625
+ const code = error.code;
626
+ if (code !== "ENOENT" && code !== "ENOTDIR") return null;
627
+ const parent = dirname2(current);
628
+ if (parent === current) return null;
629
+ missing.push(basename(current));
630
+ current = parent;
631
+ }
632
+ }
633
+ }
610
634
  function withinLexical(projectRoot, candidate) {
611
635
  const rel = relative2(projectRoot, candidate);
612
636
  if (rel === "" || rel === ".") return true;
@@ -731,7 +755,7 @@ function resolveRunnerCommand(runtime, command, options = {}) {
731
755
  }
732
756
  if (isAbsolute3(head)) {
733
757
  if (!withinProjectPath(projectRoot, head)) return null;
734
- const base = basename(head);
758
+ const base = basename2(head);
735
759
  if (base !== runtime.executable && base !== launcher) return null;
736
760
  if (second !== runtime.executable) return null;
737
761
  if (!everyFlagAllowed(runtime.allowedFlags, rest)) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/plugin-sdk",
3
- "version": "0.319.1",
3
+ "version": "1.0.0",
4
4
  "description": "Authoring SDK for WrongStack plugins — the plugin contract types, definePlugin, and shared runtime helpers. Third-party plugins should depend on this package only.",
5
5
  "license": "MIT",
6
6
  "author": "ECOSTACK TECHNOLOGY OÜ",
@@ -16,6 +16,42 @@
16
16
  "types": "./dist/runtime.d.ts",
17
17
  "import": "./dist/runtime.js"
18
18
  },
19
+ "./runtime/bounded-map": {
20
+ "types": "./dist/runtime/bounded-map.d.ts",
21
+ "import": "./dist/runtime/bounded-map.js"
22
+ },
23
+ "./runtime/credential-patterns": {
24
+ "types": "./dist/runtime/credential-patterns.d.ts",
25
+ "import": "./dist/runtime/credential-patterns.js"
26
+ },
27
+ "./runtime/h1-state": {
28
+ "types": "./dist/runtime/h1-state.d.ts",
29
+ "import": "./dist/runtime/h1-state.js"
30
+ },
31
+ "./runtime/handles": {
32
+ "types": "./dist/runtime/handles.d.ts",
33
+ "import": "./dist/runtime/handles.js"
34
+ },
35
+ "./runtime/llm": {
36
+ "types": "./dist/runtime/llm.d.ts",
37
+ "import": "./dist/runtime/llm.js"
38
+ },
39
+ "./runtime/local-bin": {
40
+ "types": "./dist/runtime/local-bin.d.ts",
41
+ "import": "./dist/runtime/local-bin.js"
42
+ },
43
+ "./runtime/redos-guard": {
44
+ "types": "./dist/runtime/redos-guard.d.ts",
45
+ "import": "./dist/runtime/redos-guard.js"
46
+ },
47
+ "./runtime/safe-json": {
48
+ "types": "./dist/runtime/safe-json.d.ts",
49
+ "import": "./dist/runtime/safe-json.js"
50
+ },
51
+ "./runtime/sandbox": {
52
+ "types": "./dist/runtime/sandbox.d.ts",
53
+ "import": "./dist/runtime/sandbox.js"
54
+ },
19
55
  "./package.json": "./package.json"
20
56
  },
21
57
  "files": [
@@ -29,8 +65,8 @@
29
65
  "vitest": "^4.1.11"
30
66
  },
31
67
  "dependencies": {
32
- "@wrongstack/core": "0.319.1",
33
- "@wrongstack/tools": "0.319.1"
68
+ "@wrongstack/core": "1.0.0",
69
+ "@wrongstack/tools": "1.0.0"
34
70
  },
35
71
  "scripts": {
36
72
  "build": "node ../../scripts/build-package.mjs",