Sign In

identityforge

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

identityforge - npm Package Compare versions

Comparing version
0.4.2
to
0.4.3
+9
-8
dist/api.js
import { randomUUID } from "node:crypto";
import { resolveApiKey, resolveApiUrl } from "./config.js";
import { isVersionGreater } from "./updateCheck.js";
export const CLI_VERSION = "0.4.2";
export const CLI_VERSION = "0.4.3";
let apiClient = "cli";

@@ -387,13 +387,14 @@ const clientProcessReference = randomUUID();

/**
* Record one completed local apply without sending paths or file contents.
* This is deliberately best-effort: analytics must never make a successful
* filesystem write look like a failed apply.
* Record the bounded local result without sending paths, filenames, file
* contents, or exception prose. Analytics remains best-effort and cannot
* change the command's result.
*/
export async function recordApplyCompleted(identifier) {
export async function recordImplementationOutcome(identifier, outcome) {
if (process.env.IDENTITYFORGE_TELEMETRY === "0")
return;
try {
await fetch(`${resolveApiUrl()}/api/v1/kits/${encodeURIComponent(identifier)}/applied`, {
await fetch(`${resolveApiUrl()}/api/v1/kits/${encodeURIComponent(identifier)}/implementation-outcome`, {
method: "POST",
headers: authHeaders(),
headers: { ...authHeaders(), "Content-Type": "application/json" },
body: JSON.stringify(outcome),
signal: AbortSignal.timeout(1_000),

@@ -403,3 +404,3 @@ });

catch {
// Best-effort telemetry. The kit is already safely written.
// Best-effort telemetry. The command result is already known locally.
}

@@ -406,0 +407,0 @@ }

import { createHash } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { exportKit, isSafeExportFilename, recordApplyCompleted, } from "./api.js";
import { ApiError, exportKit, isSafeExportFilename, recordImplementationOutcome, } from "./api.js";
// Applying a kit writes into someone else's repository, so it is the one place

@@ -298,84 +298,126 @@ // in this CLI that can destroy work. The rule here: never overwrite a file we

notes.push(stampNote);
// Both fetches complete before the first write, so a 403 or a network
// failure on the second export cannot leave a half-applied repo.
const [design, tokens] = await Promise.all([
exportKit(options.slug, "design-md"),
exportKit(options.slug, options.tokensFormat),
]);
const tokensName = assertSafeArtifactName(tokens.filename);
if (tokensName === STAMP_FILENAME || tokensName === DESIGN_FILENAME) {
throw new Error(`The tokens file for "${options.slug}" would be written to ${tokensName}, which collides with the ${tokensName === STAMP_FILENAME ? "stamp" : "design brief"}. Nothing was written. Choose another tokens format.`);
let failureStage = "fetch";
let writtenArtifactCount = 0;
try {
// Both fetches complete before the first write, so a 403 or a network
// failure on the second export cannot leave a half-applied repo.
const [design, tokens] = await Promise.all([
exportKit(options.slug, "design-md"),
exportKit(options.slug, options.tokensFormat),
]);
failureStage = "plan";
const tokensName = assertSafeArtifactName(tokens.filename);
if (tokensName === STAMP_FILENAME || tokensName === DESIGN_FILENAME) {
throw new Error(`The tokens file for "${options.slug}" would be written to ${tokensName}, which collides with the ${tokensName === STAMP_FILENAME ? "stamp" : "design brief"}. Nothing was written. Choose another tokens format.`);
}
const designMdDigest = hashContent(design.body);
const identity = parseExportIdentity(design.body);
if (priorStamp) {
notes.push(...driftNotes(priorStamp, options.slug, identity, designMdDigest));
}
const artifacts = [
planArtifact(dir, DESIGN_FILENAME, design.body, priorStamp),
planArtifact(dir, tokensName, tokens.body, priorStamp),
];
const conflicts = artifacts.filter((artifact) => artifact.status === "conflict");
const result = {
mode: "preview",
slug: options.slug,
tokensFormat: options.tokensFormat,
dir,
stampPath,
artifacts,
conflicts,
overwritten: [],
notes,
};
if (options.preview)
return result;
if (conflicts.length > 0 && !options.force) {
await recordImplementationOutcome(identity.slug ?? options.slug, {
outcome: "refused",
reason: "conflict",
conflictCount: conflicts.length,
});
return { ...result, mode: "refused" };
}
failureStage = "write";
const appliedAt = new Date().toISOString();
const overwritten = [];
for (const artifact of artifacts) {
if (artifact.status === "unchanged")
continue;
writeFileSync(artifact.path, artifact.body, "utf8");
writtenArtifactCount += 1;
if (artifact.status === "conflict")
overwritten.push(artifact);
}
// Artifacts written by an earlier apply and untouched by this one keep their
// records, so switching tokens format does not turn the previous tokens file
// into an unrecorded stranger on the next run.
const writtenPaths = new Set(artifacts.map((artifact) => artifact.relPath));
const carried = (priorStamp?.artifacts ?? []).filter((entry) => !writtenPaths.has(entry.path));
const stamp = {
stampVersion: STAMP_VERSION,
// Omitted rather than null when the export did not state one, so a reader
// can tell "built against a server that predates the contract" from "the
// contract was read and was empty".
...(identity.contract ? { designMdContract: identity.contract } : {}),
kit: {
id: identity.id,
// What the server calls this kit, not what the caller typed: `apply`
// accepts a permanent id, and stamping that as the slug would record a
// handle no human reading the file can use.
slug: identity.slug ?? options.slug,
version: identity.version,
designMdDigest,
},
layers: [],
artifacts: [
...carried,
...artifacts.map((artifact) => ({
path: artifact.relPath,
hash: artifact.hash,
writtenAt: artifact.writtenAt ?? appliedAt,
})),
].sort((a, b) => a.path.localeCompare(b.path)),
integration: {
tokensEntry: options.tokensEntry ?? priorStamp?.integration?.tokensEntry ?? null,
},
appliedAt,
};
writeFileSync(stampPath, `${JSON.stringify(stamp, null, 2)}\n`, "utf8");
const unchangedArtifactCount = artifacts.length - writtenArtifactCount;
await recordImplementationOutcome(identity.slug ?? options.slug, writtenArtifactCount > 0
? {
outcome: "files_written",
tokensFormat: options.tokensFormat,
artifactCount: writtenArtifactCount,
unchangedCount: unchangedArtifactCount,
overwrittenCount: overwritten.length,
}
: {
outcome: "artifacts_current",
tokensFormat: options.tokensFormat,
artifactCount: unchangedArtifactCount,
});
return { ...result, mode: "applied", overwritten, stamp };
}
const designMdDigest = hashContent(design.body);
const identity = parseExportIdentity(design.body);
if (priorStamp) {
notes.push(...driftNotes(priorStamp, options.slug, identity, designMdDigest));
catch (error) {
await recordImplementationOutcome(options.slug, {
outcome: "failed",
stage: failureStage,
artifactCount: writtenArtifactCount,
reason: failureStage === "write"
? "filesystem"
: failureStage === "plan"
? "invalid_artifact"
: error instanceof ApiError
? "api"
: error instanceof TypeError
? "network"
: "unknown",
});
throw error;
}
const artifacts = [
planArtifact(dir, DESIGN_FILENAME, design.body, priorStamp),
planArtifact(dir, tokensName, tokens.body, priorStamp),
];
const conflicts = artifacts.filter((artifact) => artifact.status === "conflict");
const result = {
mode: "preview",
slug: options.slug,
tokensFormat: options.tokensFormat,
dir,
stampPath,
artifacts,
conflicts,
overwritten: [],
notes,
};
if (options.preview)
return result;
if (conflicts.length > 0 && !options.force) {
return { ...result, mode: "refused" };
}
const appliedAt = new Date().toISOString();
const overwritten = [];
for (const artifact of artifacts) {
if (artifact.status === "unchanged")
continue;
writeFileSync(artifact.path, artifact.body, "utf8");
if (artifact.status === "conflict")
overwritten.push(artifact);
}
// Artifacts written by an earlier apply and untouched by this one keep their
// records, so switching tokens format does not turn the previous tokens file
// into an unrecorded stranger on the next run.
const writtenPaths = new Set(artifacts.map((artifact) => artifact.relPath));
const carried = (priorStamp?.artifacts ?? []).filter((entry) => !writtenPaths.has(entry.path));
const stamp = {
stampVersion: STAMP_VERSION,
// Omitted rather than null when the export did not state one, so a reader
// can tell "built against a server that predates the contract" from "the
// contract was read and was empty".
...(identity.contract ? { designMdContract: identity.contract } : {}),
kit: {
id: identity.id,
// What the server calls this kit, not what the caller typed: `apply`
// accepts a permanent id, and stamping that as the slug would record a
// handle no human reading the file can use.
slug: identity.slug ?? options.slug,
version: identity.version,
designMdDigest,
},
layers: [],
artifacts: [
...carried,
...artifacts.map((artifact) => ({
path: artifact.relPath,
hash: artifact.hash,
writtenAt: artifact.writtenAt ?? appliedAt,
})),
].sort((a, b) => a.path.localeCompare(b.path)),
integration: {
tokensEntry: options.tokensEntry ?? priorStamp?.integration?.tokensEntry ?? null,
},
appliedAt,
};
writeFileSync(stampPath, `${JSON.stringify(stamp, null, 2)}\n`, "utf8");
await recordApplyCompleted(identity.slug ?? options.slug);
return { ...result, mode: "applied", overwritten, stamp };
}

@@ -382,0 +424,0 @@ const CONFLICT_EXPLANATIONS = {

{
"name": "identityforge",
"version": "0.4.2",
"version": "0.4.3",
"mcpName": "io.identityforge/mcp",

@@ -5,0 +5,0 @@ "description": "Agent-native brand naming, domain research, and design systems through one CLI and MCP server.",

@@ -173,9 +173,12 @@ <p align="center">

Identity Forge requests carry one random reference that lasts only for the
current CLI or MCP process. A successful local apply also sends one bodyless
completion signal. Running as an MCP server, requests also carry the client name
current CLI or MCP process. A local apply reports one bounded result: files
written, artifacts already current, a safe conflict refusal, or the stage where
it failed. The report contains counts and classifications, never local paths or
error prose. Running as an MCP server, requests also carry the client name
your editor or agent already sends in the MCP handshake (`claude-code`,
`cursor-vscode`, `codex`), so usage can be attributed to a product rather than to
nothing. None of this includes paths, prompts, file contents, repository names,
or a persistent installation identifier. Set `IDENTITYFORGE_TELEMETRY=0` to omit
all three; ordinary API requests still appear in server access logs.
nothing. None of this includes paths, filenames, prompts, file contents,
repository names, exception text, or a persistent installation identifier. Set
`IDENTITYFORGE_TELEMETRY=0` to omit the process reference and local outcome
reports; ordinary API requests still appear in server access logs.

@@ -182,0 +185,0 @@ | File | What it is |