@cassiomc1/forgeloop 0.1.13 → 0.1.14

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,80 @@
1
+ export const AUTHORITY_TRUST_MODES = Object.freeze(["NONE", "HOST_ATTESTED"]);
2
+
3
+ const AUTHORITY_CONTEXT_FIELDS = Object.freeze([
4
+ "trustedAuthorityFile",
5
+ "trustedAuthorityDir",
6
+ "authorities",
7
+ "authority",
8
+ ]);
9
+
10
+ function configuredValue(value) {
11
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
12
+ }
13
+
14
+ function providerValue(provider) {
15
+ if (typeof provider === "function") return provider();
16
+ return provider && typeof provider === "object" && !Array.isArray(provider) ? provider : {};
17
+ }
18
+
19
+ function contextInput(input = {}) {
20
+ const source = input && typeof input === "object" && !Array.isArray(input) ? input : {};
21
+ const provider = providerValue(source.trustedAuthorityProvider);
22
+ const explicit = source.authorityContext && typeof source.authorityContext === "object"
23
+ ? source.authorityContext
24
+ : {};
25
+ const { authorityContext: _authorityContext, trustedAuthorityProvider: _trustedAuthorityProvider, ...direct } = source;
26
+ return { ...direct, ...provider, ...explicit };
27
+ }
28
+
29
+ export function hasAuthorityContext(options = {}) {
30
+ if (!options || typeof options !== "object") return false;
31
+ return Boolean(
32
+ options.authorityContext
33
+ || options.runtimeContext?.authorityContext
34
+ || (options.runtimeContext && typeof options.runtimeContext === "object" && options.runtimeContext.trustMode),
35
+ );
36
+ }
37
+
38
+ export function createAuthorityContext(input = {}) {
39
+ const values = contextInput(input);
40
+ const trustMode = values.trustMode ?? values.authorityTrustMode ?? "NONE";
41
+ if (!AUTHORITY_TRUST_MODES.includes(trustMode)) {
42
+ const error = new Error(`Unsupported authority trust mode: ${trustMode}`);
43
+ error.code = "E_AUTHORITY_INVALID";
44
+ throw error;
45
+ }
46
+
47
+ const context = { trustMode };
48
+ for (const field of AUTHORITY_CONTEXT_FIELDS) {
49
+ if (field === "trustedAuthorityFile" || field === "trustedAuthorityDir") {
50
+ const value = configuredValue(values[field]);
51
+ if (value !== undefined) context[field] = value;
52
+ } else if (values[field] !== undefined) {
53
+ context[field] = values[field];
54
+ }
55
+ }
56
+ return Object.freeze(context);
57
+ }
58
+
59
+ export function resolveAuthorityContext(options = {}) {
60
+ const source = options && typeof options === "object" && !Array.isArray(options) ? options : {};
61
+ if (source.authorityContext !== undefined) {
62
+ return createAuthorityContext(source.authorityContext);
63
+ }
64
+ if (source.runtimeContext?.authorityContext !== undefined) {
65
+ return createAuthorityContext(source.runtimeContext.authorityContext);
66
+ }
67
+ if (source.runtimeContext && typeof source.runtimeContext === "object" && source.runtimeContext.trustMode) {
68
+ return createAuthorityContext(source.runtimeContext);
69
+ }
70
+
71
+ const hasDirectAuthority = AUTHORITY_CONTEXT_FIELDS.some((field) => source[field] !== undefined);
72
+ // Direct resolver options are legacy source selectors, not a host attestation
73
+ // channel. They remain NONE unless wrapped in an explicit runtime context.
74
+ return hasDirectAuthority ? createAuthorityContext({ ...source, trustMode: "NONE" }) : createAuthorityContext();
75
+ }
76
+
77
+ export function createForgeLoopContext(options = {}) {
78
+ const authorityContext = createAuthorityContext(options);
79
+ return Object.freeze({ authorityContext });
80
+ }
@@ -0,0 +1,296 @@
1
+ import { accessSync, constants as fsConstants, lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
5
+ import {
6
+ hasAuthorityContext,
7
+ resolveAuthorityContext,
8
+ } from "./runtime-context.js";
9
+
10
+ export const E_AUTHORITY_UNTRUSTED_SOURCE = "E_AUTHORITY_UNTRUSTED_SOURCE";
11
+ const E_AUTHORITY_INVALID = "E_AUTHORITY_INVALID";
12
+ const AUTHORITY_FILE_ENV = "FORGELOOP_AUTHORITY_FILE";
13
+ const AUTHORITY_DIR_ENV = "FORGELOOP_AUTHORITY_DIR";
14
+
15
+ function configuredValue(value) {
16
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
17
+ }
18
+
19
+ function authoritySourceOptions(options = {}) {
20
+ const context = resolveAuthorityContext(options);
21
+ const contextProvided = hasAuthorityContext(options);
22
+ const directFile = configuredValue(options.trustedAuthorityFile);
23
+ const directDir = configuredValue(options.trustedAuthorityDir);
24
+ const contextFile = configuredValue(context.trustedAuthorityFile);
25
+ const contextDir = configuredValue(context.trustedAuthorityDir);
26
+ const file = contextFile ?? (contextProvided ? null : directFile);
27
+ const dir = contextDir ?? (contextProvided ? null : directDir);
28
+ const authorities = context.authorities ?? (contextProvided ? undefined : options.authorities);
29
+ const authority = context.authority ?? (contextProvided ? undefined : options.authority);
30
+
31
+ if (contextProvided
32
+ && context.trustMode !== "HOST_ATTESTED"
33
+ && !file
34
+ && !dir
35
+ && authorities === undefined
36
+ && authority === undefined) {
37
+ const envFile = configuredValue(process.env[AUTHORITY_FILE_ENV]);
38
+ const envDir = configuredValue(process.env[AUTHORITY_DIR_ENV]);
39
+ return {
40
+ context,
41
+ file: envFile,
42
+ dir: envDir,
43
+ authorities: undefined,
44
+ authority: undefined,
45
+ sourceConfigured: Boolean(envFile || envDir),
46
+ sourceType: envFile ? "external-file" : envDir ? "external-dir" : null,
47
+ };
48
+ }
49
+
50
+ if (context.trustMode === "HOST_ATTESTED" || contextProvided || file || dir || authorities !== undefined || authority !== undefined) {
51
+ return {
52
+ context,
53
+ file,
54
+ dir,
55
+ authorities,
56
+ authority,
57
+ sourceConfigured: Boolean(file || dir || authorities !== undefined || authority !== undefined),
58
+ sourceType: file ? "external-file" : dir ? "external-dir" : authorities !== undefined || authority !== undefined ? "in-memory" : null,
59
+ };
60
+ }
61
+
62
+ const envFile = configuredValue(process.env[AUTHORITY_FILE_ENV]);
63
+ const envDir = configuredValue(process.env[AUTHORITY_DIR_ENV]);
64
+ return {
65
+ context,
66
+ file: envFile,
67
+ dir: envDir,
68
+ authorities: undefined,
69
+ authority: undefined,
70
+ sourceConfigured: Boolean(envFile || envDir),
71
+ sourceType: envFile ? "external-file" : envDir ? "external-dir" : null,
72
+ };
73
+ }
74
+
75
+ export function trustedAuthorityConfiguration(options = {}) {
76
+ const { context, sourceConfigured, sourceType, file, dir } = authoritySourceOptions(options);
77
+ const sourceInsideTarget = options.target && (file || dir)
78
+ ? isInsideTarget(options.target, file ?? dir)
79
+ : false;
80
+ return {
81
+ sourceConfigured,
82
+ sourceType,
83
+ trustMode: context.trustMode,
84
+ trusted: context.trustMode === "HOST_ATTESTED" && sourceConfigured && !sourceInsideTarget,
85
+ };
86
+ }
87
+
88
+ function invalid(message, details = {}) {
89
+ return {
90
+ trusted: false,
91
+ error: { code: E_AUTHORITY_INVALID, message },
92
+ ...details,
93
+ };
94
+ }
95
+
96
+ function untrusted(message) {
97
+ return {
98
+ trusted: false,
99
+ error: { code: E_AUTHORITY_UNTRUSTED_SOURCE, message },
100
+ };
101
+ }
102
+
103
+ function authorityFileName(authorityRef) {
104
+ if (typeof authorityRef !== "string" || authorityRef.trim() === "") return null;
105
+ const ref = authorityRef.trim();
106
+ if (ref === "." || ref === ".." || ref.includes("/") || ref.includes("\\")) return null;
107
+ return ref.endsWith(".json") ? ref : `${ref}.json`;
108
+ }
109
+
110
+ function resolvedPath(candidate) {
111
+ const absolute = path.resolve(candidate);
112
+ const missingSegments = [];
113
+ let current = absolute;
114
+ while (true) {
115
+ try {
116
+ const existing = realpathSync(current);
117
+ return path.resolve(existing, ...missingSegments);
118
+ } catch (error) {
119
+ if (error.code !== "ENOENT") throw error;
120
+ const parent = path.dirname(current);
121
+ if (parent === current) return absolute;
122
+ missingSegments.unshift(path.basename(current));
123
+ current = parent;
124
+ }
125
+ }
126
+ }
127
+
128
+ function isWithin(parent, candidate) {
129
+ const relative = path.relative(parent, candidate);
130
+ return relative === ""
131
+ || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
132
+ }
133
+
134
+ function isInsideTarget(target, candidate) {
135
+ if (!target) return false;
136
+ // Check both lexical paths and their real paths. The lexical check rejects
137
+ // actor-created symlinks inside the target that point at an external file.
138
+ return isWithin(path.resolve(target), path.resolve(candidate))
139
+ || isWithin(resolvedPath(target), resolvedPath(candidate));
140
+ }
141
+
142
+ function readExternalJson(candidate, target, kind) {
143
+ if (isInsideTarget(target, candidate)) {
144
+ return untrusted(`Configured trusted authority ${kind} must be outside the actor-writable target`);
145
+ }
146
+
147
+ try {
148
+ accessSync(candidate, fsConstants.R_OK);
149
+ const info = lstatSync(candidate);
150
+ if (kind === "file" && !info.isFile()) {
151
+ return invalid("Configured trusted authority file is not a regular file");
152
+ }
153
+ if (kind === "directory" && !info.isDirectory()) {
154
+ return invalid("Configured trusted authority directory is not a directory");
155
+ }
156
+ const bytes = readFileSync(candidate);
157
+ assertJsonBytes(bytes, `trusted authority ${kind}`);
158
+ const value = JSON.parse(bytes.toString("utf8"));
159
+ assertJsonLimits(value, `trusted authority ${kind}`);
160
+ return { value };
161
+ } catch {
162
+ return invalid(`Configured trusted authority ${kind} could not be read or parsed`);
163
+ }
164
+ }
165
+
166
+ function grantsFromValue(value) {
167
+ if (Array.isArray(value)) return value;
168
+ if (value && typeof value === "object" && Array.isArray(value.authorities)) {
169
+ if (value.schemaVersion !== 1 || value.protocolVersion !== 1) return null;
170
+ return value.authorities;
171
+ }
172
+ return value && typeof value === "object" ? [value] : null;
173
+ }
174
+
175
+ function findAuthority(value, authorityRef) {
176
+ if (value && typeof value === "object" && !Array.isArray(value)) {
177
+ const keyedAuthority = value[authorityRef];
178
+ if (keyedAuthority && typeof keyedAuthority === "object") return keyedAuthority;
179
+ }
180
+ const envelope = value && typeof value === "object" && !Array.isArray(value)
181
+ && Array.isArray(value.authorities)
182
+ ? value
183
+ : null;
184
+ const grants = grantsFromValue(value);
185
+ if (!grants) return null;
186
+ const authority = grants.find((item) => item?.authorityId === authorityRef || item?.id === authorityRef) ?? null;
187
+ if (!authority || !envelope) return authority;
188
+ return {
189
+ ...authority,
190
+ schemaVersion: envelope.schemaVersion,
191
+ protocolVersion: envelope.protocolVersion,
192
+ };
193
+ }
194
+
195
+ function findInMemoryAuthority(authorities, authority, authorityRef) {
196
+ if (authority !== undefined) {
197
+ return authority?.authorityId === authorityRef || authority?.id === authorityRef ? authority : null;
198
+ }
199
+ if (authorities !== undefined) {
200
+ return findAuthority(authorities, authorityRef);
201
+ }
202
+ return undefined;
203
+ }
204
+
205
+ function resolveFromExternalFile(authorityRef, target, file) {
206
+ const read = readExternalJson(file, target, "file");
207
+ if (read.error) return read;
208
+ const authority = findAuthority(read.value, authorityRef);
209
+ return authority
210
+ ? { trusted: true, authority, sourceType: "external-file" }
211
+ : invalid(`Referenced installation authority '${authorityRef}' was not found in the trusted authority file`);
212
+ }
213
+
214
+ function resolveFromExternalDirectory(authorityRef, target, dir) {
215
+ const fileName = authorityFileName(authorityRef);
216
+ if (!fileName) return invalid("Installation authority reference must be a simple authority ID");
217
+ if (isInsideTarget(target, dir)) {
218
+ return untrusted("Configured trusted authority directory must be outside the actor-writable target");
219
+ }
220
+ try {
221
+ accessSync(dir, fsConstants.R_OK);
222
+ if (!lstatSync(dir).isDirectory()) {
223
+ return invalid("Configured trusted authority directory is not a directory");
224
+ }
225
+ } catch {
226
+ return invalid("Configured trusted authority directory could not be read");
227
+ }
228
+ const authorityPath = path.join(dir, fileName);
229
+ const read = readExternalJson(authorityPath, target, "file");
230
+ if (read.error) return read;
231
+ const authority = findAuthority(read.value, authorityRef);
232
+ return authority
233
+ ? { trusted: true, authority, sourceType: "external-dir" }
234
+ : invalid(`Referenced installation authority '${authorityRef}' does not match the trusted authority file`);
235
+ }
236
+
237
+ function projectLocalAuthorityExists(authorityRef, target) {
238
+ const fileName = authorityFileName(authorityRef);
239
+ if (!fileName || !target) return false;
240
+ const localPath = path.join(target, ".forgeloop", "authorities", fileName);
241
+ try {
242
+ accessSync(localPath, fsConstants.F_OK);
243
+ return true;
244
+ } catch (error) {
245
+ if (error.code === "ENOENT") return false;
246
+ return true;
247
+ }
248
+ }
249
+
250
+ export function resolveTrustedAuthority({
251
+ authorityRef,
252
+ target,
253
+ trustedAuthorityFile,
254
+ trustedAuthorityDir,
255
+ authorities,
256
+ authority,
257
+ authorityContext,
258
+ runtimeContext,
259
+ } = {}) {
260
+ const fileName = authorityFileName(authorityRef);
261
+ if (!fileName) return invalid("Installation authority reference must be a simple authority ID");
262
+
263
+ const sources = authoritySourceOptions({
264
+ trustedAuthorityFile,
265
+ trustedAuthorityDir,
266
+ authorities,
267
+ authority,
268
+ authorityContext,
269
+ runtimeContext,
270
+ });
271
+ if (sources.sourceConfigured && sources.context.trustMode !== "HOST_ATTESTED") {
272
+ return untrusted("Standalone configuration selects an authority source but does not attest host authority");
273
+ }
274
+ if ((sources.file || sources.dir) && !target) {
275
+ return untrusted("A target path is required to validate trusted authority provenance");
276
+ }
277
+ if (sources.file) return resolveFromExternalFile(authorityRef, target, sources.file);
278
+ if (sources.dir) return resolveFromExternalDirectory(authorityRef, target, sources.dir);
279
+
280
+ // In-memory authorities are a host-injected policy interface. They never
281
+ // originate from project-local files and are intentionally resolved only
282
+ // after the explicit external sources above.
283
+ const inMemory = findInMemoryAuthority(sources.authorities, sources.authority, authorityRef);
284
+ if (inMemory !== undefined) {
285
+ return inMemory
286
+ ? { trusted: true, authority: inMemory, sourceType: "in-memory" }
287
+ : invalid(`Referenced installation authority '${authorityRef}' could not be resolved`);
288
+ }
289
+
290
+ if (projectLocalAuthorityExists(authorityRef, target)) {
291
+ return untrusted("Project-local authority artifacts are references only and are not trusted authority sources");
292
+ }
293
+ return invalid(`Referenced installation authority '${authorityRef}' could not be resolved`, {
294
+ sourceConfigured: sources.sourceConfigured,
295
+ });
296
+ }