@remogram/core 0.1.0-beta.0 → 0.1.0-beta.1
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/caps.js +32 -1
- package/git-local.js +2 -0
- package/http.js +49 -4
- package/index.js +12 -2
- package/package.json +1 -1
package/caps.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
export const DEFAULT_MAX_BYTES = 8192;
|
|
2
2
|
export const DEFAULT_FIELD_MAX_BYTES = 512;
|
|
3
|
+
export const FORGE_INGEST_MAX_BYTES_ENV = 'REMOGRAM_FORGE_INGEST_MAX_BYTES';
|
|
4
|
+
|
|
5
|
+
export function getEffectiveIngestMaxBytes() {
|
|
6
|
+
const raw = process.env[FORGE_INGEST_MAX_BYTES_ENV];
|
|
7
|
+
if (raw == null || raw === '') {
|
|
8
|
+
return { bytes: DEFAULT_MAX_BYTES, envOverride: false };
|
|
9
|
+
}
|
|
10
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
11
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
12
|
+
return { bytes: DEFAULT_MAX_BYTES, envOverride: false, invalidEnv: true };
|
|
13
|
+
}
|
|
14
|
+
return { bytes: parsed, envOverride: true };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Facts for provider capabilities packets (forge ingest policy). */
|
|
18
|
+
export function forgeIngestCapabilityFacts() {
|
|
19
|
+
const { bytes } = getEffectiveIngestMaxBytes();
|
|
20
|
+
return { forge_ingest_cap_bytes: bytes };
|
|
21
|
+
}
|
|
3
22
|
|
|
4
23
|
export function capText(text, maxBytes = DEFAULT_MAX_BYTES) {
|
|
5
24
|
if (!text) return { text: '', truncated: false, bytes: 0 };
|
|
@@ -19,7 +38,17 @@ export function sanitizeField(value, maxBytes = DEFAULT_FIELD_MAX_BYTES) {
|
|
|
19
38
|
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
20
39
|
.replace(/\r?\n/g, ' ')
|
|
21
40
|
.trim();
|
|
22
|
-
|
|
41
|
+
const redacted = redactSecretPatterns(singleLine);
|
|
42
|
+
return capText(redacted, maxBytes).text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function redactSecretPatterns(text) {
|
|
46
|
+
return text
|
|
47
|
+
.replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]')
|
|
48
|
+
.replace(/\bghp_[A-Za-z0-9]+\b/g, '[REDACTED]')
|
|
49
|
+
.replace(/\bgho_[A-Za-z0-9]+\b/g, '[REDACTED]')
|
|
50
|
+
.replace(/\bglpat-[A-Za-z0-9_-]+\b/g, '[REDACTED]')
|
|
51
|
+
.replace(/\b(GITHUB_TOKEN|GH_TOKEN|GITLAB_TOKEN|GITEA_TOKEN)\b/gi, '[REDACTED]');
|
|
23
52
|
}
|
|
24
53
|
|
|
25
54
|
export function sanitizeUrl(value, maxBytes = DEFAULT_FIELD_MAX_BYTES) {
|
|
@@ -27,6 +56,8 @@ export function sanitizeUrl(value, maxBytes = DEFAULT_FIELD_MAX_BYTES) {
|
|
|
27
56
|
try {
|
|
28
57
|
const u = new URL(String(value));
|
|
29
58
|
if (u.protocol !== 'http:' && u.protocol !== 'https:') return null;
|
|
59
|
+
u.username = '';
|
|
60
|
+
u.password = '';
|
|
30
61
|
return sanitizeField(u.href, maxBytes);
|
|
31
62
|
} catch {
|
|
32
63
|
return null;
|
package/git-local.js
CHANGED
|
@@ -25,6 +25,8 @@ export function gitCurrentBranch(cwd) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function gitAheadBehind(cwd, base, head) {
|
|
28
|
+
assertGitRef(base, 'base');
|
|
29
|
+
assertGitRef(head, 'head');
|
|
28
30
|
try {
|
|
29
31
|
const out = gitExec(cwd, ['rev-list', '--left-right', '--count', `${base}...${head}`]);
|
|
30
32
|
const [behind, ahead] = out.split(/\s+/).map(Number);
|
package/http.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
2
|
-
import { readStreamCapped,
|
|
2
|
+
import { readStreamCapped, getEffectiveIngestMaxBytes, sanitizeField } from './caps.js';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
-
const DEFAULT_JSON_MAX_BYTES =
|
|
5
|
+
const DEFAULT_JSON_MAX_BYTES = () => getEffectiveIngestMaxBytes().bytes;
|
|
6
6
|
|
|
7
7
|
export async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
8
8
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
@@ -25,7 +25,7 @@ export async function fetchJson(
|
|
|
25
25
|
url,
|
|
26
26
|
options = {},
|
|
27
27
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
28
|
-
maxBytes = DEFAULT_JSON_MAX_BYTES,
|
|
28
|
+
maxBytes = DEFAULT_JSON_MAX_BYTES(),
|
|
29
29
|
) {
|
|
30
30
|
const res = await fetchWithTimeout(url, options, timeoutMs);
|
|
31
31
|
if (res.status >= 300 && res.status < 400) {
|
|
@@ -56,7 +56,52 @@ export async function fetchJson(
|
|
|
56
56
|
return body;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export
|
|
59
|
+
export function parseLinkHeader(linkHeader) {
|
|
60
|
+
if (!linkHeader) return {};
|
|
61
|
+
const links = {};
|
|
62
|
+
for (const segment of String(linkHeader).split(',')) {
|
|
63
|
+
const match = segment.trim().match(/^<([^>]+)>;\s*rel="([^"]+)"/);
|
|
64
|
+
if (match) links[match[2]] = match[1];
|
|
65
|
+
}
|
|
66
|
+
return links;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function fetchJsonWithMeta(
|
|
70
|
+
url,
|
|
71
|
+
options = {},
|
|
72
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
73
|
+
maxBytes = DEFAULT_JSON_MAX_BYTES(),
|
|
74
|
+
) {
|
|
75
|
+
const res = await fetchWithTimeout(url, options, timeoutMs);
|
|
76
|
+
if (res.status >= 300 && res.status < 400) {
|
|
77
|
+
const message = 'HTTP redirect rejected';
|
|
78
|
+
throw Object.assign(new Error(message), {
|
|
79
|
+
forgeError: forgeError(ERROR_CODES.API_ERROR, message, res.status),
|
|
80
|
+
status: res.status,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const text = await readResponseTextCapped(res, maxBytes);
|
|
84
|
+
let body;
|
|
85
|
+
try {
|
|
86
|
+
body = text ? JSON.parse(text) : null;
|
|
87
|
+
} catch {
|
|
88
|
+
throw Object.assign(new Error('Unparseable JSON from provider'), {
|
|
89
|
+
forgeError: forgeError(ERROR_CODES.UNPARSEABLE_PROVIDER_OUTPUT, 'Provider returned invalid JSON'),
|
|
90
|
+
status: res.status,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const raw = body?.message || body?.error || res.statusText || 'API error';
|
|
95
|
+
const message = sanitizeField(raw) || 'API error';
|
|
96
|
+
throw Object.assign(new Error(message), {
|
|
97
|
+
forgeError: forgeError(ERROR_CODES.API_ERROR, message, res.status),
|
|
98
|
+
status: res.status,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { body, headers: res.headers, status: res.status };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function fetchTextCapped(url, options = {}, maxBytes = getEffectiveIngestMaxBytes().bytes) {
|
|
60
105
|
const res = await fetchWithTimeout(url, options);
|
|
61
106
|
if (res.status >= 300 && res.status < 400) {
|
|
62
107
|
const message = 'HTTP redirect rejected';
|
package/index.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export { SCHEMA_VERSION, PACKET_TYPES, forgePacket, forgeErrorPacket, unknownForgeContext, FORBIDDEN_PACKET_KEYS } from './contracts/envelope.js';
|
|
2
2
|
export { ERROR_CODES, forgeError } from './contracts/errors.js';
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
capText,
|
|
5
|
+
sanitizeField,
|
|
6
|
+
sanitizeUrl,
|
|
7
|
+
readStreamCapped,
|
|
8
|
+
DEFAULT_MAX_BYTES,
|
|
9
|
+
DEFAULT_FIELD_MAX_BYTES,
|
|
10
|
+
FORGE_INGEST_MAX_BYTES_ENV,
|
|
11
|
+
getEffectiveIngestMaxBytes,
|
|
12
|
+
forgeIngestCapabilityFacts,
|
|
13
|
+
} from './caps.js';
|
|
4
14
|
export { assertGitRef, assertGitRemote } from './git-args.js';
|
|
5
15
|
export { gitRevParse, gitCurrentBranch, gitAheadBehind } from './git-local.js';
|
|
6
16
|
export { parseConfigFile, configSchema } from './config-schema.js';
|
|
@@ -14,4 +24,4 @@ export {
|
|
|
14
24
|
assertForgeReady,
|
|
15
25
|
forgeContext,
|
|
16
26
|
} from './resolve.js';
|
|
17
|
-
export { fetchWithTimeout, fetchJson, fetchTextCapped } from './http.js';
|
|
27
|
+
export { fetchWithTimeout, fetchJson, fetchJsonWithMeta, parseLinkHeader, fetchTextCapped } from './http.js';
|