Skip to content

Commit ee54655

Browse files
authored
Merge branch 'hyperledger-identus:main' into main
2 parents bda6b2d + 6831efe commit ee54655

7 files changed

Lines changed: 357 additions & 13 deletions

File tree

AGENTS.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Identus TypeScript SDK — Agent Instructions
2+
3+
## Developer Commands
4+
5+
| Command | What it does |
6+
|---------|-------------|
7+
| `yarn build` | Full build via `build.sh` (cleans first, then builds in dependency order) |
8+
| `yarn test` | Run all package tests (`npx nx run-many --target=test`) |
9+
| `yarn lint` | ESLint across all packages (`npx nx run-many --target=lint`) |
10+
| `yarn lint:fix` | ESLint with `--fix` across all packages |
11+
| `yarn lint:text` | markdownlint-cli2 + yamllint + editorconfig-checker |
12+
| `yarn lint:text:fix` | markdownlint-cli2 with `--fix` |
13+
| `yarn clean` | Removes generated dirs, build dirs, and resets Nx cache |
14+
| `yarn clean:generated` | Removes all `generated/` folders (WASM bindings only — protobuf `.ts` output in `packages/lib/protos/src/` is overwritten on rebuild, not deleted by this command) |
15+
| `yarn clean:build` | Removes all `build/` folders |
16+
| `yarn clean:reset` | Resets Nx cache (`npx nx reset`) |
17+
| `yarn coverage` | Vitest coverage across all workspace projects (`packages/lib/*`, `shared/*`, `wasm/*`) via root `vitest.config.js` |
18+
| `yarn coverage:nx` | Coverage across all Nx packages via `nx run-many` (includes typecheck dependency) |
19+
| `yarn docs` | TypeDoc generation across packages |
20+
21+
## Single-Package Commands
22+
23+
Nx projects are addressed by their `package.json` `name`. Key project names:
24+
25+
- `@hyperledger/identus-protos` — protobuf definitions (`packages/lib/protos`)
26+
- `@hyperledger/identus-domain` — shared domain types (`packages/shared/domain`)
27+
- `@hyperledger/identus-anoncreds` — AnonCreds WASM wrapper (`packages/wasm/anoncreds`)
28+
- `@hyperledger/identus-didcomm` — DIDComm WASM wrapper (`packages/wasm/didcomm`)
29+
- `@hyperledger/identus-jwe` — JWE WASM wrapper (`packages/wasm/jwe`)
30+
- `@hyperledger/identus-sdk` — main SDK (`packages/lib/sdk`)
31+
32+
```bash
33+
npx nx test @hyperledger/identus-sdk # test one package
34+
npx nx build @hyperledger/identus-domain # build one package
35+
npx nx lint @hyperledger/identus-anoncreds # lint one package
36+
npx nx test-watch @hyperledger/identus-sdk # vitest watch mode
37+
npx nx coverage @hyperledger/identus-sdk # coverage for one package
38+
npx nx docs @hyperledger/identus-sdk # TypeDoc for SDK
39+
```
40+
41+
The `--tui false` flag disables Nx interactive UI in CI/terminals where TUI fails.
42+
43+
## Build Order & Prerequisites
44+
45+
**Required toolchain (all must be installed before building):**
46+
47+
1. **Node.js** — LTS version (20 recommended)
48+
2. **Rust nightly + cargo** — required to compile WASM crates
49+
3. **wasm-pack** — builds Rust → WASM with JS bindings (`curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh`)
50+
4. **protobuf-compiler** (`protoc`) — compiles `.proto` files to TypeScript
51+
52+
**First-time build — submodules must be initialized first:**
53+
54+
```bash
55+
bash externals/run.sh -x update # git submodule update --init --recursive, then builds WASM
56+
```
57+
58+
**Build sequence** (as coded in `build.sh`):
59+
60+
1. `yarn clean` + `npx nx reset`
61+
2. `@hyperledger/identus-protos:build` (runs `protoc` then `tsup`)
62+
3. `@hyperledger/identus-domain:build`
63+
4. `@hyperledger/identus-anoncreds:build` (depends on `externals` target)
64+
5. `@hyperledger/identus-didcomm:build` (depends on `externals` target)
65+
6. `@hyperledger/identus-jwe:build` (depends on `externals` target)
66+
7. `@hyperledger/identus-sdk:build`
67+
68+
WASM packages have an `externals` Nx target that runs `externals/run.sh -x update` if the submodule commit has changed. The build target depends on `externals` implicitly.
69+
70+
## Monorepo Structure
71+
72+
Yarn 4 workspaces + Nx. Three workspace groups:
73+
74+
```
75+
packages/
76+
├── lib/
77+
│ ├── protos/ — @hyperledger/identus-protos (protobuf definitions)
78+
│ └── sdk/ — @hyperledger/identus-sdk (main SDK)
79+
├── shared/
80+
│ └── domain/ — @hyperledger/identus-domain (shared types/interfaces)
81+
└── wasm/
82+
├── anoncreds/ — @hyperledger/identus-anoncreds (AnonCreds WASM wrapper)
83+
├── config/ — Nx/Vitest shared WASM config (scope:config tag)
84+
├── didcomm/ — @hyperledger/identus-didcomm (DIDComm WASM wrapper)
85+
└── jwe/ — @hyperledger/identus-jwe (JWE WASM wrapper)
86+
87+
externals/
88+
├── anoncreds/ — Git submodule: Rust AnonCreds crate source
89+
├── didcomm/ — Git submodule: Rust DIDComm crate source
90+
├── run.sh — Submodule check/build/update script (-x check|build|update)
91+
└── *.commit — Tracks last-built submodule commit hashes
92+
```
93+
94+
## Package Architecture
95+
96+
The main SDK (`packages/lib/sdk`) contains the Identus building blocks:
97+
98+
- **Apollo** (`src/apollo/`) — Cryptographic operations (key generation, signing, etc.)
99+
- **Castor** (`src/castor/`) — DID creation, management, and resolution
100+
- **Mercury** (`src/mercury/`) — DIDComm V2 message handling
101+
- **Pluto** (`src/pluto/`) — Storage interface (portable, storage-agnostic)
102+
- **Agent** (`src/edge-agent/`) — Edge agent using all building blocks; implements DIDComm V2 protocols
103+
- **Pollux** (`src/pollux/`) — Verifiable credentials logic
104+
- **Plugins** (`src/plugins/`) — Optional: anoncreds, didcomm, oidc, dif, oea
105+
106+
The SDK exports sub-path entry points: `@hyperledger/identus-sdk/plugins/anoncreds`, `/plugins/didcomm`, `/plugins/oidc`, `/plugins/dif`, `/plugins/oea`.
107+
108+
## Generated Code
109+
110+
**Never edit generated files.** They are overwritten on next build.
111+
112+
- `packages/wasm/anoncreds/generated/` — WASM bindings from `wasm-pack build` on the AnonCreds Rust crate
113+
- `packages/wasm/didcomm/generated/` — WASM bindings from `wasm-pack build` on the DIDComm Rust crate
114+
- `packages/wasm/jwe/generated/` — WASM bindings from `wasm-pack build` on the JWE Rust crate
115+
- `packages/lib/protos/src/*.ts` — TypeScript output from `protoc` (invoked via `npm run protos` inside the protos package)
116+
117+
Clean all generated code: `yarn clean:generated` (finds and removes all `generated/` directories).
118+
119+
## Build Artifacts
120+
121+
- `packages/*/build/` — compiled JS + type declarations from `tsup`
122+
- `.nx/` — Nx computation cache (deleted by `yarn clean:reset` / `npx nx reset`)
123+
- `coverage/` — Vitest coverage output
124+
125+
## Testing
126+
127+
- **Unit tests**: Vitest config per package (`vitest.config.js`). Tests live in `packages/*/tests/`.
128+
- **SDK tests** use `jsdom` environment (not Node) and load WASM via a custom Vite plugin (`WasmPlugin` in `packages/lib/sdk/vitest.config.js`) that reads `.wasm` files and exports them as base64.
129+
- **Test aliases** resolve `@hyperledger/identus-*-wasm` to local `packages/wasm/*/generated/` dirs.
130+
- **testTimeout**: 12 seconds for SDK tests.
131+
- **E2E tests**: `integration-tests/e2e-tests/` — requires running Cloud Agent and Mediator. Run via `./integration-tests/e2e-tests/run.sh` or `npm run test:sdk` from within the e2e-tests directory. Requires `.env` configuration (see `integration-tests/e2e-tests/README.md`).
132+
133+
## Linting
134+
135+
- **ESLint**: flat config (`eslint.base.mjs`) with type-checked rules from `typescript-eslint`. Errors for async safety (`no-floating-promises`, `no-misused-promises`); warnings for `any` usage. Namespace declarations allowed (`@typescript-eslint/no-namespace: off`).
136+
- **Excluded from ESLint**: `**/dist/**`, `**/build/**`, `**/node_modules/**`, `**/coverage/**`, `**/*.d.ts`, `**/generated/**`, `externals/**`, `src/castor/protos/**`
137+
- **Text lint** (`yarn lint:text`): markdownlint-cli2, yamllint, editorconfig-checker
138+
- **Lint fix**: `yarn lint:fix` (ESLint), `yarn lint:text:fix` (markdownlint only)
139+
140+
## Release
141+
142+
- Nx manages independent per-project releases (`projectsRelationship: "independent"` in `nx.json`)
143+
- `release.sh` runs as the `preVersionCommand` — generates TypeDoc (`yarn docs`) and stages the output (`git add docs`) before versioning
144+
- Conventional commits required for changelog generation
145+
- Tag pattern: `{projectName}@{version}`
146+
- GitHub releases created automatically by Nx

docs/decisions/20231110-sdk-package-release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
- Deciders: [Javier Ribó](https://github.com/elribonazo) + [Gonçalo](https://github.com/goncalo-frade-iohk)
55
- Date: 2023-11-10
66

7-
Technical Story: <https://input-output.atlassian.net/browse/ATL-6147>
7+
Technical Story: [ATL-6147](https://input-output.atlassian.net/browse/ATL-6147)
88

99
## Context and Problem Statement
1010

packages/lib/sdk/src/plugins/internal/oea/sdjwt/PresentationRequest.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as Domain from "@hyperledger/identus-domain";
22
import { Payload } from "@hyperledger/identus-domain";
3+
import type { KBOptions } from "@sd-jwt/types";
34
import { SDJWTCredential } from "../../../../pollux/models/SDJWTVerifiableCredential";
45
import { OEA } from "../types";
56
import { Plugins } from "../../../../plugins";
@@ -16,6 +17,7 @@ export class PresentationRequest extends Plugins.Task<Args> {
1617

1718
if (credential instanceof SDJWTCredential) {
1819
const subjectDID = Domain.DID.from(credential.subject);
20+
const presentationRequest = this.args.presentationRequest;
1921
const keys = await ctx.Pluto.getDIDPrivateKeysByDID(subjectDID);
2022
const privateKey = keys.find((key) => key.curve === Domain.Curve.ED25519.toString());
2123

@@ -25,10 +27,27 @@ export class PresentationRequest extends Plugins.Task<Args> {
2527

2628
// [ ] https://github.com/hyperledger-identus/sdk-ts/issues/362 PresentationFrame
2729
const presentationFrame = this.args.presentationFrame ?? {};
30+
31+
// Build Key Binding JWT options when a challenge (nonce) is provided.
32+
// The KB-JWT cryptographically binds the presentation to the verifier's
33+
// challenge, preventing replay attacks (SD-JWT-VC I-D §5.1).
34+
const challenge = presentationRequest.options?.challenge;
35+
const domain = presentationRequest.options?.domain;
36+
const kb: KBOptions | undefined = challenge
37+
? {
38+
payload: {
39+
nonce: challenge,
40+
aud: domain ?? "",
41+
iat: Math.floor(Date.now() / 1000),
42+
},
43+
}
44+
: undefined;
45+
2846
const presentationJWS = await ctx.SDJWT.createPresentationFor({
2947
jws: credential.id,
3048
presentationFrame,
3149
privateKey,
50+
kb,
3251
});
3352

3453
return Payload.make(OEA.PRISM_SDJWT, presentationJWS);

packages/lib/sdk/src/pollux/utils/jwt/SDJWT.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { base64url } from "multiformats/bases/base64";
33
import { type SDJWTVCConfig, SDJwtVcInstance, type SdJwtVcPayload, } from '@sd-jwt/sd-jwt-vc';
44
import { type Disclosure } from '@sd-jwt/utils';
55
import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode';
6-
import type { DisclosureFrame, PresentationFrame } from '@sd-jwt/types';
6+
import type { DisclosureFrame, KBOptions, PresentationFrame } from '@sd-jwt/types';
77
import type * as Domain from '@hyperledger/identus-domain';
88
import { PolluxError, CastorError } from '@hyperledger/identus-domain';
99
import { SDJWTCredential } from '../../models/SDJWTVerifiableCredential';
@@ -126,9 +126,14 @@ export class SDJWT extends Task.Runner {
126126
jws: string,
127127
privateKey: Domain.PrivateKey,
128128
presentationFrame?: PresentationFrame<T>;
129+
kb?: KBOptions;
129130
}) {
130131
const sdjwt = new SDJwtVcInstance(this.getSKConfig(options.privateKey));
131-
return sdjwt.present<T>(options.jws, options.presentationFrame);
132+
return sdjwt.present<T>(
133+
options.jws,
134+
options.presentationFrame,
135+
options.kb ? { kb: options.kb } : undefined
136+
);
132137
}
133138

134139
async reveal(
@@ -177,18 +182,20 @@ export class SDJWT extends Task.Runner {
177182
}
178183

179184
public getSKConfig(privateKey: Domain.PrivateKey): SDJWTVCConfig {
185+
const signerFn = async (data: string | Uint8Array) => {
186+
if (!privateKey.isSignable()) {
187+
throw new PolluxError.InvalidCredentialError("Cannot sign with this key: key does not support signing");
188+
}
189+
const signature = privateKey.sign(Buffer.from(data));
190+
return base64url.baseEncode(signature);
191+
};
180192
return {
181193
hashAlg: defaultHashConfig.hasherAlg,
182194
hasher: defaultHashConfig.hasher,
183195
signAlg: privateKey.alg,
184-
signer: async (data: string | Uint8Array) => {
185-
if (!privateKey.isSignable()) {
186-
throw new PolluxError.InvalidCredentialError("Cannot sign with this key: key does not support signing");
187-
}
188-
const signature = privateKey.sign(Buffer.from(data));
189-
const signatureEncoded = base64url.baseEncode(signature);
190-
return signatureEncoded
191-
},
196+
signer: signerFn,
197+
kbSignAlg: privateKey.alg,
198+
kbSigner: signerFn,
192199
saltGenerator: (length: number) => this.saltGenerator(length)
193200
};
194201
}

packages/lib/sdk/tests/castor/DIDUrlParser.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,39 @@
11
import { describe, it, expect, test, beforeEach, afterEach } from 'vitest';
2+
import { DID } from "@hyperledger/identus-domain";
3+
import { DIDUrl } from "@hyperledger/identus-domain";
24

35
import * as DIDUrlParser from "../../src/castor/parser/DIDUrlParser";
46

57
describe("DIDUrlParser", () => {
8+
it("DIDUrl.string() should include path and query parameters", () => {
9+
// BUG: DIDUrl.string() drops path and parameters.
10+
// pathString() and queryString() helpers exist but string()
11+
// never calls them. Direct construction isolates string()
12+
// from the parser entirely.
13+
14+
const baseDID = DID.fromString("did:prism:123456");
15+
16+
// Primary case: path + parameters + fragment all present
17+
const full = new DIDUrl(
18+
baseDID,
19+
["path", "to", "resource"],
20+
new Map([["query", "1"]]),
21+
"fragment"
22+
);
23+
expect(full.string()).toBe(
24+
"did:prism:123456/path/to/resource?query=1#fragment"
25+
);
26+
27+
// Regression guard: fragment-only must still work
28+
const fragmentOnly = new DIDUrl(baseDID, [], new Map(), "fragment");
29+
expect(fragmentOnly.string()).toBe("did:prism:123456#fragment");
30+
31+
// Regression guard: path only, no parameters, no fragment
32+
// (also catches the secondary empty-fragment "#" bug)
33+
const pathOnly = new DIDUrl(baseDID, ["resource"], new Map(), "");
34+
expect(pathOnly.string()).toBe("did:prism:123456/resource");
35+
});
36+
637
it("should test valid URLs", () => {
738
const didExample1 = "did:example:123456:adsd/path?query=something#fragment";
839
const didExample2 =

0 commit comments

Comments
 (0)