@jini-ai/platform 0.6.15-rc.2 → 0.6.15
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/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/dist/blob-storage.js +10 -9
- package/dist/child-environment.js +1 -1
- package/dist/command.js +4 -1
- package/dist/core/effective-port.d.ts +6 -0
- package/dist/core/effective-port.js +8 -0
- package/dist/download.js +25 -24
- package/dist/fetch-with-timeout.d.ts +26 -6
- package/dist/fetch-with-timeout.js +50 -10
- package/dist/fs/atomic-write.js +5 -2
- package/dist/fs/boot-identity.d.ts +0 -4
- package/dist/fs/boot-identity.js +9 -3
- package/dist/fs/directory-registry.js +2 -2
- package/dist/fs/directory-running-lock.js +9 -4
- package/dist/fs/directory-sync.js +2 -3
- package/dist/fs/directory-workspace.js +11 -14
- package/dist/fs/durable-json.js +22 -16
- package/dist/fs/errno.d.ts +9 -0
- package/dist/fs/errno.js +21 -0
- package/dist/fs/file-lock.js +17 -19
- package/dist/fs/guarded-reader.js +15 -9
- package/dist/fs/user-data-directories.js +5 -7
- package/dist/fs.js +2 -1
- package/dist/http/guarded/client.js +2 -1
- package/dist/http.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mail/builtin-providers.js +4 -2
- package/dist/mail/feedback.js +1 -1
- package/dist/mail/prepare-sender-email.js +7 -1
- package/dist/mail/provider.js +14 -8
- package/dist/mail/rate-limit.js +3 -1
- package/dist/net/endpoint-policy.js +15 -7
- package/dist/object-store/local-fs.js +3 -1
- package/dist/process.js +10 -9
- package/dist/proxy-env.js +22 -14
- package/dist/secrets/credential-sets/provider-matches.js +14 -3
- package/dist/secrets/credential-sets/vault.meta.js +6 -2
- package/dist/secrets/credential-sets/vault.repo.memory.js +3 -5
- package/dist/secrets/credential-sets/vault.service.js +22 -17
- package/dist/secrets/credential-sets/vault.types.d.ts +6 -0
- package/dist/secrets/credential-sets/vault.types.js +7 -0
- package/dist/secrets/credential-token.js +9 -2
- package/dist/secrets/keyring.env.js +2 -4
- package/dist/shell.js +2 -1
- package/dist/terminal.js +4 -7
- package/dist/toolchain.js +4 -9
- package/dist/web/first-distinct.d.ts +11 -0
- package/dist/web/first-distinct.js +16 -0
- package/dist/web/html-document.js +35 -39
- package/dist/web/html-to-markdown.js +20 -9
- package/dist/web/web-page.js +28 -25
- package/dist/windows-system-tools.d.ts +15 -0
- package/dist/windows-system-tools.js +16 -0
- package/package.json +2 -2
package/dist/proxy-env.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* proxy-config tools; owns no other sibling dependency.
|
|
12
12
|
*/
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { captureLineTail } from "@jini-ai/core/text";
|
|
14
15
|
const CANONICAL_PROXY_ENV_KEYS = new Map([
|
|
15
16
|
["all_proxy", "ALL_PROXY"],
|
|
16
17
|
["http_proxy", "HTTP_PROXY"],
|
|
@@ -78,18 +79,21 @@ function collectSourceProxyEntries(input) {
|
|
|
78
79
|
if (value == null)
|
|
79
80
|
continue;
|
|
80
81
|
const canonicalKey = canonicalProxyEnvKey(key);
|
|
81
|
-
if (
|
|
82
|
+
if (canonicalKey)
|
|
83
|
+
keepPreferredProxyEntry(proxyEntries, { canonicalKey, key, value });
|
|
84
|
+
else
|
|
82
85
|
input.merged[key] = value;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const current = proxyEntries.get(canonicalKey);
|
|
86
|
-
const preferLowercase = key === key.toLowerCase();
|
|
87
|
-
if (!current || preferLowercase || !current.preferLowercase) {
|
|
88
|
-
proxyEntries.set(canonicalKey, { preferLowercase, value });
|
|
89
|
-
}
|
|
90
86
|
}
|
|
91
87
|
return proxyEntries;
|
|
92
88
|
}
|
|
89
|
+
/** @internal Record `key`'s value unless a lowercase spelling of the same variable already won. */
|
|
90
|
+
function keepPreferredProxyEntry(proxyEntries, entry) {
|
|
91
|
+
const current = proxyEntries.get(entry.canonicalKey);
|
|
92
|
+
const preferLowercase = entry.key === entry.key.toLowerCase();
|
|
93
|
+
if (!current || preferLowercase || !current.preferLowercase) {
|
|
94
|
+
proxyEntries.set(entry.canonicalKey, { preferLowercase, value: entry.value });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
93
97
|
/** @internal Whether `env` already contains any case-variant of the given canonical proxy key. */
|
|
94
98
|
function hasCanonicalProxyEnv(env, canonicalKey) {
|
|
95
99
|
return Object.keys(env).some((key) => key.toLowerCase() === canonicalKey.toLowerCase());
|
|
@@ -249,16 +253,20 @@ function parseScutilLines(stdout) {
|
|
|
249
253
|
}
|
|
250
254
|
/** @internal Record one `N : value` ExceptionsList entry; returns whether the array is still open (false on `}`). */
|
|
251
255
|
function collectScutilException(line, exceptions) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
// Same as /^\d+\s*:\s*(.+)$/, split so the value tail cannot backtrack.
|
|
257
|
+
const index = /^\d+\s*:/.exec(line);
|
|
258
|
+
const value = index ? captureLineTail({ rest: line.slice(index[0].length) }) : null;
|
|
259
|
+
if (value !== null)
|
|
260
|
+
exceptions.push(value.trim());
|
|
255
261
|
return line !== "}";
|
|
256
262
|
}
|
|
257
263
|
/** @internal Record one top-level `Name : value` scutil scalar, ignoring lines of any other shape. */
|
|
258
264
|
function collectScutilScalar(line, scalars) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
// Same as /^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.+)$/, split so the value tail cannot backtrack.
|
|
266
|
+
const name = /^([A-Za-z][A-Za-z0-9]*)\s*:/.exec(line);
|
|
267
|
+
const value = name ? captureLineTail({ rest: line.slice(name[0].length) }) : null;
|
|
268
|
+
if (name && value !== null)
|
|
269
|
+
scalars.set(name[1], value.trim());
|
|
262
270
|
}
|
|
263
271
|
/** @internal The `<prefix>Proxy`/`<prefix>Port` URL when `<prefix>Enable` is `1`, else `null`. */
|
|
264
272
|
function scutilEnabledProxy(input) {
|
|
@@ -23,15 +23,26 @@ export function normalizeCredentialWorksFor({ value }, _options = {}) {
|
|
|
23
23
|
*/
|
|
24
24
|
export function matchingCredentialProviderIds({ credential, providers }, _options = {}) {
|
|
25
25
|
const { credentialType, worksFor } = credential.meta;
|
|
26
|
-
if ((
|
|
27
|
-
(
|
|
28
|
-
!Array.isArray(worksFor) || Array.from(worksFor).some(id => typeof id !== "string" || !id || id !== id.toLowerCase() || /\s/.test(id)))
|
|
26
|
+
if (!hasMatchableType({ kind: credential.kind, credentialType }) ||
|
|
27
|
+
!Array.isArray(worksFor) || !Array.from(worksFor).every(isCanonicalProviderId))
|
|
29
28
|
return [];
|
|
30
29
|
const ids = new Set(worksFor);
|
|
31
30
|
return providers.filter(provider => (credentialType !== "custom" || provider.acceptsCustomToken) &&
|
|
32
31
|
(ids.has(provider.id.toLowerCase()) || ids.has(provider.vendorId.toLowerCase())))
|
|
33
32
|
.map(provider => provider.id);
|
|
34
33
|
}
|
|
34
|
+
/** vendor/custom rows only; a custom-kind row must also carry the custom subtype. */
|
|
35
|
+
function hasMatchableType({ kind, credentialType }) {
|
|
36
|
+
if (kind !== "vendor" && kind !== "custom")
|
|
37
|
+
return false;
|
|
38
|
+
if (kind === "custom" && credentialType !== "custom")
|
|
39
|
+
return false;
|
|
40
|
+
return credentialType === "vendor" || credentialType === "custom";
|
|
41
|
+
}
|
|
42
|
+
/** A non-empty, already-lowercased ID without whitespace, as {@link normalizeCredentialWorksFor} stores it. */
|
|
43
|
+
function isCanonicalProviderId(id) {
|
|
44
|
+
return typeof id === "string" && id !== "" && id === id.toLowerCase() && !/\s/.test(id);
|
|
45
|
+
}
|
|
35
46
|
/** Select an eligible explicit ID, the descriptor's provider-group default, or a sole candidate.
|
|
36
47
|
* Missing IDs and unresolved ambiguity return null; custom-group defaults do not rank candidates.
|
|
37
48
|
* @complexity O(credentials * average worksFor) time, O(credentials) candidate space.
|
|
@@ -39,16 +39,20 @@ function isJson(value) {
|
|
|
39
39
|
* @throws VaultError when a supplied subtype or compatibility list is invalid.
|
|
40
40
|
* @complexity O(worksFor characters); returns a fresh metadata object.
|
|
41
41
|
*/
|
|
42
|
+
/** Older vendor rows imply compatibility with their own provider; nothing else implies any. */
|
|
43
|
+
function defaultWorksFor({ credentialType, provider }) {
|
|
44
|
+
return credentialType === "vendor" && provider != null ? [provider] : [];
|
|
45
|
+
}
|
|
42
46
|
function credentialMeta({ meta, kind, provider }) {
|
|
43
47
|
const credentialType = meta.credentialType === undefined ? kind : meta.credentialType;
|
|
44
48
|
if ((credentialType !== "vendor" && credentialType !== "custom") || (kind === "custom" && credentialType !== "custom"))
|
|
45
49
|
throw new VaultError({ code: "invalid-meta" });
|
|
46
|
-
const worksFor = meta.worksFor === undefined ? (credentialType
|
|
50
|
+
const worksFor = meta.worksFor === undefined ? defaultWorksFor({ credentialType, provider }) : meta.worksFor;
|
|
47
51
|
return { ...meta, credentialType, worksFor: normalizeCredentialWorksFor({ value: worksFor }) };
|
|
48
52
|
}
|
|
49
53
|
/** Plain JSON object with identifier-only top-level keys; checked on input and on override output. */
|
|
50
54
|
function requireMetaShape({ value }) {
|
|
51
|
-
if (!isObject(value) || !isJson(value) || Object.keys(value).some(key =>
|
|
55
|
+
if (!isObject(value) || !isJson(value) || Object.keys(value).some(key => !/^\w+$/.test(key)))
|
|
52
56
|
throw new VaultError({ code: "invalid-meta" });
|
|
53
57
|
return value;
|
|
54
58
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertVaultReplacement, vaultGroupKey, VaultError } from "./vault.types.js";
|
|
1
|
+
import { assertVaultReplacement, isInVaultQueryScope, vaultGroupKey, VaultError } from "./vault.types.js";
|
|
2
2
|
/** Single-turn mutations are atomic in JS; clones prevent callers bypassing CAS through aliases. */
|
|
3
3
|
export class InMemoryVaultRepo {
|
|
4
4
|
rows = new Map();
|
|
@@ -56,15 +56,13 @@ export class InMemoryVaultRepo {
|
|
|
56
56
|
/** O(n) memory scan, n stored rows; clones only the default, never its siblings. */
|
|
57
57
|
async findDefault(input, _options = {}) {
|
|
58
58
|
for (const row of this.rows.values()) {
|
|
59
|
-
if (row.isDefault && row.
|
|
60
|
-
row.principalId === input.principalId && row.provider === input.provider)
|
|
59
|
+
if (row.isDefault && row.provider === input.provider && isInVaultQueryScope({ record: row, query: input }))
|
|
61
60
|
return structuredClone(row);
|
|
62
61
|
}
|
|
63
62
|
return null;
|
|
64
63
|
}
|
|
65
64
|
async list(input, _options = {}) {
|
|
66
|
-
return structuredClone([...this.rows.values()].filter(row =>
|
|
67
|
-
row.ownerKey === input.ownerKey && row.principalId === input.principalId && (input.provider === undefined || row.provider === input.provider))
|
|
65
|
+
return structuredClone([...this.rows.values()].filter(row => isInVaultQueryScope({ record: row, query: input }))
|
|
68
66
|
// Rows are keyed by (workspaceId, id), so one workspace never lists two equal IDs.
|
|
69
67
|
.sort((a, b) => a.id < b.id ? -1 : 1));
|
|
70
68
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { credentialTokenHint } from "../credential-token.js";
|
|
2
2
|
import { buildVaultAad } from "./vault.aad.js";
|
|
3
3
|
import { isCopyableKind } from "./vault.meta.js";
|
|
4
|
-
import { VAULT_KINDS, VaultError } from "./vault.types.js";
|
|
4
|
+
import { VAULT_KINDS, VaultError, isInVaultQueryScope } from "./vault.types.js";
|
|
5
5
|
/** Kinds whose payloads are addresses, environment maps or OAuth sessions, never single tokens. */
|
|
6
6
|
const HINTLESS_KINDS = ["database-destination", "mcp-env", "mcp-auth"];
|
|
7
7
|
function isNonEmptyString({ value }) {
|
|
8
8
|
return typeof value === "string" && value !== "";
|
|
9
9
|
}
|
|
10
|
+
/** The one error for malformed, missing or unsupported records and repo capabilities. */
|
|
11
|
+
function invalidRecord() {
|
|
12
|
+
return new VaultError({ code: "invalid-record" });
|
|
13
|
+
}
|
|
10
14
|
function isNullableString({ value }) {
|
|
11
15
|
return value === null || typeof value === "string";
|
|
12
16
|
}
|
|
13
17
|
function hasValidOwnership({ context }) {
|
|
18
|
+
const validBinding = !("binding" in context) || isNullableString({ value: context.binding });
|
|
14
19
|
return isNonEmptyString({ value: context.workspaceId }) && isNonEmptyString({ value: context.ownerKey }) &&
|
|
15
|
-
isNullableString({ value: context.principalId }) &&
|
|
20
|
+
isNullableString({ value: context.principalId }) && validBinding;
|
|
16
21
|
}
|
|
17
22
|
function hasValidKind({ context }) {
|
|
18
23
|
if (!VAULT_KINDS.some(kind => kind === context.kind) && context.kind !== "custom")
|
|
@@ -25,19 +30,19 @@ function assertContext(context) {
|
|
|
25
30
|
}
|
|
26
31
|
/** Unified credential rows declare both subtype and compatibility; read defaults cover older rows. */
|
|
27
32
|
function declaresCredential({ meta }) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return record.workspaceId === query.workspaceId && record.kind === query.kind && record.ownerKey === query.ownerKey &&
|
|
32
|
-
record.principalId === query.principalId && (query.provider === undefined || record.provider === query.provider);
|
|
33
|
+
if (meta === null || typeof meta !== "object")
|
|
34
|
+
return false;
|
|
35
|
+
return Object.hasOwn(meta, "credentialType") && Object.hasOwn(meta, "worksFor");
|
|
33
36
|
}
|
|
34
37
|
/** Copy changes the site/key, never the credential's owning feature or endpoint. Principal
|
|
35
38
|
* identities are local to each installation and cannot be carried to the target.
|
|
36
39
|
*/
|
|
37
40
|
function isCopyTarget({ sourceContext, targetContext, sameRepo }) {
|
|
38
|
-
|
|
39
|
-
sourceContext.binding === targetContext.binding
|
|
40
|
-
|
|
41
|
+
const sameFeature = sourceContext.kind === targetContext.kind && sourceContext.ownerKey === targetContext.ownerKey &&
|
|
42
|
+
sourceContext.binding === targetContext.binding;
|
|
43
|
+
const noPrincipal = sourceContext.principalId === null && targetContext.principalId === null;
|
|
44
|
+
const sameSite = sameRepo && sourceContext.workspaceId === targetContext.workspaceId;
|
|
45
|
+
return sameFeature && noPrincipal && !sameSite;
|
|
41
46
|
}
|
|
42
47
|
function sourceNeedsUpgrade({ raw }) {
|
|
43
48
|
return Object.hasOwn(raw.meta, "legacyAad") || (raw.kind === "vendor" && !declaresCredential({ meta: raw.meta }));
|
|
@@ -97,7 +102,7 @@ export class SecretsVaultService {
|
|
|
97
102
|
// JSON round-trip makes the payload contract explicit; encryption never receives an object
|
|
98
103
|
// coerced to '[object Object]' or a connection URI masquerading as an entire payload.
|
|
99
104
|
if (typeof input.payload !== "object" || Array.isArray(input.payload))
|
|
100
|
-
throw
|
|
105
|
+
throw invalidRecord();
|
|
101
106
|
const plaintext = JSON.stringify(input.payload);
|
|
102
107
|
return this.deps.sealer.seal({ plaintext, key: await this.deps.keyring.activeKey({}), aad: buildVaultAad(input.context) });
|
|
103
108
|
}
|
|
@@ -110,10 +115,10 @@ export class SecretsVaultService {
|
|
|
110
115
|
payload = JSON.parse(plaintext);
|
|
111
116
|
}
|
|
112
117
|
catch {
|
|
113
|
-
throw
|
|
118
|
+
throw invalidRecord();
|
|
114
119
|
}
|
|
115
120
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
|
116
|
-
throw
|
|
121
|
+
throw invalidRecord();
|
|
117
122
|
return payload;
|
|
118
123
|
}
|
|
119
124
|
/** Create/replace configuration and a sealed JSON payload. Throws on policy, validation or CAS failure. */
|
|
@@ -167,7 +172,7 @@ export class SecretsVaultService {
|
|
|
167
172
|
async putEnvelope({ input, identity, existing }) {
|
|
168
173
|
if (input.payload === undefined) {
|
|
169
174
|
if (!existing)
|
|
170
|
-
throw
|
|
175
|
+
throw invalidRecord();
|
|
171
176
|
return { sealed: existing.sealed, last4: existing.last4, tokenLength: existing.tokenLength };
|
|
172
177
|
}
|
|
173
178
|
const sealed = await this.seal({ context: identity, payload: input.payload });
|
|
@@ -195,7 +200,7 @@ export class SecretsVaultService {
|
|
|
195
200
|
assertContext(input);
|
|
196
201
|
await this.authorize({ action: "get", context: input });
|
|
197
202
|
if (!this.deps.repo.findOne)
|
|
198
|
-
throw
|
|
203
|
+
throw invalidRecord();
|
|
199
204
|
const record = await this.deps.repo.findOne(input);
|
|
200
205
|
if (!record)
|
|
201
206
|
return null;
|
|
@@ -213,7 +218,7 @@ export class SecretsVaultService {
|
|
|
213
218
|
await this.authorize({ action: "get", context: input });
|
|
214
219
|
// Old injected ports retain their existing operations; default reads require this capability.
|
|
215
220
|
if (!this.deps.repo.findDefault)
|
|
216
|
-
throw
|
|
221
|
+
throw invalidRecord();
|
|
217
222
|
const record = await this.deps.repo.findDefault(input);
|
|
218
223
|
if (!record)
|
|
219
224
|
return null;
|
|
@@ -301,7 +306,7 @@ export class SecretsVaultService {
|
|
|
301
306
|
assertContext(input);
|
|
302
307
|
await this.authorize({ action: "list", context: input });
|
|
303
308
|
return (await this.deps.repo.list(input)).map(record => {
|
|
304
|
-
if (!
|
|
309
|
+
if (!isInVaultQueryScope({ record, query: input }))
|
|
305
310
|
throw new VaultError({ code: "context" });
|
|
306
311
|
return summary(this.parse(record));
|
|
307
312
|
});
|
|
@@ -152,6 +152,12 @@ export interface VaultRepoPort {
|
|
|
152
152
|
expectedRevision: number;
|
|
153
153
|
}, options?: VaultOptions): Promise<boolean>;
|
|
154
154
|
}
|
|
155
|
+
/** The record belongs to the query's exact workspace, kind, owner and principal; an omitted provider
|
|
156
|
+
* matches every provider. @complexity O(1). */
|
|
157
|
+
export declare function isInVaultQueryScope({ record, query }: {
|
|
158
|
+
record: VaultRecord;
|
|
159
|
+
query: VaultQuery;
|
|
160
|
+
}, _options?: VaultOptions): boolean;
|
|
155
161
|
/** Unambiguous scope encoding keeps adapter default/identity checks on the same contract. */
|
|
156
162
|
export declare function vaultGroupKey({ record }: {
|
|
157
163
|
record: VaultDefaultQuery;
|
|
@@ -9,6 +9,13 @@ export class VaultError extends Error {
|
|
|
9
9
|
this.name = "VaultError";
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/** The record belongs to the query's exact workspace, kind, owner and principal; an omitted provider
|
|
13
|
+
* matches every provider. @complexity O(1). */
|
|
14
|
+
export function isInVaultQueryScope({ record, query }, _options = {}) {
|
|
15
|
+
const sameOwner = record.workspaceId === query.workspaceId && record.kind === query.kind && record.ownerKey === query.ownerKey;
|
|
16
|
+
const sameProvider = query.provider === undefined || record.provider === query.provider;
|
|
17
|
+
return sameOwner && record.principalId === query.principalId && sameProvider;
|
|
18
|
+
}
|
|
12
19
|
/** Unambiguous scope encoding keeps adapter default/identity checks on the same contract. */
|
|
13
20
|
export function vaultGroupKey({ record }, _options = {}) {
|
|
14
21
|
return JSON.stringify([record.workspaceId, record.kind, record.ownerKey, record.principalId, record.provider]);
|
|
@@ -60,10 +60,17 @@ export function credentialTokenHint({ token }, _optional = {}) {
|
|
|
60
60
|
return { length: token.length, last4: token.length >= 12 ? token.slice(-4) : null };
|
|
61
61
|
}
|
|
62
62
|
const SECRET_NAME = /(?:^|[_-])(?:key|api[_-]?key|token|access[_-]?token|secret|password|auth|sig)$/i;
|
|
63
|
-
|
|
63
|
+
// A `--flag` followed by a value; the flag name is captured whole and its suffix checked separately,
|
|
64
|
+
// so the name run and the keyword can no longer backtrack against each other.
|
|
65
|
+
const CLI_FLAG_WITH_VALUE = /(?:^|\s)--([\w-]*)(?=(?:=|\s+)\S)/gi;
|
|
66
|
+
const SECRET_FLAG_SUFFIX = /(?:api[_-]?key|token|secret|password)$/i;
|
|
67
|
+
/** True when some `--…api-key/token/secret/password` flag carries a value. @complexity O(value length). */
|
|
68
|
+
function hasSecretCliArg(value) {
|
|
69
|
+
return [...value.matchAll(CLI_FLAG_WITH_VALUE)].some(match => SECRET_FLAG_SUFFIX.test(match[1]));
|
|
70
|
+
}
|
|
64
71
|
/** Protect plaintext metadata, using Jini's detector plus structural URL/CLI rules. */
|
|
65
72
|
export function assertCredentialFreeField({ value, field, containsSecret }, _optional = {}) {
|
|
66
|
-
let credential = containsSecret({ text: value }) ||
|
|
73
|
+
let credential = containsSecret({ text: value }) || hasSecretCliArg(value);
|
|
67
74
|
try {
|
|
68
75
|
const url = new URL(value);
|
|
69
76
|
credential ||= url.username !== '' || url.password !== '' || [...url.searchParams.keys()].some(key => SECRET_NAME.test(key));
|
|
@@ -206,8 +206,7 @@ function toActiveRootKeyMaterial(source, raw) {
|
|
|
206
206
|
* @example inspectRootKeyMaterial({ env, envVarName, keyFilePath })
|
|
207
207
|
*/
|
|
208
208
|
export function inspectRootKeyMaterial(options) {
|
|
209
|
-
const envVarName = options
|
|
210
|
-
const keyFilePath = options.keyFilePath;
|
|
209
|
+
const { envVarName, keyFilePath } = options;
|
|
211
210
|
const raw = readActiveRootKeyMaterial({ env: options.env, envVarName, keyFilePath });
|
|
212
211
|
if (raw.hex)
|
|
213
212
|
return { active: true, source: raw.source, fingerprint: fingerprintRootKeyHex({ hex: raw.hex }), keyFilePath };
|
|
@@ -221,8 +220,7 @@ function invalidFields(raw) {
|
|
|
221
220
|
* @example revealRootKeyMaterial({ env, envVarName, keyFilePath })
|
|
222
221
|
*/
|
|
223
222
|
export function revealRootKeyMaterial(options) {
|
|
224
|
-
const envVarName = options
|
|
225
|
-
const keyFilePath = options.keyFilePath;
|
|
223
|
+
const { envVarName, keyFilePath } = options;
|
|
226
224
|
const raw = readActiveRootKeyMaterial({ env: options.env, envVarName, keyFilePath });
|
|
227
225
|
if (raw.hex)
|
|
228
226
|
return { active: true, source: raw.source, hex: raw.hex, fingerprint: fingerprintRootKeyHex({ hex: raw.hex }), keyFilePath };
|
package/dist/shell.js
CHANGED
|
@@ -41,7 +41,8 @@ export function execFileBuffered(command, args, opts = {}) {
|
|
|
41
41
|
/** @internal Single-quote a POSIX shell argument, escaping embedded single quotes. */
|
|
42
42
|
function quotePosixShellArg(value) {
|
|
43
43
|
const text = String(value ?? "");
|
|
44
|
-
|
|
44
|
+
const escapedQuote = "'\\''";
|
|
45
|
+
return `'${text.replace(/'/g, escapedQuote)}'`;
|
|
45
46
|
}
|
|
46
47
|
/** @internal Join a command and its arguments into one quoted POSIX shell command line. */
|
|
47
48
|
function buildShellCommandLine(command, args) {
|
package/dist/terminal.js
CHANGED
|
@@ -171,7 +171,8 @@ export function createTerminalService(options) {
|
|
|
171
171
|
}
|
|
172
172
|
};
|
|
173
173
|
const emit = (session, event, data) => {
|
|
174
|
-
const id = session.nextEventId
|
|
174
|
+
const id = session.nextEventId;
|
|
175
|
+
session.nextEventId += 1;
|
|
175
176
|
const byteLength = recordByteLength(data);
|
|
176
177
|
const record = { id, event, data, timestamp: Date.now(), byteLength };
|
|
177
178
|
session.events.push(record);
|
|
@@ -282,12 +283,8 @@ export function createTerminalService(options) {
|
|
|
282
283
|
return session ? toPublicSession(session) : null;
|
|
283
284
|
};
|
|
284
285
|
const list = (filter = {}) => Array.from(sessions.values())
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
return true;
|
|
290
|
-
})
|
|
286
|
+
// Only a non-empty string projectId filters; anything else lists every session.
|
|
287
|
+
.filter((session) => !(typeof filter.projectId === 'string' && filter.projectId && session.projectId !== filter.projectId))
|
|
291
288
|
.map(toPublicSession);
|
|
292
289
|
const attach = (id, lastEventId, sink) => {
|
|
293
290
|
const session = sessions.get(id);
|
package/dist/toolchain.js
CHANGED
|
@@ -255,18 +255,13 @@ function compareVersionLikeDirNames(left, right) {
|
|
|
255
255
|
const leftSemver = parseVersionLikeDirName(left);
|
|
256
256
|
const rightSemver = parseVersionLikeDirName(right);
|
|
257
257
|
if (leftSemver && rightSemver) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (difference !== 0)
|
|
261
|
-
return difference;
|
|
262
|
-
}
|
|
258
|
+
const difference = leftSemver.map((part, index) => rightSemver[index] - part).find((value) => value !== 0);
|
|
259
|
+
return difference ?? left.localeCompare(right);
|
|
263
260
|
}
|
|
264
|
-
|
|
261
|
+
if (leftSemver)
|
|
265
262
|
return -1;
|
|
266
|
-
|
|
267
|
-
else if (rightSemver) {
|
|
263
|
+
if (rightSemver)
|
|
268
264
|
return 1;
|
|
269
|
-
}
|
|
270
265
|
return left.localeCompare(right);
|
|
271
266
|
}
|
|
272
267
|
/** @internal Parse a `vX.Y.Z` / `X.Y.Z` directory name into its numeric parts, or `null` when it does not match. */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Items with distinct non-empty keys, in order, capped at `max`; `truncated` reports a further
|
|
2
|
+
* distinct key beyond the cap (scanning stops there). @complexity O(items). */
|
|
3
|
+
export declare function firstDistinct<T>({ items, max, keyOf }: {
|
|
4
|
+
items: readonly T[];
|
|
5
|
+
max: number;
|
|
6
|
+
keyOf: (item: T) => string | null | undefined;
|
|
7
|
+
}): {
|
|
8
|
+
picked: [string, T][];
|
|
9
|
+
truncated: boolean;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=first-distinct.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Items with distinct non-empty keys, in order, capped at `max`; `truncated` reports a further
|
|
2
|
+
* distinct key beyond the cap (scanning stops there). @complexity O(items). */
|
|
3
|
+
export function firstDistinct({ items, max, keyOf }) {
|
|
4
|
+
const picked = new Map();
|
|
5
|
+
const truncated = items.some((item) => {
|
|
6
|
+
const key = keyOf(item);
|
|
7
|
+
if (!key || picked.has(key))
|
|
8
|
+
return false;
|
|
9
|
+
if (picked.size === max)
|
|
10
|
+
return true;
|
|
11
|
+
picked.set(key, item);
|
|
12
|
+
return false;
|
|
13
|
+
});
|
|
14
|
+
return { picked: [...picked], truncated };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=first-distinct.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parse, serialize } from "parse5";
|
|
2
|
+
import { firstDistinct } from "./first-distinct.js";
|
|
2
3
|
export const MAX_LINKS = 500;
|
|
3
4
|
export const MAX_IMAGES = 300;
|
|
4
5
|
const MAX_STYLESHEETS = 100;
|
|
@@ -88,19 +89,25 @@ const COLLECTED = new Map([["a", "anchors"], ["img", "images"], ["link", "linkTa
|
|
|
88
89
|
function collect(document) {
|
|
89
90
|
const raw = { anchors: [], images: [], linkTags: [], metas: [] };
|
|
90
91
|
for (const node of walk(document)) {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
const list = COLLECTED.get(node.tagName);
|
|
94
|
-
if (list)
|
|
95
|
-
raw[list].push(node);
|
|
96
|
-
else if (node.tagName === "title" || node.tagName === "html")
|
|
97
|
-
raw[node.tagName] ??= node;
|
|
98
|
-
// The first base WITH an href wins, as in browsers.
|
|
99
|
-
else if (node.tagName === "base")
|
|
100
|
-
raw.base ??= attr({ element: node, name: "href" });
|
|
92
|
+
if (isElement(node))
|
|
93
|
+
collectElement(raw, node);
|
|
101
94
|
}
|
|
102
95
|
return raw;
|
|
103
96
|
}
|
|
97
|
+
function collectElement(raw, node) {
|
|
98
|
+
const list = COLLECTED.get(node.tagName);
|
|
99
|
+
if (list) {
|
|
100
|
+
raw[list].push(node);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (node.tagName === "title" || node.tagName === "html") {
|
|
104
|
+
raw[node.tagName] ??= node;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// The first base WITH an href wins, as in browsers.
|
|
108
|
+
if (node.tagName === "base")
|
|
109
|
+
raw.base ??= attr({ element: node, name: "href" });
|
|
110
|
+
}
|
|
104
111
|
/** Meta keys whose values are URLs and are returned absolute. */
|
|
105
112
|
const URL_META = new Set(["og:image", "og:url", "twitter:image"]);
|
|
106
113
|
/** `<link rel>` tokens copied into meta, in emission order, under their meta key. */
|
|
@@ -127,11 +134,16 @@ function putMeta({ meta, key, value }, _optional = {}) {
|
|
|
127
134
|
return;
|
|
128
135
|
meta[key] = collapse(value).slice(0, MAX_META_VALUE);
|
|
129
136
|
}
|
|
137
|
+
function metaKey({ property, name }) {
|
|
138
|
+
if (property?.startsWith("og:"))
|
|
139
|
+
return property;
|
|
140
|
+
return name && META_NAMES.has(name) ? name : undefined;
|
|
141
|
+
}
|
|
130
142
|
/** Every `og:*` property, or an allowlisted `name`; URL-valued keys are resolved. */
|
|
131
143
|
function metaTagEntry({ element, base }, _optional = {}) {
|
|
132
144
|
const property = attr({ element, name: "property" })?.toLowerCase();
|
|
133
145
|
const name = attr({ element, name: "name" })?.toLowerCase();
|
|
134
|
-
const key =
|
|
146
|
+
const key = metaKey({ property, name });
|
|
135
147
|
if (key === undefined)
|
|
136
148
|
return undefined;
|
|
137
149
|
const content = attr({ element, name: "content" });
|
|
@@ -141,37 +153,21 @@ function relTokens({ element }, _optional = {}) {
|
|
|
141
153
|
return (attr({ element, name: "rel" }) ?? "").toLowerCase().split(/\s+/);
|
|
142
154
|
}
|
|
143
155
|
function readLinks(anchors, base, pageHost) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
continue;
|
|
151
|
-
seen.add(href);
|
|
152
|
-
if (links.length === MAX_LINKS) {
|
|
153
|
-
truncated = true;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
links.push({ href, text: linkText(anchor), internal: isInternal(href, pageHost) });
|
|
157
|
-
}
|
|
156
|
+
const { picked, truncated } = firstDistinct({
|
|
157
|
+
items: anchors,
|
|
158
|
+
max: MAX_LINKS,
|
|
159
|
+
keyOf: (anchor) => absolute({ raw: attr({ element: anchor, name: "href" }), base, protocols: LINK_PROTOCOLS }),
|
|
160
|
+
});
|
|
161
|
+
const links = picked.map(([href, anchor]) => ({ href, text: linkText(anchor), internal: isInternal(href, pageHost) }));
|
|
158
162
|
return { links, truncated };
|
|
159
163
|
}
|
|
160
164
|
function readImages(images, base) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
continue;
|
|
168
|
-
seen.add(src);
|
|
169
|
-
if (out.length === MAX_IMAGES) {
|
|
170
|
-
truncated = true;
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
out.push({ src, alt: collapse(attr({ element: img, name: "alt" }) ?? "") });
|
|
174
|
-
}
|
|
165
|
+
const { picked, truncated } = firstDistinct({
|
|
166
|
+
items: images,
|
|
167
|
+
max: MAX_IMAGES,
|
|
168
|
+
keyOf: (img) => absolute({ raw: imageSource({ img }), base, protocols: ASSET_PROTOCOLS }),
|
|
169
|
+
});
|
|
170
|
+
const out = picked.map(([src, img]) => ({ src, alt: collapse(attr({ element: img, name: "alt" }) ?? "") }));
|
|
175
171
|
return { images: out, truncated };
|
|
176
172
|
}
|
|
177
173
|
function readStylesheets(linkTags, base) {
|
|
@@ -12,18 +12,25 @@ const MAX_DEPTH = 300;
|
|
|
12
12
|
function isElement(node) {
|
|
13
13
|
return "tagName" in node;
|
|
14
14
|
}
|
|
15
|
+
/** A node's children for {@link plainText}; skipped elements and leaves contribute none. */
|
|
16
|
+
function scannableChildren(node) {
|
|
17
|
+
if (!("childNodes" in node) || (isElement(node) && SKIPPED.has(node.tagName)))
|
|
18
|
+
return [];
|
|
19
|
+
return node.childNodes;
|
|
20
|
+
}
|
|
15
21
|
/** Iterative so a degraded, very deep subtree cannot overflow the stack either. O(subtree nodes). */
|
|
16
22
|
function plainText(root) {
|
|
17
23
|
const pending = [root];
|
|
18
24
|
const parts = [];
|
|
19
25
|
while (pending.length) {
|
|
20
26
|
const node = pending.pop();
|
|
21
|
-
if (node.nodeName === "#text" && "value" in node)
|
|
27
|
+
if (node.nodeName === "#text" && "value" in node) {
|
|
22
28
|
parts.push(node.value);
|
|
23
|
-
|
|
24
|
-
for (let i = node.childNodes.length - 1; i >= 0; i--)
|
|
25
|
-
pending.push(node.childNodes[i]);
|
|
29
|
+
continue;
|
|
26
30
|
}
|
|
31
|
+
const children = scannableChildren(node);
|
|
32
|
+
for (let i = children.length - 1; i >= 0; i--)
|
|
33
|
+
pending.push(children[i]);
|
|
27
34
|
}
|
|
28
35
|
return parts.join("").replace(/\s+/g, " ");
|
|
29
36
|
}
|
|
@@ -112,9 +119,11 @@ function takeEdgeWhitespace({ node, last }, _options = {}) {
|
|
|
112
119
|
const edge = edgeText({ node, last });
|
|
113
120
|
if (!edge)
|
|
114
121
|
return "";
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
const value = edge.value;
|
|
123
|
+
// trimStart/trimEnd strip exactly the characters `\s` matches.
|
|
124
|
+
const kept = last ? value.trimEnd() : value.trimStart();
|
|
125
|
+
edge.value = kept;
|
|
126
|
+
return last ? value.slice(kept.length) : value.slice(0, value.length - kept.length);
|
|
118
127
|
}
|
|
119
128
|
/** The first/last leaf of the subtree when it is a text node. */
|
|
120
129
|
function edgeText({ node, last }, _options = {}) {
|
|
@@ -141,9 +150,11 @@ function hastText(root) {
|
|
|
141
150
|
while (pending.length) {
|
|
142
151
|
const node = pending.pop();
|
|
143
152
|
// HAST text nodes always carry a value.
|
|
144
|
-
if (node.type === "text")
|
|
153
|
+
if (node.type === "text") {
|
|
145
154
|
parts.push(node.value);
|
|
146
|
-
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (node.children)
|
|
147
158
|
for (let i = node.children.length - 1; i >= 0; i--)
|
|
148
159
|
pending.push(node.children[i]);
|
|
149
160
|
}
|