sentinelayer-cli 0.1.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.
- package/README.md +996 -0
- package/bin/create-sentinelayer.js +5 -0
- package/bin/sentinelayer-cli.js +5 -0
- package/bin/sl.js +5 -0
- package/package.json +54 -0
- package/src/agents/jules/config/definition.js +209 -0
- package/src/agents/jules/config/system-prompt.js +175 -0
- package/src/agents/jules/error-intake.js +51 -0
- package/src/agents/jules/fix-cycle.js +377 -0
- package/src/agents/jules/loop.js +367 -0
- package/src/agents/jules/pulse.js +319 -0
- package/src/agents/jules/stream.js +186 -0
- package/src/agents/jules/swarm/file-scanner.js +74 -0
- package/src/agents/jules/swarm/index.js +11 -0
- package/src/agents/jules/swarm/orchestrator.js +362 -0
- package/src/agents/jules/swarm/pattern-hunter.js +123 -0
- package/src/agents/jules/swarm/sub-agent.js +308 -0
- package/src/agents/jules/tools/auth-audit.js +222 -0
- package/src/agents/jules/tools/dispatch.js +327 -0
- package/src/agents/jules/tools/file-edit.js +180 -0
- package/src/agents/jules/tools/file-read.js +100 -0
- package/src/agents/jules/tools/frontend-analyze.js +570 -0
- package/src/agents/jules/tools/glob.js +168 -0
- package/src/agents/jules/tools/grep.js +228 -0
- package/src/agents/jules/tools/index.js +29 -0
- package/src/agents/jules/tools/path-guards.js +161 -0
- package/src/agents/jules/tools/runtime-audit.js +409 -0
- package/src/agents/jules/tools/shell.js +383 -0
- package/src/ai/aidenid.js +945 -0
- package/src/ai/client.js +508 -0
- package/src/ai/domain-target-store.js +268 -0
- package/src/ai/identity-store.js +270 -0
- package/src/ai/site-store.js +145 -0
- package/src/audit/agents/architecture.js +180 -0
- package/src/audit/agents/compliance.js +179 -0
- package/src/audit/agents/documentation.js +165 -0
- package/src/audit/agents/performance.js +145 -0
- package/src/audit/agents/security.js +215 -0
- package/src/audit/agents/testing.js +172 -0
- package/src/audit/orchestrator.js +557 -0
- package/src/audit/package.js +204 -0
- package/src/audit/registry.js +284 -0
- package/src/audit/replay.js +103 -0
- package/src/auth/http.js +113 -0
- package/src/auth/service.js +848 -0
- package/src/auth/session-store.js +345 -0
- package/src/cli.js +244 -0
- package/src/commands/ai/identity-lifecycle.js +1337 -0
- package/src/commands/ai/provision-governance.js +1246 -0
- package/src/commands/ai/shared.js +147 -0
- package/src/commands/ai.js +11 -0
- package/src/commands/apply.js +19 -0
- package/src/commands/audit.js +1147 -0
- package/src/commands/auth.js +366 -0
- package/src/commands/chat.js +191 -0
- package/src/commands/config.js +184 -0
- package/src/commands/cost.js +311 -0
- package/src/commands/daemon/core.js +850 -0
- package/src/commands/daemon/extended.js +1048 -0
- package/src/commands/daemon/shared.js +213 -0
- package/src/commands/daemon.js +11 -0
- package/src/commands/guide.js +174 -0
- package/src/commands/ingest.js +58 -0
- package/src/commands/init.js +55 -0
- package/src/commands/legacy-args.js +30 -0
- package/src/commands/mcp.js +404 -0
- package/src/commands/omargate.js +21 -0
- package/src/commands/persona.js +27 -0
- package/src/commands/plugin.js +260 -0
- package/src/commands/policy.js +132 -0
- package/src/commands/prompt.js +238 -0
- package/src/commands/review.js +704 -0
- package/src/commands/scan.js +788 -0
- package/src/commands/spec.js +716 -0
- package/src/commands/swarm.js +651 -0
- package/src/commands/telemetry.js +202 -0
- package/src/commands/watch.js +510 -0
- package/src/config/agent-dictionary.js +182 -0
- package/src/config/io.js +56 -0
- package/src/config/paths.js +18 -0
- package/src/config/schema.js +55 -0
- package/src/config/service.js +184 -0
- package/src/cost/budget.js +235 -0
- package/src/cost/history.js +188 -0
- package/src/cost/tracker.js +171 -0
- package/src/daemon/artifact-lineage.js +534 -0
- package/src/daemon/assignment-ledger.js +770 -0
- package/src/daemon/ast-parser-layer.js +258 -0
- package/src/daemon/budget-governor.js +633 -0
- package/src/daemon/callgraph-overlay.js +646 -0
- package/src/daemon/error-worker.js +626 -0
- package/src/daemon/hybrid-mapper.js +929 -0
- package/src/daemon/jira-lifecycle.js +632 -0
- package/src/daemon/operator-control.js +657 -0
- package/src/daemon/reliability-lane.js +471 -0
- package/src/daemon/watchdog.js +971 -0
- package/src/guide/generator.js +316 -0
- package/src/ingest/engine.js +918 -0
- package/src/legacy-cli.js +2435 -0
- package/src/mcp/registry.js +695 -0
- package/src/memory/blackboard.js +301 -0
- package/src/memory/retrieval.js +581 -0
- package/src/plugin/manifest.js +553 -0
- package/src/policy/packs.js +144 -0
- package/src/prompt/generator.js +106 -0
- package/src/review/ai-review.js +669 -0
- package/src/review/local-review.js +1284 -0
- package/src/review/replay.js +235 -0
- package/src/review/report.js +664 -0
- package/src/review/spec-binding.js +487 -0
- package/src/scan/generator.js +351 -0
- package/src/spec/generator.js +519 -0
- package/src/spec/regenerate.js +237 -0
- package/src/spec/templates.js +91 -0
- package/src/swarm/dashboard.js +247 -0
- package/src/swarm/factory.js +363 -0
- package/src/swarm/pentest.js +934 -0
- package/src/swarm/registry.js +419 -0
- package/src/swarm/report.js +158 -0
- package/src/swarm/runtime.js +576 -0
- package/src/swarm/scenario-dsl.js +272 -0
- package/src/telemetry/ledger.js +302 -0
- package/src/ui/markdown.js +220 -0
- package/src/ui/progress.js +100 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const REGISTRY_SCHEMA_VERSION = "1.0.0";
|
|
5
|
+
|
|
6
|
+
function normalizeString(value) {
|
|
7
|
+
return String(value || "").trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeDomainRecord(record = {}) {
|
|
11
|
+
return {
|
|
12
|
+
domainId: normalizeString(record.domainId),
|
|
13
|
+
domainName: normalizeString(record.domainName) || null,
|
|
14
|
+
projectId: normalizeString(record.projectId) || null,
|
|
15
|
+
verificationStatus: normalizeString(record.verificationStatus) || "UNKNOWN",
|
|
16
|
+
freezeStatus: normalizeString(record.freezeStatus) || "UNKNOWN",
|
|
17
|
+
trustClass: normalizeString(record.trustClass) || null,
|
|
18
|
+
verificationMethod: normalizeString(record.verificationMethod) || null,
|
|
19
|
+
challengeValue: normalizeString(record.challengeValue) || null,
|
|
20
|
+
proofId: normalizeString(record.proofId) || null,
|
|
21
|
+
proofStatus: normalizeString(record.proofStatus) || null,
|
|
22
|
+
proofExpiresAt: normalizeString(record.proofExpiresAt) || null,
|
|
23
|
+
createdAt: normalizeString(record.createdAt) || new Date().toISOString(),
|
|
24
|
+
lastUpdatedAt: normalizeString(record.lastUpdatedAt) || new Date().toISOString(),
|
|
25
|
+
metadata: record.metadata && typeof record.metadata === "object" ? record.metadata : {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeTargetRecord(record = {}) {
|
|
30
|
+
return {
|
|
31
|
+
targetId: normalizeString(record.targetId),
|
|
32
|
+
host: normalizeString(record.host) || null,
|
|
33
|
+
domainId: normalizeString(record.domainId) || null,
|
|
34
|
+
projectId: normalizeString(record.projectId) || null,
|
|
35
|
+
verificationStatus: normalizeString(record.verificationStatus) || "UNKNOWN",
|
|
36
|
+
status: normalizeString(record.status) || "UNKNOWN",
|
|
37
|
+
freezeStatus: normalizeString(record.freezeStatus) || "UNKNOWN",
|
|
38
|
+
challengeValue: normalizeString(record.challengeValue) || null,
|
|
39
|
+
proofId: normalizeString(record.proofId) || null,
|
|
40
|
+
proofStatus: normalizeString(record.proofStatus) || null,
|
|
41
|
+
proofExpiresAt: normalizeString(record.proofExpiresAt) || null,
|
|
42
|
+
policy:
|
|
43
|
+
record.policy && typeof record.policy === "object"
|
|
44
|
+
? {
|
|
45
|
+
allowedPaths: Array.isArray(record.policy.allowedPaths) ? record.policy.allowedPaths : [],
|
|
46
|
+
allowedMethods: Array.isArray(record.policy.allowedMethods) ? record.policy.allowedMethods : [],
|
|
47
|
+
allowedScenarios: Array.isArray(record.policy.allowedScenarios)
|
|
48
|
+
? record.policy.allowedScenarios
|
|
49
|
+
: [],
|
|
50
|
+
maxRps: Number.isFinite(Number(record.policy.maxRps)) ? Number(record.policy.maxRps) : null,
|
|
51
|
+
maxConcurrency: Number.isFinite(Number(record.policy.maxConcurrency))
|
|
52
|
+
? Number(record.policy.maxConcurrency)
|
|
53
|
+
: null,
|
|
54
|
+
stopConditions:
|
|
55
|
+
record.policy.stopConditions && typeof record.policy.stopConditions === "object"
|
|
56
|
+
? record.policy.stopConditions
|
|
57
|
+
: {},
|
|
58
|
+
}
|
|
59
|
+
: {
|
|
60
|
+
allowedPaths: [],
|
|
61
|
+
allowedMethods: [],
|
|
62
|
+
allowedScenarios: [],
|
|
63
|
+
maxRps: null,
|
|
64
|
+
maxConcurrency: null,
|
|
65
|
+
stopConditions: {},
|
|
66
|
+
},
|
|
67
|
+
createdAt: normalizeString(record.createdAt) || new Date().toISOString(),
|
|
68
|
+
lastUpdatedAt: normalizeString(record.lastUpdatedAt) || new Date().toISOString(),
|
|
69
|
+
metadata: record.metadata && typeof record.metadata === "object" ? record.metadata : {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveDomainTargetRegistryPath({ outputRoot } = {}) {
|
|
74
|
+
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
75
|
+
return path.join(resolvedOutputRoot, "aidenid", "domain-target-registry.json");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function loadRegistryInternal(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
const domains = Array.isArray(parsed.domains)
|
|
83
|
+
? parsed.domains.map((item) => normalizeDomainRecord(item)).filter((item) => item.domainId)
|
|
84
|
+
: [];
|
|
85
|
+
const targets = Array.isArray(parsed.targets)
|
|
86
|
+
? parsed.targets.map((item) => normalizeTargetRecord(item)).filter((item) => item.targetId)
|
|
87
|
+
: [];
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: normalizeString(parsed.schemaVersion) || REGISTRY_SCHEMA_VERSION,
|
|
90
|
+
generatedAt: normalizeString(parsed.generatedAt) || new Date().toISOString(),
|
|
91
|
+
domains,
|
|
92
|
+
targets,
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
96
|
+
return {
|
|
97
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
98
|
+
generatedAt: new Date().toISOString(),
|
|
99
|
+
domains: [],
|
|
100
|
+
targets: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function writeRegistryInternal(filePath, registry = {}) {
|
|
108
|
+
const normalized = {
|
|
109
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
110
|
+
generatedAt: new Date().toISOString(),
|
|
111
|
+
domains: Array.isArray(registry.domains) ? registry.domains.map(normalizeDomainRecord) : [],
|
|
112
|
+
targets: Array.isArray(registry.targets) ? registry.targets.map(normalizeTargetRecord) : [],
|
|
113
|
+
};
|
|
114
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
115
|
+
await fsp.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
116
|
+
return normalized;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function listDomainTargetRecords({ outputRoot } = {}) {
|
|
120
|
+
const registryPath = resolveDomainTargetRegistryPath({ outputRoot });
|
|
121
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
122
|
+
return {
|
|
123
|
+
registryPath,
|
|
124
|
+
domains: registry.domains,
|
|
125
|
+
targets: registry.targets,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function getDomainById({ outputRoot, domainId } = {}) {
|
|
130
|
+
const normalizedDomainId = normalizeString(domainId);
|
|
131
|
+
if (!normalizedDomainId) {
|
|
132
|
+
throw new Error("domainId is required.");
|
|
133
|
+
}
|
|
134
|
+
const { registryPath, domains } = await listDomainTargetRecords({ outputRoot });
|
|
135
|
+
const domain = domains.find((item) => item.domainId === normalizedDomainId) || null;
|
|
136
|
+
return {
|
|
137
|
+
registryPath,
|
|
138
|
+
domain,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function getTargetById({ outputRoot, targetId } = {}) {
|
|
143
|
+
const normalizedTargetId = normalizeString(targetId);
|
|
144
|
+
if (!normalizedTargetId) {
|
|
145
|
+
throw new Error("targetId is required.");
|
|
146
|
+
}
|
|
147
|
+
const { registryPath, targets } = await listDomainTargetRecords({ outputRoot });
|
|
148
|
+
const target = targets.find((item) => item.targetId === normalizedTargetId) || null;
|
|
149
|
+
return {
|
|
150
|
+
registryPath,
|
|
151
|
+
target,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function recordDomainProofResponse({
|
|
156
|
+
outputRoot,
|
|
157
|
+
domain = {},
|
|
158
|
+
proof = {},
|
|
159
|
+
context = {},
|
|
160
|
+
} = {}) {
|
|
161
|
+
const domainId = normalizeString(domain.id || context.domainId);
|
|
162
|
+
if (!domainId) {
|
|
163
|
+
throw new Error("Cannot record domain without domain.id.");
|
|
164
|
+
}
|
|
165
|
+
const registryPath = resolveDomainTargetRegistryPath({ outputRoot });
|
|
166
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
167
|
+
const nowIso = new Date().toISOString();
|
|
168
|
+
const nextRecord = normalizeDomainRecord({
|
|
169
|
+
domainId,
|
|
170
|
+
domainName: domain.domainName,
|
|
171
|
+
projectId: domain.projectId || context.projectId,
|
|
172
|
+
verificationStatus: domain.verificationStatus,
|
|
173
|
+
freezeStatus: domain.freezeStatus,
|
|
174
|
+
trustClass: domain.trustClass,
|
|
175
|
+
verificationMethod: domain.verificationMethod,
|
|
176
|
+
challengeValue: proof.challengeValue || context.challengeValue,
|
|
177
|
+
proofId: proof.proofId,
|
|
178
|
+
proofStatus: proof.proofStatus,
|
|
179
|
+
proofExpiresAt: proof.proofExpiresAt,
|
|
180
|
+
createdAt: nowIso,
|
|
181
|
+
lastUpdatedAt: nowIso,
|
|
182
|
+
metadata: {
|
|
183
|
+
source: normalizeString(context.source) || "domain",
|
|
184
|
+
idempotencyKey: context.idempotencyKey || null,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const index = registry.domains.findIndex((item) => item.domainId === domainId);
|
|
189
|
+
if (index >= 0) {
|
|
190
|
+
const existing = registry.domains[index];
|
|
191
|
+
registry.domains[index] = normalizeDomainRecord({
|
|
192
|
+
...existing,
|
|
193
|
+
...nextRecord,
|
|
194
|
+
createdAt: existing.createdAt || nextRecord.createdAt,
|
|
195
|
+
metadata: {
|
|
196
|
+
...existing.metadata,
|
|
197
|
+
...nextRecord.metadata,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
registry.domains.push(nextRecord);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
205
|
+
const record = saved.domains.find((item) => item.domainId === domainId) || null;
|
|
206
|
+
return {
|
|
207
|
+
registryPath,
|
|
208
|
+
domain: record,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function recordTargetProofResponse({
|
|
213
|
+
outputRoot,
|
|
214
|
+
target = {},
|
|
215
|
+
proof = {},
|
|
216
|
+
context = {},
|
|
217
|
+
} = {}) {
|
|
218
|
+
const targetId = normalizeString(target.id || context.targetId);
|
|
219
|
+
if (!targetId) {
|
|
220
|
+
throw new Error("Cannot record target without target.id.");
|
|
221
|
+
}
|
|
222
|
+
const registryPath = resolveDomainTargetRegistryPath({ outputRoot });
|
|
223
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
224
|
+
const nowIso = new Date().toISOString();
|
|
225
|
+
const nextRecord = normalizeTargetRecord({
|
|
226
|
+
targetId,
|
|
227
|
+
host: target.host,
|
|
228
|
+
domainId: target.domainId || context.domainId || null,
|
|
229
|
+
projectId: target.projectId || context.projectId || null,
|
|
230
|
+
verificationStatus: target.verificationStatus,
|
|
231
|
+
status: target.status,
|
|
232
|
+
freezeStatus: target.freezeStatus,
|
|
233
|
+
challengeValue: proof.challengeValue || context.challengeValue,
|
|
234
|
+
proofId: proof.proofId,
|
|
235
|
+
proofStatus: proof.proofStatus,
|
|
236
|
+
proofExpiresAt: proof.proofExpiresAt,
|
|
237
|
+
policy: target.policy || {},
|
|
238
|
+
createdAt: nowIso,
|
|
239
|
+
lastUpdatedAt: nowIso,
|
|
240
|
+
metadata: {
|
|
241
|
+
source: normalizeString(context.source) || "target",
|
|
242
|
+
idempotencyKey: context.idempotencyKey || null,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const index = registry.targets.findIndex((item) => item.targetId === targetId);
|
|
247
|
+
if (index >= 0) {
|
|
248
|
+
const existing = registry.targets[index];
|
|
249
|
+
registry.targets[index] = normalizeTargetRecord({
|
|
250
|
+
...existing,
|
|
251
|
+
...nextRecord,
|
|
252
|
+
createdAt: existing.createdAt || nextRecord.createdAt,
|
|
253
|
+
metadata: {
|
|
254
|
+
...existing.metadata,
|
|
255
|
+
...nextRecord.metadata,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
registry.targets.push(nextRecord);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
263
|
+
const record = saved.targets.find((item) => item.targetId === targetId) || null;
|
|
264
|
+
return {
|
|
265
|
+
registryPath,
|
|
266
|
+
target: record,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const REGISTRY_SCHEMA_VERSION = "1.0.0";
|
|
5
|
+
const TERMINAL_IDENTITY_STATUSES = new Set(["SQUASHED"]);
|
|
6
|
+
|
|
7
|
+
function normalizeString(value) {
|
|
8
|
+
return String(value || "").trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeUpper(value) {
|
|
12
|
+
return normalizeString(value).toUpperCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeTagList(value) {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const unique = new Set();
|
|
20
|
+
for (const item of value) {
|
|
21
|
+
const normalized = normalizeString(item);
|
|
22
|
+
if (normalized) {
|
|
23
|
+
unique.add(normalized);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...unique];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeLegalHoldStatus(value) {
|
|
30
|
+
const normalized = normalizeUpper(value);
|
|
31
|
+
if (!normalized) {
|
|
32
|
+
return "NONE";
|
|
33
|
+
}
|
|
34
|
+
if (normalized === "HOLD" || normalized === "NONE" || normalized === "UNKNOWN") {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeMetadata(value) {
|
|
41
|
+
const source = value && typeof value === "object" ? value : {};
|
|
42
|
+
return {
|
|
43
|
+
...source,
|
|
44
|
+
tags: normalizeTagList(source.tags),
|
|
45
|
+
legalHoldStatus: normalizeLegalHoldStatus(source.legalHoldStatus),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeIdentityRecord(record = {}) {
|
|
50
|
+
const metadata = normalizeMetadata(record.metadata);
|
|
51
|
+
const legalHoldStatus = normalizeLegalHoldStatus(record.legalHoldStatus || metadata.legalHoldStatus);
|
|
52
|
+
return {
|
|
53
|
+
identityId: normalizeString(record.identityId),
|
|
54
|
+
parentIdentityId: normalizeString(record.parentIdentityId) || null,
|
|
55
|
+
emailAddress: normalizeString(record.emailAddress) || null,
|
|
56
|
+
status: normalizeString(record.status) || "UNKNOWN",
|
|
57
|
+
projectId: normalizeString(record.projectId) || null,
|
|
58
|
+
orgId: normalizeString(record.orgId) || null,
|
|
59
|
+
apiUrl: normalizeString(record.apiUrl) || null,
|
|
60
|
+
createdAt: normalizeString(record.createdAt) || new Date().toISOString(),
|
|
61
|
+
lastUpdatedAt: normalizeString(record.lastUpdatedAt) || new Date().toISOString(),
|
|
62
|
+
expiresAt: normalizeString(record.expiresAt) || null,
|
|
63
|
+
revokedAt: normalizeString(record.revokedAt) || null,
|
|
64
|
+
squashedAt: normalizeString(record.squashedAt) || null,
|
|
65
|
+
legalHoldStatus,
|
|
66
|
+
metadata,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveIdentityRegistryPath({ outputRoot } = {}) {
|
|
71
|
+
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
72
|
+
return path.join(resolvedOutputRoot, "aidenid", "identity-registry.json");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadRegistryInternal(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
const identities = Array.isArray(parsed.identities)
|
|
80
|
+
? parsed.identities.map((item) => normalizeIdentityRecord(item)).filter((item) => item.identityId)
|
|
81
|
+
: [];
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: normalizeString(parsed.schemaVersion) || REGISTRY_SCHEMA_VERSION,
|
|
84
|
+
generatedAt: normalizeString(parsed.generatedAt) || new Date().toISOString(),
|
|
85
|
+
identities,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
91
|
+
generatedAt: new Date().toISOString(),
|
|
92
|
+
identities: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function writeRegistryInternal(filePath, registry = {}) {
|
|
100
|
+
const normalized = {
|
|
101
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
102
|
+
generatedAt: new Date().toISOString(),
|
|
103
|
+
identities: Array.isArray(registry.identities) ? registry.identities.map(normalizeIdentityRecord) : [],
|
|
104
|
+
};
|
|
105
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
106
|
+
await fsp.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function listIdentities({ outputRoot } = {}) {
|
|
111
|
+
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
112
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
113
|
+
return {
|
|
114
|
+
registryPath,
|
|
115
|
+
identities: registry.identities,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getIdentityById({ outputRoot, identityId } = {}) {
|
|
120
|
+
const normalizedIdentityId = normalizeString(identityId);
|
|
121
|
+
if (!normalizedIdentityId) {
|
|
122
|
+
throw new Error("identityId is required.");
|
|
123
|
+
}
|
|
124
|
+
const { registryPath, identities } = await listIdentities({ outputRoot });
|
|
125
|
+
const identity = identities.find((item) => item.identityId === normalizedIdentityId) || null;
|
|
126
|
+
return {
|
|
127
|
+
registryPath,
|
|
128
|
+
identity,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function recordProvisionedIdentity({
|
|
133
|
+
outputRoot,
|
|
134
|
+
response = {},
|
|
135
|
+
context = {},
|
|
136
|
+
} = {}) {
|
|
137
|
+
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
138
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
139
|
+
const identityId = normalizeString(response.id);
|
|
140
|
+
if (!identityId) {
|
|
141
|
+
throw new Error("Cannot record identity without response.id.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nowIso = new Date().toISOString();
|
|
145
|
+
const source = normalizeString(context.source) || "provision-email";
|
|
146
|
+
const nextRecord = normalizeIdentityRecord({
|
|
147
|
+
identityId,
|
|
148
|
+
parentIdentityId: response.parentIdentityId || context.parentIdentityId || null,
|
|
149
|
+
emailAddress: response.emailAddress,
|
|
150
|
+
status: response.status || "ACTIVE",
|
|
151
|
+
projectId: response.projectId || context.projectId,
|
|
152
|
+
orgId: context.orgId,
|
|
153
|
+
apiUrl: context.apiUrl,
|
|
154
|
+
createdAt: nowIso,
|
|
155
|
+
lastUpdatedAt: nowIso,
|
|
156
|
+
expiresAt: response.expiresAt || null,
|
|
157
|
+
legalHoldStatus: response.legalHoldStatus || context.legalHoldStatus || "NONE",
|
|
158
|
+
metadata: {
|
|
159
|
+
source,
|
|
160
|
+
idempotencyKey: context.idempotencyKey || null,
|
|
161
|
+
eventBudget: context.eventBudget ?? null,
|
|
162
|
+
tags: normalizeTagList(context.tags || response.tags || []),
|
|
163
|
+
legalHoldStatus: response.legalHoldStatus || context.legalHoldStatus || "NONE",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const index = registry.identities.findIndex((item) => item.identityId === identityId);
|
|
168
|
+
if (index >= 0) {
|
|
169
|
+
const existing = registry.identities[index];
|
|
170
|
+
registry.identities[index] = normalizeIdentityRecord({
|
|
171
|
+
...existing,
|
|
172
|
+
...nextRecord,
|
|
173
|
+
createdAt: existing.createdAt || nextRecord.createdAt,
|
|
174
|
+
metadata: {
|
|
175
|
+
...existing.metadata,
|
|
176
|
+
...nextRecord.metadata,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
registry.identities.push(nextRecord);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
184
|
+
const identity = saved.identities.find((item) => item.identityId === identityId) || null;
|
|
185
|
+
return {
|
|
186
|
+
registryPath,
|
|
187
|
+
identity,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function updateIdentityStatus({
|
|
192
|
+
outputRoot,
|
|
193
|
+
identityId,
|
|
194
|
+
status,
|
|
195
|
+
revokedAt = "",
|
|
196
|
+
squashedAt = "",
|
|
197
|
+
metadataPatch = {},
|
|
198
|
+
} = {}) {
|
|
199
|
+
const normalizedIdentityId = normalizeString(identityId);
|
|
200
|
+
if (!normalizedIdentityId) {
|
|
201
|
+
throw new Error("identityId is required.");
|
|
202
|
+
}
|
|
203
|
+
const nextStatus = normalizeString(status);
|
|
204
|
+
if (!nextStatus) {
|
|
205
|
+
throw new Error("status is required.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
209
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
210
|
+
const index = registry.identities.findIndex((item) => item.identityId === normalizedIdentityId);
|
|
211
|
+
if (index < 0) {
|
|
212
|
+
throw new Error(`Identity '${normalizedIdentityId}' was not found in local registry.`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const existing = registry.identities[index];
|
|
216
|
+
registry.identities[index] = normalizeIdentityRecord({
|
|
217
|
+
...existing,
|
|
218
|
+
status: nextStatus,
|
|
219
|
+
revokedAt: normalizeString(revokedAt) || existing.revokedAt || null,
|
|
220
|
+
squashedAt: normalizeString(squashedAt) || existing.squashedAt || null,
|
|
221
|
+
legalHoldStatus: metadataPatch?.legalHoldStatus || existing.legalHoldStatus || "NONE",
|
|
222
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
223
|
+
metadata: {
|
|
224
|
+
...existing.metadata,
|
|
225
|
+
...(metadataPatch && typeof metadataPatch === "object" ? metadataPatch : {}),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
230
|
+
return {
|
|
231
|
+
registryPath,
|
|
232
|
+
identity: saved.identities[index],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function filterIdentitiesByTags(identities = [], tags = []) {
|
|
237
|
+
const requestedTags = normalizeTagList(tags);
|
|
238
|
+
if (requestedTags.length === 0) {
|
|
239
|
+
return [...identities];
|
|
240
|
+
}
|
|
241
|
+
const required = new Set(requestedTags);
|
|
242
|
+
return identities.filter((identity) => {
|
|
243
|
+
const identityTags = normalizeTagList(identity?.metadata?.tags);
|
|
244
|
+
if (identityTags.length === 0) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return [...required].every((tag) => identityTags.includes(tag));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function findStaleIdentities({ outputRoot, nowIso = new Date().toISOString() } = {}) {
|
|
252
|
+
const { registryPath, identities } = await listIdentities({ outputRoot });
|
|
253
|
+
const nowMs = new Date(nowIso).getTime();
|
|
254
|
+
const stale = identities.filter((identity) => {
|
|
255
|
+
const status = normalizeUpper(identity.status);
|
|
256
|
+
if (TERMINAL_IDENTITY_STATUSES.has(status)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const expiresAtMs = new Date(identity.expiresAt || "").getTime();
|
|
260
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return expiresAtMs <= nowMs;
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
registryPath,
|
|
267
|
+
identities,
|
|
268
|
+
stale,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const REGISTRY_SCHEMA_VERSION = "1.0.0";
|
|
5
|
+
|
|
6
|
+
function normalizeString(value) {
|
|
7
|
+
return String(value || "").trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeSiteRecord(record = {}) {
|
|
11
|
+
return {
|
|
12
|
+
siteId: normalizeString(record.siteId),
|
|
13
|
+
projectId: normalizeString(record.projectId) || null,
|
|
14
|
+
identityId: normalizeString(record.identityId) || null,
|
|
15
|
+
domainId: normalizeString(record.domainId) || null,
|
|
16
|
+
host: normalizeString(record.host) || null,
|
|
17
|
+
callbackPath: normalizeString(record.callbackPath) || null,
|
|
18
|
+
callbackUrl: normalizeString(record.callbackUrl) || null,
|
|
19
|
+
status: normalizeString(record.status) || "UNKNOWN",
|
|
20
|
+
dnsCleanupStatus: normalizeString(record.dnsCleanupStatus) || "UNKNOWN",
|
|
21
|
+
expiresAt: normalizeString(record.expiresAt) || null,
|
|
22
|
+
teardownReason: normalizeString(record.teardownReason) || null,
|
|
23
|
+
teardownAt: normalizeString(record.teardownAt) || null,
|
|
24
|
+
createdAt: normalizeString(record.createdAt) || new Date().toISOString(),
|
|
25
|
+
lastUpdatedAt: normalizeString(record.lastUpdatedAt) || new Date().toISOString(),
|
|
26
|
+
dnsCleanupContract:
|
|
27
|
+
record.dnsCleanupContract && typeof record.dnsCleanupContract === "object"
|
|
28
|
+
? record.dnsCleanupContract
|
|
29
|
+
: {},
|
|
30
|
+
metadata: record.metadata && typeof record.metadata === "object" ? record.metadata : {},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveSiteRegistryPath({ outputRoot } = {}) {
|
|
35
|
+
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
36
|
+
return path.join(resolvedOutputRoot, "aidenid", "site-registry.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function loadRegistryInternal(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
const sites = Array.isArray(parsed.sites)
|
|
44
|
+
? parsed.sites.map((item) => normalizeSiteRecord(item)).filter((item) => item.siteId)
|
|
45
|
+
: [];
|
|
46
|
+
return {
|
|
47
|
+
schemaVersion: normalizeString(parsed.schemaVersion) || REGISTRY_SCHEMA_VERSION,
|
|
48
|
+
generatedAt: normalizeString(parsed.generatedAt) || new Date().toISOString(),
|
|
49
|
+
sites,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
55
|
+
generatedAt: new Date().toISOString(),
|
|
56
|
+
sites: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeRegistryInternal(filePath, registry = {}) {
|
|
64
|
+
const normalized = {
|
|
65
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
66
|
+
generatedAt: new Date().toISOString(),
|
|
67
|
+
sites: Array.isArray(registry.sites) ? registry.sites.map(normalizeSiteRecord) : [],
|
|
68
|
+
};
|
|
69
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
70
|
+
await fsp.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function listSites({ outputRoot, identityId = "" } = {}) {
|
|
75
|
+
const registryPath = resolveSiteRegistryPath({ outputRoot });
|
|
76
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
77
|
+
const normalizedIdentityId = normalizeString(identityId);
|
|
78
|
+
const filteredSites = normalizedIdentityId
|
|
79
|
+
? registry.sites.filter((item) => item.identityId === normalizedIdentityId)
|
|
80
|
+
: registry.sites;
|
|
81
|
+
return {
|
|
82
|
+
registryPath,
|
|
83
|
+
sites: filteredSites,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function recordTemporarySite({
|
|
88
|
+
outputRoot,
|
|
89
|
+
site = {},
|
|
90
|
+
context = {},
|
|
91
|
+
} = {}) {
|
|
92
|
+
const siteId = normalizeString(site.id || context.siteId);
|
|
93
|
+
if (!siteId) {
|
|
94
|
+
throw new Error("Cannot record temporary site without site.id.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const registryPath = resolveSiteRegistryPath({ outputRoot });
|
|
98
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
99
|
+
const nowIso = new Date().toISOString();
|
|
100
|
+
const nextRecord = normalizeSiteRecord({
|
|
101
|
+
siteId,
|
|
102
|
+
projectId: site.projectId || context.projectId,
|
|
103
|
+
identityId: site.identityId || context.identityId,
|
|
104
|
+
domainId: site.domainId || context.domainId,
|
|
105
|
+
host: site.host,
|
|
106
|
+
callbackPath: site.callbackPath,
|
|
107
|
+
callbackUrl: site.callbackUrl,
|
|
108
|
+
status: site.status,
|
|
109
|
+
dnsCleanupStatus: site.dnsCleanupStatus,
|
|
110
|
+
expiresAt: site.expiresAt,
|
|
111
|
+
teardownReason: site.teardownReason,
|
|
112
|
+
teardownAt: site.teardownAt,
|
|
113
|
+
createdAt: nowIso,
|
|
114
|
+
lastUpdatedAt: nowIso,
|
|
115
|
+
dnsCleanupContract: site.dnsCleanupContract,
|
|
116
|
+
metadata: {
|
|
117
|
+
...(site.metadata && typeof site.metadata === "object" ? site.metadata : {}),
|
|
118
|
+
source: normalizeString(context.source) || "site-create",
|
|
119
|
+
idempotencyKey: context.idempotencyKey || null,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const index = registry.sites.findIndex((item) => item.siteId === siteId);
|
|
124
|
+
if (index >= 0) {
|
|
125
|
+
const existing = registry.sites[index];
|
|
126
|
+
registry.sites[index] = normalizeSiteRecord({
|
|
127
|
+
...existing,
|
|
128
|
+
...nextRecord,
|
|
129
|
+
createdAt: existing.createdAt || nextRecord.createdAt,
|
|
130
|
+
metadata: {
|
|
131
|
+
...existing.metadata,
|
|
132
|
+
...nextRecord.metadata,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
registry.sites.push(nextRecord);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
140
|
+
const savedSite = saved.sites.find((item) => item.siteId === siteId) || null;
|
|
141
|
+
return {
|
|
142
|
+
registryPath,
|
|
143
|
+
site: savedSite,
|
|
144
|
+
};
|
|
145
|
+
}
|