Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# npm 12+ defaults allow-git=none.
# @fabric/core itself has no Fabric git dependencies, but consumers that install
# core/http/hub from GitHub need `allow-git=all` so nested git dep preparation
# (commit-SHA fetches) is not refused. Keep this file so the monorepo default
# matches Hub / http / app packages.
allow-git=all

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which was added specifically to block install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts).

Attack path: A compromised or malicious transitive dependency introduces a git+/github: URL → npm fetches it under all → that git checkout’s .npmrc can redirect git → arbitrary code at install time. @fabric/core currently declares no git deps, so all is broader than needed; root (or keeping the default and scoping the opt-in to Hub/http only) would preserve the boundary.

Same relaxation is also forced in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust during that script.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all still opts this repo out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (even under --ignore-scripts). Confirmed still present at HEAD 91f034254; CHANGELOG notes root was rejected for nested SHA fetches.

Attack path: Compromised or malicious transitive git+/github: dep → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. This package still declares no git deps, so all is broader than needed.

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD 3a4d87ab.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD b984c1f60.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD 3fc4b6602.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — Supply-chain: re-enables npm 12 git-dependency RCE path

allow-git=all opts this repo (and GitHub installs of it) out of npm 12’s default allow-git=none, which blocks install-time code execution via a git dependency’s .npmrc overriding the git binary (works even with --ignore-scripts). Confirmed still present at HEAD a97e7523a.

Attack path: Compromised or malicious transitive git+/github: dependency → fetched under all → that checkout’s .npmrc redirects git → arbitrary code at install time. @fabric/core still declares no git deps, so all is broader than needed (.npmrc is not in the published files list, but applies to clones and github: installs).

Same relaxation remains in package.json report:install (npm i --allow-git=all after wiping the lockfile), which further widens install-time trust on a lockfile-free resolve.

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# `@fabric/core` Changelog
Recent changes to Fabric Core.

## 2026-08-06
- **npm git deps:** add **`.npmrc`** with **`allow-git=all`** (and `report:install`) so the monorepo matches Hub/http/app consumers — npm 12+ nested git-dep preparation of commit SHAs fails under `allow-git=root`.

## 2026-07-29
JS-canonical protocol for 0.1.0; Lightning-style wire preimage; unsigned document binding; public-readiness cuts; Peer scoring; directed onion forward.

Expand Down
3 changes: 2 additions & 1 deletion DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ Read **[VISION.md](VISION.md)** first for what Fabric is building, how **`@fabri
See also [`QUICKSTART.md`][quickstart-guide] for up-to-date instructions.

0. `nvm use 24.15.0` (install [`nvm`][nvm-official] if needed; matches `.nvmrc` / `package.json` engines)
1. From a clone of this repo: `npm install` (or `npm install -g @fabric/core` to put `fabric` on your `PATH`)
0b. Ensure **npm 12+** (`npm -v`). Node 24.15.0 may ship npm 11.x — upgrade with `npm install -g npm@12` (or newer) before installing. Downstream packages that install Fabric from GitHub need **`.npmrc` `allow-git=all`** (npm 12+); see Hub / `@fabric/http` / GoonCitizen.
1. From a clone of this repo: `npm install` (or `npm install -g @fabric/core` to put `fabric` on your `PATH`).
2. (optional) `fabric setup` to generate a master key and local config
3. (optional) `fabric keygen` to generate a new master key without saving to disk (ephemeral)
4. Run `fabric` — the CLI entry is wired through `types/cli.js` and extends **`Service.FabricShell`**. **Contracts** (HTLCs, document sessions, programs, shell packs, …) are documented in **[docs/CONTRACTS.md](docs/CONTRACTS.md)**; terminal UX in **[docs/CLI.md](docs/CLI.md)**.
Expand Down
21 changes: 19 additions & 2 deletions docs/APPLICATION_NAMESPACES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,24 @@ These `type` strings ride inside `CONTRACT_MESSAGE` (not outer opcodes):
| `GroupChat` | GoonCitizen Group Federation | Group channel chat |
| `GroupChange` | GoonCitizen Group Federation | Membership / meta |
| `GroupShare` | GoonCitizen Group Federation | Group-scoped shares (mission offers; `kind: GroupOffer` for opaque `fabric:<hex>` join offers) |
| `GroupActivityTree` | GoonCitizen Group Federation | Merkle root + digests of cumulative history under a Group namespace |
| `GroupJournalRequest` | GoonCitizen Group Federation | Request missing Statechain journal entries (`fromClock` → tip) |
| `GroupJournalBatch` | GoonCitizen Group Federation | Catch-up batch of journal rows + tip Schnorr (`ContractStateTip`) |
| `GroupStateJournal` | GoonCitizen Group Federation | Optional tip attestation: folded `stateDigest` signed to threshold |
| `ContractCapabilityGrant` | Hub, GoonCitizen | Token-backed reader/signer grant (`OP_CONTRACT_READ` / `OP_CONTRACT_SIGN`) |
| `ContractWithdrawalRequest` | Hub, GoonCitizen | Spend or decay-migrate from contract Taproot UTXO |
| `ContractWithdrawalWitness` | Hub, GoonCitizen | Co-signer witness for withdrawal / migration |
| `GameStateSnapshot` | GoonCitizen → Hub sidechain | Cumulative analytics snapshot for Beacon seal (also listed under `ACTIVITY_TYPES`) |

**Tip attestation:** journal tips use
[`functions/contractStateSigning`](../functions/contractStateSigning.js)
(`kind: ContractStateTip`, same k-of-n witness shape as Beacon epochs). Hub must
track this module when sealing contract-namespace sidechains.

**Taproot spend ladder:** [`functions/contractTaproot`](../functions/contractTaproot.js)
builds deterministic P2TR trees from author-defined failover tiers (`after` / `until`
decay + optional migrate). See DISTRIBUTED_EXECUTION.md.

### Shared activity / GenericMessage types

Not outer opcodes; not always `CONTRACT_MESSAGE` bodies. Catalogued as
Expand Down Expand Up @@ -71,5 +87,6 @@ it is hashed into the contract `Actor` id (GoonCitizen network genesis).
1. **New mesh features** use the outer types above — not new one-off opcodes per app.
2. **App-specific semantics** go in `CONTRACT_MESSAGE` body `type` + `object` under a published contract id.
3. **Ignore unknown namespaces** — never crash the Peer on unfamiliar `contract` ids.
4. **Hub invite JSON** (`FederationContractInvite` v2) is the shared join/policy shape.
5. Prefer importing names from `@fabric/core/functions/applicationNamespaces` rather than duplicating string literals.
4. **Invite JSON** (`FederationContractInvite` v2) is the shared join/policy shape — parse/build lives in **`@fabric/http/functions/federationContractInvite`** (keep JSON bridges out of core).
5. Prefer importing body-type names from `@fabric/core/functions/applicationNamespaces` rather than duplicating string literals.
6. **`contract:message` events** expose `wireMessage` / `messageHex` so apps can attach bit-identical AMP frames to journal rows.
4 changes: 4 additions & 0 deletions docs/DISTRIBUTED_EXECUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Hub’s longer narrative (Beacon, delegation, signing rounds): hub.fabric.pub
|--------|------|
| `fabricCanonicalJson` | Deterministic digests (Actor / sidechain / Program) |
| `beaconFederationSigning` | Epoch commitment strings + federation witness verify |
| `contractStateSigning` | Contract-namespace tip Schnorr (`ContractStateTip`); same witness shape as Beacon |
| `contractTaproot` | Failover ladder → P2TR (`toAddress` / `toTaprootContract`); `after`/`until` decay + migrate |
| `contractTierWhen` | Off-chain `when` predicates for tier activation |
| `contractCapability` | Token issue/verify for `OP_CONTRACT_READ` / `OP_CONTRACT_SIGN` |
| `fabricProgramManifest` | Manifest v1 (`programId` / `programHash` / allowed types / optional `sidechainPolicy`) |
| **`sidechainState`** | Sealed JSON document: digests, RFC6902 patches, path policy, journal, snapshots, Beacon tip restore, contract namespace seals |

Expand Down
14 changes: 13 additions & 1 deletion functions/applicationNamespaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,19 @@ const CONTRACT_BODY_TYPES = Object.freeze({
GroupChange: 'GroupChange',
GroupShare: 'GroupShare',
/** Merkle root + digests of cumulative local history leaves under a Group namespace. */
GroupActivityTree: 'GroupActivityTree'
GroupActivityTree: 'GroupActivityTree',
/** Request missing Statechain journal entries (fromClock → tip). */
GroupJournalRequest: 'GroupJournalRequest',
/** Catch-up batch of accepted journal rows + tip Schnorr (k-of-n members). */
GroupJournalBatch: 'GroupJournalBatch',
/** Optional tip attestation: folded stateDigest signed by member threshold. */
GroupStateJournal: 'GroupStateJournal',
/** Token-backed contract role grant (reader / signer). */
ContractCapabilityGrant: 'ContractCapabilityGrant',
/** Propose spend or decay-migrate from contract Taproot UTXO. */
ContractWithdrawalRequest: 'ContractWithdrawalRequest',
/** Co-signer witness for a withdrawal / migration. */
ContractWithdrawalWitness: 'ContractWithdrawalWitness'
});

/**
Expand Down
118 changes: 118 additions & 0 deletions functions/contractCapability.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use strict';

/**
* Contract-scoped Token capabilities (read-only member vs signer).
*/

const Key = require('../types/key');
const Token = require('../types/token');

const OP_CONTRACT_READ = 'OP_CONTRACT_READ';
const OP_CONTRACT_SIGN = 'OP_CONTRACT_SIGN';

/**
* @param {object} opts
* @param {object|Key} opts.issuerKey Fabric Key (or settings) of issuer
* @param {string} opts.subject Subject compressed pubkey
* @param {string} opts.contractId Contract / namespace id
* @param {string} [opts.capability=OP_CONTRACT_READ]
* @param {number} [opts.expiresInSeconds]
* @returns {string} Token.toSignedString()
*/
function issueContractCapability (opts = {}) {
const capability = opts.capability || OP_CONTRACT_READ;
if (capability !== OP_CONTRACT_READ && capability !== OP_CONTRACT_SIGN) {
throw new Error(`unsupported contract capability: ${capability}`);
}
const contractId = String(opts.contractId || '').trim().toLowerCase();
if (!contractId) throw new Error('contractId required');
const subject = String(opts.subject || '').trim().toLowerCase();
if (!subject) throw new Error('subject required');
const issuer = opts.issuerKey && typeof opts.issuerKey.sign === 'function'
? opts.issuerKey
: new Key(opts.issuerKey || {});
const token = new Token({
capability,
issuer,
subject,
ctx: { contractId }
});
return token.toSignedString({
expiresInSeconds: opts.expiresInSeconds,
ctx: { contractId }
});
}

/**
* @param {string} tokenString
* @param {object} expect
* @param {string} expect.contractId
* @param {string} [expect.expectedCap]
* @param {string} [expect.subject]
* @param {Key|object} [expect.issuerKey] Verify Schnorr against this key (auth path)
* @param {boolean} [expect.allowUnverified] Opt-in parse-only (no sig check); result has verified:false
* @returns {{ cap: string, iss: string, sub: string, iat: number, exp: number, ctx?: object, verified?: boolean }|null}
*/
function verifyContractCapability (tokenString, expect = {}) {
const contractId = String(expect.contractId || '').trim().toLowerCase();
if (!contractId) return null;
let payload = null;
let verified = false;
if (expect.issuerKey) {
const key = expect.issuerKey && typeof expect.issuerKey.verify === 'function'
? expect.issuerKey
: new Key(expect.issuerKey);
payload = Token.verifySigned(tokenString, key);
verified = !!payload;
} else if (expect.allowUnverified === true) {
// Parse-only: not authorization. Callers must not treat this as a verified grant.
if (!tokenString || typeof tokenString !== 'string') return null;
const parts = tokenString.split('.');
if (parts.length !== 2) return null;
try {
const { tryParseWireJson } = require('./wireJson');
const payloadStr = Token.base64UrlDecode(parts[0]);
const pr = tryParseWireJson(payloadStr);
if (!pr.ok) return null;
payload = pr.value;
if (!payload || payload.exp == null || Date.now() / 1000 > payload.exp) return null;
verified = false;
} catch (_) {
return null;
}
} else {
return null;
}
if (!payload) return null;
const ctxId = payload.ctx && payload.ctx.contractId
? String(payload.ctx.contractId).trim().toLowerCase()
: '';
if (ctxId !== contractId) return null;
if (expect.expectedCap && payload.cap !== expect.expectedCap) return null;
if (expect.subject) {
const sub = String(payload.sub || '').trim().toLowerCase();
if (sub !== String(expect.subject).trim().toLowerCase()) return null;
}
return Object.assign({}, payload, { verified });
}

function roleToCapability (role) {
const r = String(role || '').toLowerCase();
if (r === 'signer' || r === 'sign') return OP_CONTRACT_SIGN;
return OP_CONTRACT_READ;
}

function capabilityToRole (cap) {
if (cap === OP_CONTRACT_SIGN) return 'signer';
if (cap === OP_CONTRACT_READ) return 'reader';
return null;
}

module.exports = {
OP_CONTRACT_READ,
OP_CONTRACT_SIGN,
issueContractCapability,
verifyContractCapability,
roleToCapability,
capabilityToRole
};
143 changes: 143 additions & 0 deletions functions/contractStateSigning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict';

/**
* Contract-namespace tip attestation (k-of-n Schnorr).
*
* Same witness shape as {@link beaconFederationSigning}: members / validators
* Schnorr-sign a canonical tip string. Used by GoonCitizen Group Statechain
* journals today; Hub contract sidechains SHOULD reuse this when sealing
* per-namespace tips (keep Hub docs / RPC in sync if the tip kind changes).
*
* @see functions/beaconFederationSigning.js
* @see docs/APPLICATION_NAMESPACES.md
* @see docs/DISTRIBUTED_EXECUTION.md
*/

const crypto = require('crypto');
const Key = require('../types/key');
const fabricCanonicalJson = require('./fabricCanonicalJson');
const { verifyFederationWitnessOnMessage } = require('./beaconFederationSigning');

/** Canonical tip kind — Hub and apps must agree; bump only with protocol note. */
const CONTRACT_STATE_TIP_KIND = 'ContractStateTip';

/**
* UTF-8 string members Schnorr-sign for a contract-namespace tip.
* @param {object} fields
* @param {string} fields.contractId
* @param {number} fields.clock
* @param {string} fields.stateDigest
* @returns {string}
*/
function signingStringForContractStateTip (fields = {}) {
return fabricCanonicalJson({
version: 1,
kind: CONTRACT_STATE_TIP_KIND,
contractId: String(fields.contractId || '').trim().toLowerCase(),
clock: Number(fields.clock) || 0,
stateDigest: String(fields.stateDigest || '').trim().toLowerCase()
});
}

/**
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @returns {Buffer}
*/
function tipMessageBuffer (contractId, clock, stateDigest) {
return Buffer.from(signingStringForContractStateTip({ contractId, clock, stateDigest }), 'utf8');
}

/**
* Sign a tip with a Fabric Key (or `{ xprv }` / Key-like).
* @param {object} keyOrSettings Key instance or settings for `new Key(...)`
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @returns {{ pubkey: string, signature: string, message: string }}
*/
function signContractStateTip (keyOrSettings, contractId, clock, stateDigest) {
const key = keyOrSettings && typeof keyOrSettings.signSchnorr === 'function'
? keyOrSettings
: new Key(keyOrSettings || {});
const message = signingStringForContractStateTip({ contractId, clock, stateDigest });
const signature = Buffer.from(key.signSchnorr(Buffer.from(message, 'utf8'))).toString('hex');
return { pubkey: key.pubkey, signature, message };
}

/**
* Verify k-of-n tip signatures (Federation witness shape).
* @param {string[]} validatorPubkeys
* @param {number} threshold
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @param {{ [pubkey: string]: string }|{ signatures: object }} signaturesOrWitness
* @returns {boolean}
*/
function verifyContractStateTip (
validatorPubkeys,
threshold,
contractId,
clock,
stateDigest,
signaturesOrWitness
) {
const thr = Number(threshold);
if (!Number.isInteger(thr) || thr < 1) {
throw new Error('threshold must be a positive integer');
}
const witness = signaturesOrWitness && signaturesOrWitness.signatures
&& typeof signaturesOrWitness.signatures === 'object'
? signaturesOrWitness
: { signatures: signaturesOrWitness || {} };
return verifyFederationWitnessOnMessage(
tipMessageBuffer(contractId, clock, stateDigest),
witness,
validatorPubkeys,
thr
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Merge signature maps (pubkey → sig hex), last write wins per key.
* @param {...object} maps
* @returns {{ [pubkey: string]: string }}
*/
function mergeTipSignatures (...maps) {
const out = {};
for (const m of maps) {
if (!m || typeof m !== 'object') continue;
const src = m.signatures && typeof m.signatures === 'object' ? m.signatures : m;
for (const [pk, sig] of Object.entries(src)) {
if (typeof pk === 'string' && pk && typeof sig === 'string' && sig) {
out[pk] = sig;
}
}
}
return out;
}

/**
* Stable digest helper for tests / logging.
* @param {string} contractId
* @param {number} clock
* @param {string} stateDigest
* @returns {string} hex sha256 of tip message
*/
function tipDigestHex (contractId, clock, stateDigest) {
return crypto.createHash('sha256')
.update(tipMessageBuffer(contractId, clock, stateDigest))
.digest('hex');
}

module.exports = {
CONTRACT_STATE_TIP_KIND,
signingStringForContractStateTip,
tipMessageBuffer,
signContractStateTip,
verifyContractStateTip,
mergeTipSignatures,
tipDigestHex
};
Loading
Loading