@thirtified/eudi-dev 0.0.0-stage → 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/LICENSE +202 -0
- package/README.md +223 -2
- package/dist/binary.d.ts +42 -0
- package/dist/binary.js +76 -0
- package/dist/binary.js.map +1 -0
- package/dist/commands/credentials.d.ts +151 -0
- package/dist/commands/credentials.js +234 -0
- package/dist/commands/credentials.js.map +1 -0
- package/dist/commands/registrar.d.ts +116 -0
- package/dist/commands/registrar.js +150 -0
- package/dist/commands/registrar.js.map +1 -0
- package/dist/commands/trust.d.ts +58 -0
- package/dist/commands/trust.js +90 -0
- package/dist/commands/trust.js.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +41 -0
- package/dist/errors.js.map +1 -0
- package/dist/eudi.d.ts +66 -0
- package/dist/eudi.js +113 -0
- package/dist/eudi.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +32 -0
- package/dist/input.js +7 -0
- package/dist/input.js.map +1 -0
- package/dist/runner.d.ts +86 -0
- package/dist/runner.js +246 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +338 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +24 -0
- package/dist/version.js +35 -0
- package/dist/version.js.map +1 -0
- package/package.json +75 -4
- package/src/binary.ts +114 -0
- package/src/commands/credentials.ts +384 -0
- package/src/commands/registrar.ts +243 -0
- package/src/commands/trust.ts +123 -0
- package/src/errors.ts +66 -0
- package/src/eudi.ts +159 -0
- package/src/index.ts +44 -0
- package/src/input.ts +27 -0
- package/src/runner.ts +310 -0
- package/src/types.ts +389 -0
- package/src/version.ts +36 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { FileInput } from '../input.js';
|
|
2
|
+
import { ArgList, type Runner, type RunOptions, Scratch } from '../runner.js';
|
|
3
|
+
import type {
|
|
4
|
+
AccessCertificateResult,
|
|
5
|
+
DcqlQuery,
|
|
6
|
+
Enrolment,
|
|
7
|
+
EnrolmentResult,
|
|
8
|
+
RegistrationCertificateResult,
|
|
9
|
+
RegistrationExtensionResult,
|
|
10
|
+
RelyingParty,
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
import type { CallOptions } from './credentials.js';
|
|
13
|
+
|
|
14
|
+
function runOpts(opts: CallOptions, stdin?: string): RunOptions {
|
|
15
|
+
const r: RunOptions = {};
|
|
16
|
+
if (opts.timeoutMs !== undefined) r.timeoutMs = opts.timeoutMs;
|
|
17
|
+
if (opts.signal !== undefined) r.signal = opts.signal;
|
|
18
|
+
if (stdin !== undefined) r.stdin = stdin;
|
|
19
|
+
return r;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Registration fields shared by verifiers and issuers. */
|
|
23
|
+
export interface PartyOptions {
|
|
24
|
+
/** Trade name. Required for a new registration. */
|
|
25
|
+
name: string;
|
|
26
|
+
/** An EUID or an organizationIdentifier. Default: an assigned EUID. */
|
|
27
|
+
identifier?: string;
|
|
28
|
+
/** Default `name`. */
|
|
29
|
+
legalName?: string;
|
|
30
|
+
/** Country code. Default the identifier's country. */
|
|
31
|
+
country?: string;
|
|
32
|
+
/** Web or email address. */
|
|
33
|
+
supportUri?: string;
|
|
34
|
+
/** Service identifier (the OU in access certificates). */
|
|
35
|
+
serviceId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface IntendedUseOptions {
|
|
39
|
+
/** Shown in the consent dialog. */
|
|
40
|
+
purpose: string;
|
|
41
|
+
/** DCQL query with the credentials and claims to register. */
|
|
42
|
+
dcql: DcqlQuery | FileInput;
|
|
43
|
+
/** Privacy policy URL. Default a placeholder page on the wallet. */
|
|
44
|
+
privacyPolicy?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type AddVerifierOptions = PartyOptions & IntendedUseOptions & CallOptions;
|
|
48
|
+
export type ExtendVerifierOptions = IntendedUseOptions &
|
|
49
|
+
CallOptions & {
|
|
50
|
+
/** Identifier of the registered relying party to add the intended use to. */
|
|
51
|
+
to: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface AttestationOptions {
|
|
55
|
+
/** `format:type`, e.g. `dc+sd-jwt:urn:eudi:pid:1` or `mso_mdoc:eu.europa.ec.eudi.pid.1`. */
|
|
56
|
+
attestations: string[];
|
|
57
|
+
/** Provider entitlement categories: pid, qeaa, pub-eaa, eaa. Default from the catalogue. */
|
|
58
|
+
categories?: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type AddIssuerOptions = PartyOptions & AttestationOptions & CallOptions;
|
|
62
|
+
export type ExtendIssuerOptions = AttestationOptions & CallOptions & { to: string };
|
|
63
|
+
|
|
64
|
+
export interface AccessCertOptions extends CallOptions {
|
|
65
|
+
/** Identifier of the registered relying party. */
|
|
66
|
+
identifier: string;
|
|
67
|
+
/** PEM certificate signing request. */
|
|
68
|
+
csr: FileInput;
|
|
69
|
+
serviceId?: string;
|
|
70
|
+
/** Extra DNS names for the x509_san_dns client identifier. */
|
|
71
|
+
dns?: string[];
|
|
72
|
+
/** Go duration, at most one year. */
|
|
73
|
+
validity?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RegistrationCertOptions extends CallOptions {
|
|
77
|
+
identifier: string;
|
|
78
|
+
serviceId?: string;
|
|
79
|
+
/** Default the relying party's only intended use. */
|
|
80
|
+
intendedUse?: string;
|
|
81
|
+
/** Certify the attestation provider service instead of an intended use. */
|
|
82
|
+
provider?: boolean;
|
|
83
|
+
/** Issue a new certificate and revoke the current one (`--new`). */
|
|
84
|
+
renew?: boolean;
|
|
85
|
+
/** Validity of a new certificate as a Go duration. Requires `renew` when one exists. */
|
|
86
|
+
validity?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RegistrationScopeOptions extends CallOptions {
|
|
90
|
+
identifier: string;
|
|
91
|
+
intendedUse?: string;
|
|
92
|
+
serviceId?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isDcqlObject(v: unknown): v is DcqlQuery {
|
|
96
|
+
return typeof v === 'object' && v !== null && !(v instanceof Uint8Array) && Array.isArray((v as { credentials?: unknown }).credentials);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function partyArgs(args: ArgList, p: PartyOptions): ArgList {
|
|
100
|
+
return args
|
|
101
|
+
.value('name', p.name)
|
|
102
|
+
.value('identifier', p.identifier)
|
|
103
|
+
.value('legal-name', p.legalName)
|
|
104
|
+
.value('country', p.country)
|
|
105
|
+
.value('support-uri', p.supportUri)
|
|
106
|
+
.value('service-id', p.serviceId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function intendedUseArgs(args: ArgList, u: IntendedUseOptions, scratch: Scratch): Promise<ArgList> {
|
|
110
|
+
const dcql = isDcqlObject(u.dcql) ? JSON.stringify(u.dcql) : u.dcql;
|
|
111
|
+
return args
|
|
112
|
+
.value('purpose', u.purpose)
|
|
113
|
+
.value('dcql', await scratch.file(dcql, 'dcql.json'))
|
|
114
|
+
.value('privacy-policy', u.privacyPolicy);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function attestationArgs(args: ArgList, a: AttestationOptions): ArgList {
|
|
118
|
+
return args.repeat('attestation', a.attestations).repeat('category', a.categories);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class PartyCommands<AddOpts, ExtendOpts> {
|
|
122
|
+
constructor(
|
|
123
|
+
protected readonly runner: Runner,
|
|
124
|
+
private readonly role: 'verifiers' | 'issuers',
|
|
125
|
+
private readonly build: (args: ArgList, opts: AddOpts | ExtendOpts, scratch: Scratch) => Promise<ArgList>,
|
|
126
|
+
) {}
|
|
127
|
+
|
|
128
|
+
/** `registrar <role> list`. */
|
|
129
|
+
async list(opts: CallOptions = {}): Promise<RelyingParty[]> {
|
|
130
|
+
const { document } = await this.runner.run<RelyingParty[]>(['registrar', this.role, 'list', ...(opts.extraArgs ?? [])], runOpts(opts));
|
|
131
|
+
return document;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `registrar <role> add`: register a new relying party. */
|
|
135
|
+
async add(opts: AddOpts & CallOptions): Promise<RelyingParty> {
|
|
136
|
+
return this.runAdd<RelyingParty>(opts);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `registrar <role> add --to`: extend an existing registration. */
|
|
140
|
+
async extend(opts: ExtendOpts & CallOptions & { to: string }): Promise<RegistrationExtensionResult> {
|
|
141
|
+
return this.runAdd<RegistrationExtensionResult>(opts);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `registrar <role> rm <identifier>`: remove it and revoke its registration certificates. */
|
|
145
|
+
async remove(identifier: string, opts: CallOptions = {}): Promise<{ removed: string }> {
|
|
146
|
+
const { document } = await this.runner.run<{ removed: string }>(
|
|
147
|
+
['registrar', this.role, 'rm', ...(opts.extraArgs ?? []), identifier],
|
|
148
|
+
runOpts(opts),
|
|
149
|
+
);
|
|
150
|
+
return document;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async runAdd<T>(opts: (AddOpts | ExtendOpts) & CallOptions & { to?: string }): Promise<T> {
|
|
154
|
+
const scratch = new Scratch();
|
|
155
|
+
try {
|
|
156
|
+
const args = await this.build(new ArgList('registrar', this.role, 'add'), opts, scratch);
|
|
157
|
+
args.value('to', opts.to).push(...(opts.extraArgs ?? []));
|
|
158
|
+
const { document } = await this.runner.run<T>(args.args, runOpts(opts));
|
|
159
|
+
return document;
|
|
160
|
+
} finally {
|
|
161
|
+
await scratch.cleanup();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* `eudi registrar ...` against the wallet in this instance's EUDI_DEV_HOME.
|
|
168
|
+
* Registrations persist there between calls.
|
|
169
|
+
*/
|
|
170
|
+
export class RegistrarCommands {
|
|
171
|
+
readonly verifiers: PartyCommands<AddVerifierOptions, ExtendVerifierOptions>;
|
|
172
|
+
readonly issuers: PartyCommands<AddIssuerOptions, ExtendIssuerOptions>;
|
|
173
|
+
|
|
174
|
+
constructor(private readonly runner: Runner) {
|
|
175
|
+
this.verifiers = new PartyCommands(runner, 'verifiers', async (args, opts, scratch) => {
|
|
176
|
+
if (!('to' in opts && opts.to)) partyArgs(args, opts as PartyOptions);
|
|
177
|
+
return intendedUseArgs(args, opts, scratch);
|
|
178
|
+
});
|
|
179
|
+
this.issuers = new PartyCommands(runner, 'issuers', async (args, opts) => {
|
|
180
|
+
if (!('to' in opts && opts.to)) partyArgs(args, opts as PartyOptions);
|
|
181
|
+
return attestationArgs(args, opts);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `registrar enrol`: register a relying party and issue its certificates in one step. */
|
|
186
|
+
async enrol(enrolment: Enrolment, opts: CallOptions & { update?: boolean } = {}): Promise<EnrolmentResult> {
|
|
187
|
+
const args = new ArgList('registrar', 'enrol').on('update', opts.update).push(...(opts.extraArgs ?? []), '-');
|
|
188
|
+
const { document } = await this.runner.run<EnrolmentResult>(args.args, runOpts(opts, JSON.stringify(enrolment)));
|
|
189
|
+
return document;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `registrar access-cert`: issue an access certificate for a CSR. */
|
|
193
|
+
async accessCert(opts: AccessCertOptions): Promise<AccessCertificateResult> {
|
|
194
|
+
const scratch = new Scratch();
|
|
195
|
+
try {
|
|
196
|
+
const args = new ArgList('registrar', 'access-cert')
|
|
197
|
+
.value('identifier', opts.identifier)
|
|
198
|
+
.value('csr', await scratch.file(opts.csr, 'request.csr'))
|
|
199
|
+
.value('service-id', opts.serviceId)
|
|
200
|
+
.repeat('dns', opts.dns)
|
|
201
|
+
.value('validity', opts.validity)
|
|
202
|
+
.push(...(opts.extraArgs ?? []));
|
|
203
|
+
const { document } = await this.runner.run<AccessCertificateResult>(args.args, runOpts(opts));
|
|
204
|
+
return document;
|
|
205
|
+
} finally {
|
|
206
|
+
await scratch.cleanup();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `registrar registration-cert`: the current registration certificate, issuing one if needed. */
|
|
211
|
+
async registrationCert(opts: RegistrationCertOptions): Promise<RegistrationCertificateResult> {
|
|
212
|
+
const args = new ArgList('registrar', 'registration-cert')
|
|
213
|
+
.value('identifier', opts.identifier)
|
|
214
|
+
.value('service-id', opts.serviceId)
|
|
215
|
+
.value('intended-use', opts.intendedUse)
|
|
216
|
+
.on('provider', opts.provider)
|
|
217
|
+
.on('new', opts.renew)
|
|
218
|
+
.value('validity', opts.validity)
|
|
219
|
+
.push(...(opts.extraArgs ?? []));
|
|
220
|
+
const { document } = await this.runner.run<RegistrationCertificateResult>(args.args, runOpts(opts));
|
|
221
|
+
return document;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** `registrar revoke`: revoke registration certificates. Returns how many changed. */
|
|
225
|
+
async revoke(opts: RegistrationScopeOptions): Promise<{ changed: number }> {
|
|
226
|
+
return this.setStatus('revoke', opts);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** `registrar activate`: reactivate revoked registration certificates. */
|
|
230
|
+
async activate(opts: RegistrationScopeOptions): Promise<{ changed: number }> {
|
|
231
|
+
return this.setStatus('activate', opts);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async setStatus(cmd: 'revoke' | 'activate', opts: RegistrationScopeOptions): Promise<{ changed: number }> {
|
|
235
|
+
const args = new ArgList('registrar', cmd)
|
|
236
|
+
.value('identifier', opts.identifier)
|
|
237
|
+
.value('intended-use', opts.intendedUse)
|
|
238
|
+
.value('service-id', opts.serviceId)
|
|
239
|
+
.push(...(opts.extraArgs ?? []));
|
|
240
|
+
const { document } = await this.runner.run<{ changed: number }>(args.args, runOpts(opts));
|
|
241
|
+
return document;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { FileInput } from '../input.js';
|
|
2
|
+
import { ArgList, type Runner, type RunOptions, Scratch } from '../runner.js';
|
|
3
|
+
import type { Jwks, TrustEntity, TrustListLink, TrustOverview } from '../types.js';
|
|
4
|
+
import type { CallOptions } from './credentials.js';
|
|
5
|
+
|
|
6
|
+
function runOpts(opts: CallOptions): RunOptions {
|
|
7
|
+
const r: RunOptions = {};
|
|
8
|
+
if (opts.timeoutMs !== undefined) r.timeoutMs = opts.timeoutMs;
|
|
9
|
+
if (opts.signal !== undefined) r.signal = opts.signal;
|
|
10
|
+
return r;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The wallet's own lists, or the `tl-` id of a custom list. */
|
|
14
|
+
export type TrustListId = 'pid' | 'qeaa' | 'pub-eaa' | 'eaa' | 'wallet-provider' | 'access-ca' | 'registrar' | 'lists' | (string & {});
|
|
15
|
+
|
|
16
|
+
export interface TrustShowOptions extends CallOptions {
|
|
17
|
+
/** List id. Default: the PID provider list. */
|
|
18
|
+
list?: TrustListId;
|
|
19
|
+
/** Select the list covering this SD-JWT VCT. */
|
|
20
|
+
vct?: string;
|
|
21
|
+
/** Select the list covering this mdoc docType. */
|
|
22
|
+
doctype?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TrustUrlOptions extends TrustShowOptions {
|
|
26
|
+
/** Wallet server port the URL is built for. Default 8085 upstream. */
|
|
27
|
+
port?: number;
|
|
28
|
+
/** Use host.docker.internal instead of localhost. */
|
|
29
|
+
docker?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TrustAddCaOptions extends CallOptions {
|
|
33
|
+
/** One of `TrustOverview.entity_lists`, e.g. `pid`, `eaa`, `trusted-list-ca`. */
|
|
34
|
+
list: string;
|
|
35
|
+
/** PEM with the CA certificates. */
|
|
36
|
+
ca: FileInput;
|
|
37
|
+
/** Provider name on the list. Default the CA's common name. */
|
|
38
|
+
name?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `eudi trust ...` against the wallet in this instance's EUDI_DEV_HOME. The
|
|
43
|
+
* wallet (with its CA) is created on first use.
|
|
44
|
+
*/
|
|
45
|
+
export class TrustCommands {
|
|
46
|
+
constructor(private readonly runner: Runner) {}
|
|
47
|
+
|
|
48
|
+
/** `trust list`: the wallet's trusted lists and the CAs and lists you added. */
|
|
49
|
+
async list(opts: CallOptions = {}): Promise<TrustOverview> {
|
|
50
|
+
const { document } = await this.runner.run<TrustOverview>(['trust', 'list', ...(opts.extraArgs ?? [])], runOpts(opts));
|
|
51
|
+
return document;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `trust show`: one of the wallet's trusted lists as a signed JWT. */
|
|
55
|
+
async show(opts: TrustShowOptions = {}): Promise<string> {
|
|
56
|
+
const args = new ArgList('trust', 'show')
|
|
57
|
+
.value('vct', opts.vct)
|
|
58
|
+
.value('doctype', opts.doctype)
|
|
59
|
+
.push(...(opts.extraArgs ?? []));
|
|
60
|
+
if (opts.list) args.push(opts.list);
|
|
61
|
+
const { document } = await this.runner.run<{ trust_list: string }>(args.args, runOpts(opts));
|
|
62
|
+
return document.trust_list;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `trust show --url`: where a wallet server on `port` would serve the list. */
|
|
66
|
+
async url(opts: TrustUrlOptions = {}): Promise<string> {
|
|
67
|
+
const args = new ArgList('trust', 'show', '--url')
|
|
68
|
+
.value('port', opts.port)
|
|
69
|
+
.on('docker', opts.docker)
|
|
70
|
+
.value('vct', opts.vct)
|
|
71
|
+
.value('doctype', opts.doctype)
|
|
72
|
+
.push(...(opts.extraArgs ?? []));
|
|
73
|
+
if (opts.list) args.push(opts.list);
|
|
74
|
+
const { document } = await this.runner.run<{ url: string }>(args.args, runOpts(opts));
|
|
75
|
+
return document.url;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `trust ca-cert`: the shared wallet CA as PEM. */
|
|
79
|
+
async caCert(opts: CallOptions = {}): Promise<string> {
|
|
80
|
+
const { document } = await this.runner.run<{ pem: string }>(['trust', 'ca-cert', ...(opts.extraArgs ?? [])], runOpts(opts));
|
|
81
|
+
return document.pem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `trust ca-cert --jwks`: the shared wallet CA as a JWKS with x5c. */
|
|
85
|
+
async caJwks(opts: CallOptions = {}): Promise<Jwks> {
|
|
86
|
+
const { document } = await this.runner.run<Jwks>(['trust', 'ca-cert', '--jwks', ...(opts.extraArgs ?? [])], runOpts(opts));
|
|
87
|
+
return document;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `trust add-ca`: put a CA on one of the wallet's lists. */
|
|
91
|
+
async addCa(opts: TrustAddCaOptions): Promise<TrustEntity> {
|
|
92
|
+
const scratch = new Scratch();
|
|
93
|
+
try {
|
|
94
|
+
const args = new ArgList('trust', 'add-ca')
|
|
95
|
+
.value('list', opts.list)
|
|
96
|
+
.value('ca', await scratch.file(opts.ca, 'ca.pem'))
|
|
97
|
+
.value('name', opts.name)
|
|
98
|
+
.push(...(opts.extraArgs ?? []));
|
|
99
|
+
const { document } = await this.runner.run<TrustEntity>(args.args, runOpts(opts));
|
|
100
|
+
return document;
|
|
101
|
+
} finally {
|
|
102
|
+
await scratch.cleanup();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `trust rm-ca <id>`. */
|
|
107
|
+
async removeCa(id: string, opts: CallOptions = {}): Promise<{ removed: string }> {
|
|
108
|
+
const { document } = await this.runner.run<{ removed: string }>(['trust', 'rm-ca', ...(opts.extraArgs ?? []), id], runOpts(opts));
|
|
109
|
+
return document;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `trust add-list <url>`: add an external list to the list of trusted lists. */
|
|
113
|
+
async addList(url: string, opts: CallOptions = {}): Promise<TrustListLink> {
|
|
114
|
+
const { document } = await this.runner.run<TrustListLink>(['trust', 'add-list', ...(opts.extraArgs ?? []), url], runOpts(opts));
|
|
115
|
+
return document;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `trust rm-list <url>`. */
|
|
119
|
+
async removeList(url: string, opts: CallOptions = {}): Promise<{ removed: string }> {
|
|
120
|
+
const { document } = await this.runner.run<{ removed: string }>(['trust', 'rm-list', ...(opts.extraArgs ?? []), url], runOpts(opts));
|
|
121
|
+
return document;
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Details of a failed eudi-dev invocation. */
|
|
2
|
+
export interface EudiDevErrorDetails {
|
|
3
|
+
/** Arguments passed to the binary (without the binary path). */
|
|
4
|
+
args: readonly string[];
|
|
5
|
+
/** Exit code, or `null` when the process was killed by a signal. */
|
|
6
|
+
exitCode: number | null;
|
|
7
|
+
/** Signal that killed the process, if any. */
|
|
8
|
+
signal: NodeJS.Signals | null;
|
|
9
|
+
/** Everything the process wrote to stderr. */
|
|
10
|
+
stderr: string;
|
|
11
|
+
/** Everything the process wrote to stdout. */
|
|
12
|
+
stdout: string;
|
|
13
|
+
/**
|
|
14
|
+
* The JSON document from stdout, if one was printed. `validate` prints its
|
|
15
|
+
* report even when a check fails (upstream ADR 0020).
|
|
16
|
+
*/
|
|
17
|
+
document?: unknown;
|
|
18
|
+
/** Set when the process did not finish in time. */
|
|
19
|
+
timedOut?: boolean;
|
|
20
|
+
/** Underlying error (spawn failure, invalid JSON, ...). */
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Thrown when eudi-dev exits non-zero, cannot be started, times out, or
|
|
26
|
+
* breaks the one-JSON-document contract.
|
|
27
|
+
*
|
|
28
|
+
* eudi-dev only uses exit codes 0 and 1, so the reason is in {@link stderr}.
|
|
29
|
+
* {@link message} holds its last non-empty line, which is the error cobra
|
|
30
|
+
* printed.
|
|
31
|
+
*/
|
|
32
|
+
export class EudiDevError extends Error {
|
|
33
|
+
override readonly name = 'EudiDevError';
|
|
34
|
+
readonly args: readonly string[];
|
|
35
|
+
readonly exitCode: number | null;
|
|
36
|
+
readonly signal: NodeJS.Signals | null;
|
|
37
|
+
readonly stderr: string;
|
|
38
|
+
readonly stdout: string;
|
|
39
|
+
readonly document: unknown;
|
|
40
|
+
readonly timedOut: boolean;
|
|
41
|
+
|
|
42
|
+
constructor(message: string, details: EudiDevErrorDetails) {
|
|
43
|
+
super(message, details.cause !== undefined ? { cause: details.cause } : undefined);
|
|
44
|
+
this.args = details.args;
|
|
45
|
+
this.exitCode = details.exitCode;
|
|
46
|
+
this.signal = details.signal;
|
|
47
|
+
this.stderr = details.stderr;
|
|
48
|
+
this.stdout = details.stdout;
|
|
49
|
+
this.document = details.document;
|
|
50
|
+
this.timedOut = details.timedOut ?? false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Thrown when the eudi-dev binary cannot be located, is not executable, or has an unsupported version. */
|
|
55
|
+
export class EudiDevBinaryError extends Error {
|
|
56
|
+
override readonly name = 'EudiDevBinaryError';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns the last non-empty line of a stderr dump. */
|
|
60
|
+
export function lastLine(text: string): string {
|
|
61
|
+
const lines = text
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.map((l) => l.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
return lines.at(-1) ?? '';
|
|
66
|
+
}
|
package/src/eudi.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { type ResolveBinaryOptions, resolveBinary } from './binary.js';
|
|
6
|
+
import {
|
|
7
|
+
type CallOptions,
|
|
8
|
+
type DecodeOptions,
|
|
9
|
+
dcql,
|
|
10
|
+
decode,
|
|
11
|
+
type IssueJwtOptions,
|
|
12
|
+
type IssueMdocOptions,
|
|
13
|
+
type IssueResult,
|
|
14
|
+
type IssueSdJwtOptions,
|
|
15
|
+
issueJwt,
|
|
16
|
+
issueMdoc,
|
|
17
|
+
issueSdJwt,
|
|
18
|
+
type ValidateOptions,
|
|
19
|
+
type ValidateResult,
|
|
20
|
+
validate,
|
|
21
|
+
version,
|
|
22
|
+
} from './commands/credentials.js';
|
|
23
|
+
import { RegistrarCommands } from './commands/registrar.js';
|
|
24
|
+
import { TrustCommands } from './commands/trust.js';
|
|
25
|
+
import { EudiDevBinaryError } from './errors.js';
|
|
26
|
+
import type { Input } from './input.js';
|
|
27
|
+
import { Runner, type RunOptions, type RunResult } from './runner.js';
|
|
28
|
+
import type { DcqlQuery, DecodeResult, VersionInfo } from './types.js';
|
|
29
|
+
import { parseVersion, SUPPORTED_MAJOR } from './version.js';
|
|
30
|
+
|
|
31
|
+
export interface EudiDevOptions extends ResolveBinaryOptions {
|
|
32
|
+
/**
|
|
33
|
+
* EUDI_DEV_HOME for every call: wallet, CA, templates, registrar state.
|
|
34
|
+
* Default: a new temp directory per instance, removed by {@link EudiDev.dispose}.
|
|
35
|
+
*/
|
|
36
|
+
home?: string;
|
|
37
|
+
/** Extra environment variables for every call. `undefined` removes one. */
|
|
38
|
+
env?: Record<string, string | undefined>;
|
|
39
|
+
/** Pass the caller's EUDI_DEV_* variables through. Default `false`. */
|
|
40
|
+
inheritEudiEnv?: boolean;
|
|
41
|
+
/** Default timeout per call in ms. Default 60 000. 0 turns it off. */
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
/** Working directory of the child process. */
|
|
44
|
+
cwd?: string;
|
|
45
|
+
/** Accept a binary whose major version isn't {@link SUPPORTED_MAJOR}. Default `false`. */
|
|
46
|
+
allowAnyVersion?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A handle on the eudi-dev CLI with its own state directory.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* await using eudi = await EudiDev.create();
|
|
54
|
+
* const { credential } = await eudi.issue.sdjwt({ pid: true });
|
|
55
|
+
* const { valid } = await eudi.validate(credential);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class EudiDev implements AsyncDisposable {
|
|
59
|
+
/** Path of the binary in use. */
|
|
60
|
+
readonly bin: string;
|
|
61
|
+
/** EUDI_DEV_HOME of this instance. */
|
|
62
|
+
readonly home: string;
|
|
63
|
+
/** Version reported by the binary. */
|
|
64
|
+
readonly version: VersionInfo;
|
|
65
|
+
|
|
66
|
+
readonly issue: {
|
|
67
|
+
sdjwt(opts?: IssueSdJwtOptions): Promise<IssueResult>;
|
|
68
|
+
jwt(opts?: IssueJwtOptions): Promise<IssueResult>;
|
|
69
|
+
mdoc(opts?: IssueMdocOptions): Promise<IssueResult>;
|
|
70
|
+
};
|
|
71
|
+
readonly trust: TrustCommands;
|
|
72
|
+
readonly registrar: RegistrarCommands;
|
|
73
|
+
|
|
74
|
+
private readonly runner: Runner;
|
|
75
|
+
private readonly ownsHome: boolean;
|
|
76
|
+
private disposed = false;
|
|
77
|
+
|
|
78
|
+
private constructor(runner: Runner, versionInfo: VersionInfo, ownsHome: boolean) {
|
|
79
|
+
this.runner = runner;
|
|
80
|
+
this.bin = runner.bin;
|
|
81
|
+
this.home = runner.home;
|
|
82
|
+
this.version = versionInfo;
|
|
83
|
+
this.ownsHome = ownsHome;
|
|
84
|
+
this.issue = {
|
|
85
|
+
sdjwt: (opts) => issueSdJwt(this.runner, opts),
|
|
86
|
+
jwt: (opts) => issueJwt(this.runner, opts),
|
|
87
|
+
mdoc: (opts) => issueMdoc(this.runner, opts),
|
|
88
|
+
};
|
|
89
|
+
this.trust = new TrustCommands(runner);
|
|
90
|
+
this.registrar = new RegistrarCommands(runner);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolves the binary, checks its version and sets up the home directory. */
|
|
94
|
+
static async create(options: EudiDevOptions = {}): Promise<EudiDev> {
|
|
95
|
+
const bin = await resolveBinary(options);
|
|
96
|
+
const ownsHome = options.home === undefined;
|
|
97
|
+
const home = options.home ?? (await mkdtemp(join(tmpdir(), 'eudi-dev-home-')));
|
|
98
|
+
const config: ConstructorParameters<typeof Runner>[0] = { bin, home };
|
|
99
|
+
if (options.env) config.env = options.env;
|
|
100
|
+
if (options.inheritEudiEnv !== undefined) config.inheritEudiEnv = options.inheritEudiEnv;
|
|
101
|
+
if (options.timeoutMs !== undefined) config.timeoutMs = options.timeoutMs;
|
|
102
|
+
if (options.cwd !== undefined) config.cwd = options.cwd;
|
|
103
|
+
const runner = new Runner(config);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const info = await version(runner);
|
|
107
|
+
const parsed = parseVersion(info.version);
|
|
108
|
+
// A "dev" build has no version to check.
|
|
109
|
+
if (parsed && parsed.major !== SUPPORTED_MAJOR && !options.allowAnyVersion) {
|
|
110
|
+
throw new EudiDevBinaryError(
|
|
111
|
+
`${bin} is eudi-dev ${info.version}, but this package supports v${SUPPORTED_MAJOR}.x. Pass allowAnyVersion to use it anyway.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return new EudiDev(runner, info, ownsHome);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (ownsHome) await rm(home, { recursive: true, force: true });
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Runs any eudi-dev command with `--json` and returns its parsed document. */
|
|
122
|
+
run<T = unknown>(args: readonly string[], opts?: RunOptions): Promise<RunResult<T>> {
|
|
123
|
+
this.assertOpen();
|
|
124
|
+
return this.runner.run<T>(args, opts);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `eudi decode`. */
|
|
128
|
+
decode(input: Input, opts?: DecodeOptions): Promise<DecodeResult> {
|
|
129
|
+
this.assertOpen();
|
|
130
|
+
return decode(this.runner, input, opts);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `eudi validate`. A failing check gives `valid: false` instead of throwing. */
|
|
134
|
+
validate(input: Input, opts?: ValidateOptions): Promise<ValidateResult> {
|
|
135
|
+
this.assertOpen();
|
|
136
|
+
return validate(this.runner, input, opts);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `eudi dcql`. */
|
|
140
|
+
dcql(input: Input, opts?: CallOptions): Promise<DcqlQuery> {
|
|
141
|
+
this.assertOpen();
|
|
142
|
+
return dcql(this.runner, input, opts);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Removes the home directory if this instance created it. */
|
|
146
|
+
async dispose(): Promise<void> {
|
|
147
|
+
if (this.disposed) return;
|
|
148
|
+
this.disposed = true;
|
|
149
|
+
if (this.ownsHome) await rm(this.home, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
153
|
+
await this.dispose();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private assertOpen(): void {
|
|
157
|
+
if (this.disposed) throw new Error('EudiDev instance is disposed');
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PLATFORM_PACKAGES,
|
|
3
|
+
type PlatformPackage,
|
|
4
|
+
platformPackage,
|
|
5
|
+
type ResolveBinaryOptions,
|
|
6
|
+
resolveBinary,
|
|
7
|
+
} from './binary.js';
|
|
8
|
+
export {
|
|
9
|
+
type CallOptions,
|
|
10
|
+
type CredentialCategory,
|
|
11
|
+
type DecodeFormat,
|
|
12
|
+
type DecodeOptions,
|
|
13
|
+
type IssueCommonOptions,
|
|
14
|
+
type IssueDisplayOptions,
|
|
15
|
+
type IssueJwtOptions,
|
|
16
|
+
type IssueMdocOptions,
|
|
17
|
+
type IssueRegistrationOptions,
|
|
18
|
+
type IssueResult,
|
|
19
|
+
type IssueSdJwtOptions,
|
|
20
|
+
isDecodedCredential,
|
|
21
|
+
type ValidateOptions,
|
|
22
|
+
type ValidateResult,
|
|
23
|
+
} from './commands/credentials.js';
|
|
24
|
+
export type {
|
|
25
|
+
AccessCertOptions,
|
|
26
|
+
AddIssuerOptions,
|
|
27
|
+
AddVerifierOptions,
|
|
28
|
+
AttestationOptions,
|
|
29
|
+
ExtendIssuerOptions,
|
|
30
|
+
ExtendVerifierOptions,
|
|
31
|
+
IntendedUseOptions,
|
|
32
|
+
PartyCommands,
|
|
33
|
+
PartyOptions,
|
|
34
|
+
RegistrarCommands,
|
|
35
|
+
RegistrationCertOptions,
|
|
36
|
+
RegistrationScopeOptions,
|
|
37
|
+
} from './commands/registrar.js';
|
|
38
|
+
export type { TrustAddCaOptions, TrustCommands, TrustListId, TrustShowOptions, TrustUrlOptions } from './commands/trust.js';
|
|
39
|
+
export { EudiDevBinaryError, EudiDevError, type EudiDevErrorDetails } from './errors.js';
|
|
40
|
+
export { EudiDev, type EudiDevOptions } from './eudi.js';
|
|
41
|
+
export type { FileInput, Input } from './input.js';
|
|
42
|
+
export { Runner, type RunnerConfig, type RunOptions, type RunResult } from './runner.js';
|
|
43
|
+
export * from './types.js';
|
|
44
|
+
export { EUDI_DEV_VERSION, SUPPORTED_MAJOR, UPSTREAM_REPO } from './version.js';
|
package/src/input.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input to a command that reads a credential, request or certificate.
|
|
3
|
+
*
|
|
4
|
+
* - A string or bytes is the content itself. It goes to the binary on stdin
|
|
5
|
+
* (as the `-` argument), so there is no argv length limit.
|
|
6
|
+
* - `{ path }` is a file the binary reads. It must exist: upstream treats a
|
|
7
|
+
* path that doesn't exist as raw input.
|
|
8
|
+
* - `{ url }` is an http(s) URL the binary fetches.
|
|
9
|
+
*/
|
|
10
|
+
export type Input = string | Uint8Array | { path: string } | { url: string };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A file-valued flag (`--key`, `--trusted-list`, `--csr`, `--dcql`, ...).
|
|
14
|
+
*
|
|
15
|
+
* A string or bytes is the content. The runner writes it to a private temp
|
|
16
|
+
* file and passes that path. `{ path }` passes an existing file and `{ url }`
|
|
17
|
+
* a URL for flags that accept one.
|
|
18
|
+
*/
|
|
19
|
+
export type FileInput = string | Uint8Array | { path: string } | { url: string };
|
|
20
|
+
|
|
21
|
+
export function isPathInput(v: unknown): v is { path: string } {
|
|
22
|
+
return typeof v === 'object' && v !== null && !(v instanceof Uint8Array) && typeof (v as { path?: unknown }).path === 'string';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isUrlInput(v: unknown): v is { url: string } {
|
|
26
|
+
return typeof v === 'object' && v !== null && !(v instanceof Uint8Array) && typeof (v as { url?: unknown }).url === 'string';
|
|
27
|
+
}
|