@ghub/gopencsr-client 0.0.0-stage → 0.2.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 +115 -0
- package/dist/canonical.d.ts +9 -0
- package/dist/canonical.js +38 -0
- package/dist/client.d.ts +218 -0
- package/dist/client.js +373 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/package.json +38 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Copyright (c) 2026 Gelhaus Solutions (Enno Gelhaus). All rights reserved.
|
|
2
|
+
|
|
3
|
+
Elastic License 2.0 (ELv2)
|
|
4
|
+
|
|
5
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
14
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
15
|
+
available, and prepare derivative works of the software, in each case subject to
|
|
16
|
+
the limitations and conditions below.
|
|
17
|
+
|
|
18
|
+
## Limitations
|
|
19
|
+
|
|
20
|
+
You may not provide the software to third parties as a hosted or managed
|
|
21
|
+
service, where the service provides users with access to any substantial set of
|
|
22
|
+
the features or functionality of the software.
|
|
23
|
+
|
|
24
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
25
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
26
|
+
software that is protected by the license key.
|
|
27
|
+
|
|
28
|
+
You may not alter, remove, or obscure any licensing, copyright, or other notices
|
|
29
|
+
of the licensor in the software. Any use of the licensor's trademarks is subject
|
|
30
|
+
to applicable law.
|
|
31
|
+
|
|
32
|
+
## Patents
|
|
33
|
+
|
|
34
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
35
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
36
|
+
sale, import and have imported the software, in each case subject to the
|
|
37
|
+
limitations and conditions in this license. This license does not cover any
|
|
38
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
39
|
+
the software. If you or your company make any written claim that the software
|
|
40
|
+
infringes or contributes to infringement of any patent, your patent license for
|
|
41
|
+
the software granted under these terms ends immediately. If your company makes
|
|
42
|
+
such a claim, your patent license ends immediately for work on behalf of your
|
|
43
|
+
company.
|
|
44
|
+
|
|
45
|
+
## Notices
|
|
46
|
+
|
|
47
|
+
You must ensure that anyone who gets a copy of any part of the software from you
|
|
48
|
+
also gets a copy of these terms.
|
|
49
|
+
|
|
50
|
+
If you modify the software, you must include in any modified copies of the
|
|
51
|
+
software prominent notices stating that you have modified the software.
|
|
52
|
+
|
|
53
|
+
## No Other Rights
|
|
54
|
+
|
|
55
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
56
|
+
these terms.
|
|
57
|
+
|
|
58
|
+
## Termination
|
|
59
|
+
|
|
60
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
61
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
62
|
+
with a notice of your violation, and you cease all violation of this license no
|
|
63
|
+
later than 30 days after you receive that notice, your licenses will be
|
|
64
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
65
|
+
reinstatement, any additional violation of these terms will cause your licenses
|
|
66
|
+
to terminate automatically and permanently.
|
|
67
|
+
|
|
68
|
+
## No Liability
|
|
69
|
+
|
|
70
|
+
*As far as the law allows, the software comes as is, without any warranty or
|
|
71
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
72
|
+
out of these terms or the use or nature of the software, under any kind of legal
|
|
73
|
+
claim.*
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the entity offering these terms, and the **software** is the
|
|
78
|
+
software the licensor makes available under these terms, including any portion
|
|
79
|
+
of it.
|
|
80
|
+
|
|
81
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
84
|
+
organization that you work for, plus all organizations that have control over,
|
|
85
|
+
are under the control of, or are under common control with that organization.
|
|
86
|
+
**control** means ownership of substantially all the assets of an entity, or the
|
|
87
|
+
power to direct its management and policies by vote, contract, or otherwise.
|
|
88
|
+
Control can be direct or indirect.
|
|
89
|
+
|
|
90
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
91
|
+
these terms.
|
|
92
|
+
|
|
93
|
+
**use** means anything you do with the software requiring one of your licenses.
|
|
94
|
+
|
|
95
|
+
**trademark** means trademarks, service marks, and similar rights.
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Additional restriction
|
|
99
|
+
|
|
100
|
+
In addition to the limitations in the Elastic License 2.0 above, and as a
|
|
101
|
+
condition of the licence granted by it:
|
|
102
|
+
|
|
103
|
+
You may not use the software for commercial purposes without a separate
|
|
104
|
+
business licence granted in writing by Gelhaus Solutions.
|
|
105
|
+
|
|
106
|
+
Commercial purposes include use in, by or for a business or other commercial
|
|
107
|
+
undertaking, whether or not the software is provided to anyone outside it, and
|
|
108
|
+
whether or not a fee is charged. Non-commercial use is not restricted by this
|
|
109
|
+
paragraph.
|
|
110
|
+
|
|
111
|
+
This restriction is ours and is not part of the Elastic License 2.0. The
|
|
112
|
+
Elastic License 2.0 by itself permits commercial internal use; this paragraph
|
|
113
|
+
removes that permission. Where the two conflict, this paragraph applies.
|
|
114
|
+
|
|
115
|
+
For a business licence, contact egelhaus@ennogelhaus.de.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 8785 canonical JSON (JCS), as @gopencsr/contracts writes it: one value always has one
|
|
3
|
+
* serialisation, so a registry computes the same change hash GOpenCSR's signing step shows and
|
|
4
|
+
* signs. Object keys are sorted by UTF-16 code units, which is how JavaScript compares strings;
|
|
5
|
+
* strings and numbers are written as ECMAScript's JSON.stringify writes them, which is what RFC
|
|
6
|
+
* 8785 specifies.
|
|
7
|
+
*/
|
|
8
|
+
export declare function canonicalJson(value: unknown): string;
|
|
9
|
+
//# sourceMappingURL=canonical.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.canonicalJson = canonicalJson;
|
|
4
|
+
/**
|
|
5
|
+
* RFC 8785 canonical JSON (JCS), as @gopencsr/contracts writes it: one value always has one
|
|
6
|
+
* serialisation, so a registry computes the same change hash GOpenCSR's signing step shows and
|
|
7
|
+
* signs. Object keys are sorted by UTF-16 code units, which is how JavaScript compares strings;
|
|
8
|
+
* strings and numbers are written as ECMAScript's JSON.stringify writes them, which is what RFC
|
|
9
|
+
* 8785 specifies.
|
|
10
|
+
*/
|
|
11
|
+
function canonicalJson(value) {
|
|
12
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "number") {
|
|
16
|
+
if (!Number.isFinite(value))
|
|
17
|
+
throw new Error("canonical JSON has no form for a non-finite number");
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value))
|
|
21
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
22
|
+
if (typeof value === "object") {
|
|
23
|
+
// A plain object only. A Date, a Buffer or a Decimal would be written through its own
|
|
24
|
+
// members here, while JSON.stringify and a jsonb column go through its toJSON: the log and
|
|
25
|
+
// the database would then hold two different bodies.
|
|
26
|
+
const prototype = Object.getPrototypeOf(value);
|
|
27
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
28
|
+
const kind = value.constructor?.name;
|
|
29
|
+
throw new Error(`canonical JSON has no form for a ${kind ?? "non-plain object"}`);
|
|
30
|
+
}
|
|
31
|
+
const members = Object.entries(value)
|
|
32
|
+
.filter(([, member]) => member !== undefined)
|
|
33
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
34
|
+
return `{${members.map(([key, member]) => `${JSON.stringify(key)}:${canonicalJson(member)}`).join(",")}}`;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`canonical JSON has no form for a ${typeof value}`);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=canonical.js.map
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** GOpenCSR could not be asked: unreachable, too slow, or failing on its side. */
|
|
2
|
+
export declare class CsrUnavailable extends Error {
|
|
3
|
+
readonly name = "CsrUnavailable";
|
|
4
|
+
}
|
|
5
|
+
/** GOpenCSR answered, and refused: what it said, for the log and the audit record. */
|
|
6
|
+
export declare class CsrRefused extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
readonly name = "CsrRefused";
|
|
10
|
+
constructor(status: number, reason: string);
|
|
11
|
+
}
|
|
12
|
+
/** What GOpenCSR's tokens say when the sign-in was a passkey (GOPENCSR-15). */
|
|
13
|
+
export declare const ACR_PASSKEY = "urn:gopencsr:acr:passkey";
|
|
14
|
+
/** What a registry asks GOpenCSR to tell it: who, their address and name, and their passkeys. */
|
|
15
|
+
export declare const SCOPE = "openid email profile roles";
|
|
16
|
+
/** The registries GOpenCSR signs changes for; every change hash names its own. */
|
|
17
|
+
export type Product = "cdr" | "cnr" | "csr";
|
|
18
|
+
/** The person as GOpenCSR's ID token describes them, checked. */
|
|
19
|
+
export interface Identity {
|
|
20
|
+
/** GOpenCSR's subject, the person's account id in every registry. */
|
|
21
|
+
sub: string;
|
|
22
|
+
acr: string;
|
|
23
|
+
/** When the person last signed in at GOpenCSR, by `acr`. */
|
|
24
|
+
authTime: Date;
|
|
25
|
+
/** GOpenCSR's grant. */
|
|
26
|
+
sid: string | null;
|
|
27
|
+
email: string | null;
|
|
28
|
+
emailVerified: boolean;
|
|
29
|
+
name: string | null;
|
|
30
|
+
/** How many passkeys the account holds at GOpenCSR, or null when the token did not say. */
|
|
31
|
+
passkeys: number | null;
|
|
32
|
+
/**
|
|
33
|
+
* The person's standing with the registry's documents, which GOpenCSR states in every token for
|
|
34
|
+
* the registry to enforce (consent per product): agreed; asked, with the date the version they
|
|
35
|
+
* have not accepted takes effect; restricted, allowed to sign in, read, export and use their
|
|
36
|
+
* data-protection rights and nothing else; null when the token said nothing.
|
|
37
|
+
*/
|
|
38
|
+
terms: {
|
|
39
|
+
state: string;
|
|
40
|
+
inForceFrom: Date | null;
|
|
41
|
+
} | null;
|
|
42
|
+
}
|
|
43
|
+
/** A token response, its ID token checked. */
|
|
44
|
+
export interface CsrTokens {
|
|
45
|
+
refreshToken: string;
|
|
46
|
+
accessExpiresAt: Date;
|
|
47
|
+
identity: Identity;
|
|
48
|
+
}
|
|
49
|
+
/** A change as GOpenCSR's signing step shows it (GOPENCSR-17). */
|
|
50
|
+
export interface ChangeDocument {
|
|
51
|
+
type: string;
|
|
52
|
+
object: string;
|
|
53
|
+
description: string;
|
|
54
|
+
changes: {
|
|
55
|
+
label: string;
|
|
56
|
+
before: string | null;
|
|
57
|
+
after: string | null;
|
|
58
|
+
}[];
|
|
59
|
+
detail?: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
/** A signing request as GOpenCSR reads it back to the client that made it. */
|
|
62
|
+
export interface SigningState {
|
|
63
|
+
id: string;
|
|
64
|
+
status: "open" | "signed" | "declined" | "moved" | "expired";
|
|
65
|
+
account: string;
|
|
66
|
+
hash: string;
|
|
67
|
+
signaturesRequired: number;
|
|
68
|
+
signatures: {
|
|
69
|
+
passkey: string;
|
|
70
|
+
signedAt: string;
|
|
71
|
+
receipt: string;
|
|
72
|
+
}[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* An entry of the one log as a registry delivers it (GOPENCSR-44): the registry's own id and time
|
|
76
|
+
* for it, a kind with the registry's prefix (cnr.change.signed), and the public body, ids and
|
|
77
|
+
* hashes only.
|
|
78
|
+
*/
|
|
79
|
+
export interface TlogEntry {
|
|
80
|
+
id: string;
|
|
81
|
+
kind: string;
|
|
82
|
+
/** As toISOString() writes it. */
|
|
83
|
+
time: string;
|
|
84
|
+
body: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
/** An entry as the one log holds it: its leaf byte for byte and, once appended, its index. */
|
|
87
|
+
export interface TlogEntryState {
|
|
88
|
+
id: string;
|
|
89
|
+
kind: string;
|
|
90
|
+
time: string;
|
|
91
|
+
leaf: string;
|
|
92
|
+
index: number | null;
|
|
93
|
+
appendedAt: string | null;
|
|
94
|
+
}
|
|
95
|
+
/** One passkey's signature, as GOpenCSR's receipt states it, checked. */
|
|
96
|
+
export interface Receipt {
|
|
97
|
+
passkey: string;
|
|
98
|
+
hash: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The SHA-256 GOpenCSR's signing step commits to, in hex: the change's canonical form with the
|
|
102
|
+
* registry's product added, as GOpenCSR computes it.
|
|
103
|
+
*/
|
|
104
|
+
export declare function changeHash(product: Product, change: ChangeDocument): string;
|
|
105
|
+
/** How a registry is GOpenCSR's client. */
|
|
106
|
+
export interface CsrClientOptions {
|
|
107
|
+
/** GOpenCSR's issuer, the origin people sign in at, such as https://csr.gplatform.org. */
|
|
108
|
+
issuer: string;
|
|
109
|
+
/**
|
|
110
|
+
* Where the registry's own calls to GOpenCSR go, when not the issuer: a direct link between
|
|
111
|
+
* the hosts. The browser always goes to the issuer.
|
|
112
|
+
*/
|
|
113
|
+
endpoints?: string | undefined;
|
|
114
|
+
clientId: string;
|
|
115
|
+
/** Where GOpenCSR sends the browser back, one of the client's registered redirect URIs. */
|
|
116
|
+
redirectUri: string;
|
|
117
|
+
/** The registry this client is, which every change hash names. */
|
|
118
|
+
product: Product;
|
|
119
|
+
/**
|
|
120
|
+
* The client secret, asked for at each authenticated call: the registry keeps it where its
|
|
121
|
+
* secrets are (Vault KV) and reads it again as often as it likes, so a new one takes without a
|
|
122
|
+
* restart.
|
|
123
|
+
*/
|
|
124
|
+
clientSecret: () => Promise<string>;
|
|
125
|
+
/** The scope asked for; SCOPE when left out. */
|
|
126
|
+
scope?: string;
|
|
127
|
+
/** The tests' own fetch and clock. */
|
|
128
|
+
fetch?: typeof fetch;
|
|
129
|
+
now?: () => number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A registry as GOpenCSR's client (GOPENCDR-333, extracted from GOpenCDR's api for every
|
|
133
|
+
* registry): the authorisation code flow with PKCE for sign-in and step-up, refresh and revocation
|
|
134
|
+
* of the tokens a session holds, the signing step that a change asks one or two passkeys through,
|
|
135
|
+
* and the delivery of the registry's entries to the one log. Every token is checked against GOpenCSR's published keys (EdDSA), which are read again
|
|
136
|
+
* for a key id not seen yet. Sessions, accounts and what a person may do stay the registry's own.
|
|
137
|
+
*/
|
|
138
|
+
export declare class CsrClient {
|
|
139
|
+
private readonly options;
|
|
140
|
+
private keys;
|
|
141
|
+
private readonly fetch;
|
|
142
|
+
private readonly now;
|
|
143
|
+
constructor(options: CsrClientOptions);
|
|
144
|
+
/** GOpenCSR's issuer, the origin people sign in at. */
|
|
145
|
+
get issuer(): string;
|
|
146
|
+
/** The authorisation request a browser takes to GOpenCSR. */
|
|
147
|
+
authorizeUrl(request: {
|
|
148
|
+
state: string;
|
|
149
|
+
nonce: string;
|
|
150
|
+
challenge: string;
|
|
151
|
+
/** For a step-up: a sign-in older than this many seconds is asked for again. */
|
|
152
|
+
maxAge?: number;
|
|
153
|
+
}): string;
|
|
154
|
+
/** The code GOpenCSR sent back, for tokens. The ID token must carry the nonce sent. */
|
|
155
|
+
exchange(code: string, verifier: string, nonce: string): Promise<CsrTokens>;
|
|
156
|
+
/**
|
|
157
|
+
* New tokens for a refresh token, which GOpenCSR replaces at each use; null when it refuses,
|
|
158
|
+
* because the person signed out there or the grant was revoked.
|
|
159
|
+
*/
|
|
160
|
+
refresh(refreshToken: string): Promise<CsrTokens | null>;
|
|
161
|
+
/** Revokes a refresh token, and with it GOpenCSR's grant (RFC 7009). */
|
|
162
|
+
revoke(refreshToken: string): Promise<void>;
|
|
163
|
+
/**
|
|
164
|
+
* Asks GOpenCSR for the person's signature over a change (GOPENCSR-17), and checks that the
|
|
165
|
+
* hash it will show and sign is the one the registry computed.
|
|
166
|
+
*/
|
|
167
|
+
requestSignature(request: {
|
|
168
|
+
account: string;
|
|
169
|
+
change: ChangeDocument;
|
|
170
|
+
returnUri: string;
|
|
171
|
+
signatures: 1 | 2;
|
|
172
|
+
}): Promise<{
|
|
173
|
+
id: string;
|
|
174
|
+
url: string;
|
|
175
|
+
hash: string;
|
|
176
|
+
expiresAt: string;
|
|
177
|
+
}>;
|
|
178
|
+
/** A signing request this client made, as GOpenCSR reads it back; null if it knows none. */
|
|
179
|
+
signingRequest(id: string): Promise<SigningState | null>;
|
|
180
|
+
/** Withdraws an open signing request whose change moved on: nothing can sign it any more. */
|
|
181
|
+
withdrawSignature(id: string): Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* A signature's receipt, checked: GOpenCSR's key, its type, made for this client and account,
|
|
184
|
+
* over this request; null for any receipt that is not all of those.
|
|
185
|
+
*/
|
|
186
|
+
receipt(token: string, expect: {
|
|
187
|
+
account: string;
|
|
188
|
+
request: string;
|
|
189
|
+
}): Promise<Receipt | null>;
|
|
190
|
+
private tokensOf;
|
|
191
|
+
/** The ID token's claims, checked (OpenID Connect Core 3.1.3.7), or null. */
|
|
192
|
+
private identityOf;
|
|
193
|
+
/**
|
|
194
|
+
* The claims of a compact JWS GOpenCSR signed, typed `typ`, or null for any that is not one.
|
|
195
|
+
* Times and audience are the caller's to check.
|
|
196
|
+
*/
|
|
197
|
+
private verifyJws;
|
|
198
|
+
private publicKeys;
|
|
199
|
+
/**
|
|
200
|
+
* Delivers entries to the one log (GOPENCSR-44), at least once: an id delivered again with the
|
|
201
|
+
* same leaf is the same entry, and with another it is refused. Each comes back as the log holds
|
|
202
|
+
* it, its leaf checked to be the one this registry wrote (the canonical JSON of id, kind, time
|
|
203
|
+
* and body), with its index once the appender added it, which is soon but not at once.
|
|
204
|
+
*/
|
|
205
|
+
deliverEntries(entries: TlogEntry[]): Promise<TlogEntryState[]>;
|
|
206
|
+
/** An entry of the one log as GOpenCSR holds it, by its id; null if it holds none. */
|
|
207
|
+
tlogEntry(id: string): Promise<TlogEntryState | null>;
|
|
208
|
+
private tokenCall;
|
|
209
|
+
/** client_secret_basic (RFC 6749 2.3.1): the id and the secret, each form-encoded. */
|
|
210
|
+
private authorization;
|
|
211
|
+
/**
|
|
212
|
+
* One call to GOpenCSR's api, authenticated as the registry's client unless `anonymous`. A
|
|
213
|
+
* refusal (4xx) throws CsrRefused; anything that kept an answer from coming (the network, a
|
|
214
|
+
* timeout, a 5xx) throws CsrUnavailable, which the caller does not take as a refusal.
|
|
215
|
+
*/
|
|
216
|
+
private call;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CsrClient = exports.SCOPE = exports.ACR_PASSKEY = exports.CsrRefused = exports.CsrUnavailable = void 0;
|
|
4
|
+
exports.changeHash = changeHash;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const canonical_1 = require("./canonical");
|
|
7
|
+
/** GOpenCSR could not be asked: unreachable, too slow, or failing on its side. */
|
|
8
|
+
class CsrUnavailable extends Error {
|
|
9
|
+
name = "CsrUnavailable";
|
|
10
|
+
}
|
|
11
|
+
exports.CsrUnavailable = CsrUnavailable;
|
|
12
|
+
/** GOpenCSR answered, and refused: what it said, for the log and the audit record. */
|
|
13
|
+
class CsrRefused extends Error {
|
|
14
|
+
status;
|
|
15
|
+
reason;
|
|
16
|
+
name = "CsrRefused";
|
|
17
|
+
constructor(status, reason) {
|
|
18
|
+
super(`gopencsr refused: HTTP ${status} ${reason}`);
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.reason = reason;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.CsrRefused = CsrRefused;
|
|
24
|
+
/** What GOpenCSR's tokens say when the sign-in was a passkey (GOPENCSR-15). */
|
|
25
|
+
exports.ACR_PASSKEY = "urn:gopencsr:acr:passkey";
|
|
26
|
+
/** What a registry asks GOpenCSR to tell it: who, their address and name, and their passkeys. */
|
|
27
|
+
exports.SCOPE = "openid email profile roles";
|
|
28
|
+
/** The JOSE type of a signing receipt, which nothing else of GOpenCSR's carries. */
|
|
29
|
+
const RECEIPT_TYPE = "gopencsr-signature+jwt";
|
|
30
|
+
/** Every call to GOpenCSR must answer within this. */
|
|
31
|
+
const TIMEOUT_MS = 10_000;
|
|
32
|
+
/** How long GOpenCSR's public keys are trusted before they are read again. */
|
|
33
|
+
const KEYS_FOR_MS = 5 * 60_000;
|
|
34
|
+
/** How far apart the two clocks may be. */
|
|
35
|
+
const LEEWAY_S = 60;
|
|
36
|
+
/**
|
|
37
|
+
* The SHA-256 GOpenCSR's signing step commits to, in hex: the change's canonical form with the
|
|
38
|
+
* registry's product added, as GOpenCSR computes it.
|
|
39
|
+
*/
|
|
40
|
+
function changeHash(product, change) {
|
|
41
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
42
|
+
.update((0, canonical_1.canonicalJson)({ ...change, product }))
|
|
43
|
+
.digest("hex");
|
|
44
|
+
}
|
|
45
|
+
const isString = (value) => typeof value === "string";
|
|
46
|
+
/** The terms claim, as GOpenCSR writes it: { state, in_force_from? }, or null for anything else. */
|
|
47
|
+
function termsOf(claim) {
|
|
48
|
+
if (typeof claim !== "object" || claim === null)
|
|
49
|
+
return null;
|
|
50
|
+
const { state, in_force_from: inForceFrom } = claim;
|
|
51
|
+
if (!isString(state))
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
state,
|
|
55
|
+
inForceFrom: typeof inForceFrom === "number" ? new Date(inForceFrom * 1000) : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A registry as GOpenCSR's client (GOPENCDR-333, extracted from GOpenCDR's api for every
|
|
60
|
+
* registry): the authorisation code flow with PKCE for sign-in and step-up, refresh and revocation
|
|
61
|
+
* of the tokens a session holds, the signing step that a change asks one or two passkeys through,
|
|
62
|
+
* and the delivery of the registry's entries to the one log. Every token is checked against GOpenCSR's published keys (EdDSA), which are read again
|
|
63
|
+
* for a key id not seen yet. Sessions, accounts and what a person may do stay the registry's own.
|
|
64
|
+
*/
|
|
65
|
+
class CsrClient {
|
|
66
|
+
options;
|
|
67
|
+
keys = null;
|
|
68
|
+
fetch;
|
|
69
|
+
now;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.options = options;
|
|
72
|
+
this.fetch = options.fetch ?? fetch;
|
|
73
|
+
this.now = options.now ?? Date.now;
|
|
74
|
+
}
|
|
75
|
+
/** GOpenCSR's issuer, the origin people sign in at. */
|
|
76
|
+
get issuer() {
|
|
77
|
+
return new URL(this.options.issuer).origin;
|
|
78
|
+
}
|
|
79
|
+
/** The authorisation request a browser takes to GOpenCSR. */
|
|
80
|
+
authorizeUrl(request) {
|
|
81
|
+
const url = new URL("/api/v1/oidc/authorize", this.issuer);
|
|
82
|
+
url.search = new URLSearchParams({
|
|
83
|
+
response_type: "code",
|
|
84
|
+
client_id: this.options.clientId,
|
|
85
|
+
redirect_uri: this.options.redirectUri,
|
|
86
|
+
scope: this.options.scope ?? exports.SCOPE,
|
|
87
|
+
state: request.state,
|
|
88
|
+
nonce: request.nonce,
|
|
89
|
+
code_challenge: request.challenge,
|
|
90
|
+
code_challenge_method: "S256",
|
|
91
|
+
acr_values: exports.ACR_PASSKEY,
|
|
92
|
+
...(request.maxAge === undefined ? {} : { max_age: String(request.maxAge) }),
|
|
93
|
+
}).toString();
|
|
94
|
+
return url.toString();
|
|
95
|
+
}
|
|
96
|
+
/** The code GOpenCSR sent back, for tokens. The ID token must carry the nonce sent. */
|
|
97
|
+
async exchange(code, verifier, nonce) {
|
|
98
|
+
const body = await this.tokenCall({
|
|
99
|
+
grant_type: "authorization_code",
|
|
100
|
+
code,
|
|
101
|
+
redirect_uri: this.options.redirectUri,
|
|
102
|
+
code_verifier: verifier,
|
|
103
|
+
});
|
|
104
|
+
return this.tokensOf(body, nonce);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* New tokens for a refresh token, which GOpenCSR replaces at each use; null when it refuses,
|
|
108
|
+
* because the person signed out there or the grant was revoked.
|
|
109
|
+
*/
|
|
110
|
+
async refresh(refreshToken) {
|
|
111
|
+
try {
|
|
112
|
+
return await this.tokensOf(await this.tokenCall({ grant_type: "refresh_token", refresh_token: refreshToken }), null);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof CsrRefused && error.status === 400)
|
|
116
|
+
return null;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Revokes a refresh token, and with it GOpenCSR's grant (RFC 7009). */
|
|
121
|
+
async revoke(refreshToken) {
|
|
122
|
+
await this.call("POST", "oidc/revoke", {
|
|
123
|
+
form: { token: refreshToken, token_type_hint: "refresh_token" },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Asks GOpenCSR for the person's signature over a change (GOPENCSR-17), and checks that the
|
|
128
|
+
* hash it will show and sign is the one the registry computed.
|
|
129
|
+
*/
|
|
130
|
+
async requestSignature(request) {
|
|
131
|
+
const created = (await this.call("POST", "signing-requests", { json: request }));
|
|
132
|
+
const hash = changeHash(this.options.product, request.change);
|
|
133
|
+
if (!isString(created.id) ||
|
|
134
|
+
!isString(created.url) ||
|
|
135
|
+
!isString(created.expiresAt) ||
|
|
136
|
+
created.hash !== hash) {
|
|
137
|
+
throw new Error("gopencsr: the signing request it made is not the change sent");
|
|
138
|
+
}
|
|
139
|
+
if (new URL(created.url).origin !== this.issuer) {
|
|
140
|
+
throw new Error("gopencsr: the signing page is not on its own origin");
|
|
141
|
+
}
|
|
142
|
+
return { id: created.id, url: created.url, hash, expiresAt: created.expiresAt };
|
|
143
|
+
}
|
|
144
|
+
/** A signing request this client made, as GOpenCSR reads it back; null if it knows none. */
|
|
145
|
+
async signingRequest(id) {
|
|
146
|
+
try {
|
|
147
|
+
return (await this.call("GET", `signing-requests/${encodeURIComponent(id)}`, {}));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof CsrRefused && error.status === 404)
|
|
151
|
+
return null;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Withdraws an open signing request whose change moved on: nothing can sign it any more. */
|
|
156
|
+
async withdrawSignature(id) {
|
|
157
|
+
await this.call("POST", `signing-requests/${encodeURIComponent(id)}/withdrawal`, {
|
|
158
|
+
json: { reason: "moved" },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A signature's receipt, checked: GOpenCSR's key, its type, made for this client and account,
|
|
163
|
+
* over this request; null for any receipt that is not all of those.
|
|
164
|
+
*/
|
|
165
|
+
async receipt(token, expect) {
|
|
166
|
+
const claims = await this.verifyJws(token, RECEIPT_TYPE);
|
|
167
|
+
if (claims === null ||
|
|
168
|
+
claims.iss !== this.issuer ||
|
|
169
|
+
claims.aud !== this.options.clientId ||
|
|
170
|
+
claims.sub !== expect.account ||
|
|
171
|
+
claims.req !== expect.request ||
|
|
172
|
+
!isString(claims.hash) ||
|
|
173
|
+
!isString(claims.passkey)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return { passkey: claims.passkey, hash: claims.hash };
|
|
177
|
+
}
|
|
178
|
+
async tokensOf(body, nonce) {
|
|
179
|
+
const { refresh_token: refreshToken, id_token: idToken, expires_in: expiresIn } = body;
|
|
180
|
+
if (!isString(refreshToken) || !isString(idToken) || typeof expiresIn !== "number") {
|
|
181
|
+
throw new Error("gopencsr: the token response lacks a refresh token or an ID token");
|
|
182
|
+
}
|
|
183
|
+
const identity = await this.identityOf(idToken, nonce);
|
|
184
|
+
if (identity === null)
|
|
185
|
+
throw new Error("gopencsr: an ID token that does not verify");
|
|
186
|
+
return { refreshToken, accessExpiresAt: new Date(this.now() + expiresIn * 1000), identity };
|
|
187
|
+
}
|
|
188
|
+
/** The ID token's claims, checked (OpenID Connect Core 3.1.3.7), or null. */
|
|
189
|
+
async identityOf(idToken, nonce) {
|
|
190
|
+
const claims = await this.verifyJws(idToken, "JWT");
|
|
191
|
+
const now = Math.floor(this.now() / 1000);
|
|
192
|
+
if (claims === null ||
|
|
193
|
+
claims.iss !== this.issuer ||
|
|
194
|
+
claims.aud !== this.options.clientId ||
|
|
195
|
+
!isString(claims.sub) ||
|
|
196
|
+
!isString(claims.acr) ||
|
|
197
|
+
typeof claims.exp !== "number" ||
|
|
198
|
+
claims.exp + LEEWAY_S <= now ||
|
|
199
|
+
typeof claims.iat !== "number" ||
|
|
200
|
+
claims.iat - LEEWAY_S > now ||
|
|
201
|
+
typeof claims.auth_time !== "number" ||
|
|
202
|
+
(nonce !== null && claims.nonce !== nonce)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
sub: claims.sub,
|
|
207
|
+
acr: claims.acr,
|
|
208
|
+
authTime: new Date(claims.auth_time * 1000),
|
|
209
|
+
sid: isString(claims.sid) ? claims.sid : null,
|
|
210
|
+
email: isString(claims.email) ? claims.email : null,
|
|
211
|
+
emailVerified: claims.email_verified === true,
|
|
212
|
+
name: isString(claims.name) ? claims.name : null,
|
|
213
|
+
passkeys: typeof claims.passkeys === "number" && Number.isInteger(claims.passkeys)
|
|
214
|
+
? claims.passkeys
|
|
215
|
+
: null,
|
|
216
|
+
terms: termsOf(claims.terms),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The claims of a compact JWS GOpenCSR signed, typed `typ`, or null for any that is not one.
|
|
221
|
+
* Times and audience are the caller's to check.
|
|
222
|
+
*/
|
|
223
|
+
async verifyJws(token, typ) {
|
|
224
|
+
const parts = token.split(".");
|
|
225
|
+
if (parts.length !== 3)
|
|
226
|
+
return null;
|
|
227
|
+
const [head, body, signature] = parts;
|
|
228
|
+
let header;
|
|
229
|
+
let claims;
|
|
230
|
+
try {
|
|
231
|
+
header = JSON.parse(Buffer.from(head, "base64url").toString("utf8"));
|
|
232
|
+
claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
if (header.alg !== "EdDSA" || header.typ !== typ || !isString(header.kid))
|
|
238
|
+
return null;
|
|
239
|
+
if (typeof claims !== "object" || claims === null || Array.isArray(claims))
|
|
240
|
+
return null;
|
|
241
|
+
let key = (await this.publicKeys()).get(header.kid);
|
|
242
|
+
// A key id not seen yet may be a rotation since the last read: ask once more.
|
|
243
|
+
if (key === undefined) {
|
|
244
|
+
this.keys = null;
|
|
245
|
+
key = (await this.publicKeys()).get(header.kid);
|
|
246
|
+
}
|
|
247
|
+
if (key === undefined)
|
|
248
|
+
return null;
|
|
249
|
+
const ok = (0, node_crypto_1.verify)(null, Buffer.from(`${head}.${body}`, "ascii"), key, Buffer.from(signature, "base64url"));
|
|
250
|
+
return ok ? claims : null;
|
|
251
|
+
}
|
|
252
|
+
async publicKeys() {
|
|
253
|
+
if (this.keys !== null && this.now() - this.keys.at < KEYS_FOR_MS)
|
|
254
|
+
return this.keys.byKid;
|
|
255
|
+
const jwks = (await this.call("GET", "oidc/jwks", { anonymous: true }));
|
|
256
|
+
const byKid = new Map();
|
|
257
|
+
for (const jwk of jwks.keys ?? []) {
|
|
258
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !isString(jwk.x) || !isString(jwk.kid)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
byKid.set(jwk.kid, (0, node_crypto_1.createPublicKey)({ key: { kty: "OKP", crv: "Ed25519", x: jwk.x }, format: "jwk" }));
|
|
262
|
+
}
|
|
263
|
+
this.keys = { at: this.now(), byKid };
|
|
264
|
+
return byKid;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Delivers entries to the one log (GOPENCSR-44), at least once: an id delivered again with the
|
|
268
|
+
* same leaf is the same entry, and with another it is refused. Each comes back as the log holds
|
|
269
|
+
* it, its leaf checked to be the one this registry wrote (the canonical JSON of id, kind, time
|
|
270
|
+
* and body), with its index once the appender added it, which is soon but not at once.
|
|
271
|
+
*/
|
|
272
|
+
async deliverEntries(entries) {
|
|
273
|
+
const prefix = `${this.options.product}.`;
|
|
274
|
+
for (const entry of entries) {
|
|
275
|
+
if (!entry.kind.startsWith(prefix)) {
|
|
276
|
+
throw new Error(`gopencsr: ${entry.kind} is not a ${this.options.product} kind`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const answer = (await this.call("POST", "tlog/entries", { json: { entries } }));
|
|
280
|
+
const held = Array.isArray(answer?.entries) ? answer.entries : [];
|
|
281
|
+
if (held.length !== entries.length) {
|
|
282
|
+
throw new Error("gopencsr: the log answered for other entries than those delivered");
|
|
283
|
+
}
|
|
284
|
+
entries.forEach((entry, at) => {
|
|
285
|
+
const leaf = (0, canonical_1.canonicalJson)({
|
|
286
|
+
id: entry.id,
|
|
287
|
+
kind: entry.kind,
|
|
288
|
+
time: entry.time,
|
|
289
|
+
body: entry.body,
|
|
290
|
+
});
|
|
291
|
+
if (held[at]?.id !== entry.id || held[at]?.leaf !== leaf) {
|
|
292
|
+
throw new Error(`gopencsr: the log holds entry ${entry.id} with another leaf`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
return held;
|
|
296
|
+
}
|
|
297
|
+
/** An entry of the one log as GOpenCSR holds it, by its id; null if it holds none. */
|
|
298
|
+
async tlogEntry(id) {
|
|
299
|
+
try {
|
|
300
|
+
return (await this.call("GET", `tlog/entries/${encodeURIComponent(id)}`, {
|
|
301
|
+
anonymous: true,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error instanceof CsrRefused && error.status === 404)
|
|
306
|
+
return null;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async tokenCall(form) {
|
|
311
|
+
return (await this.call("POST", "oidc/token", { form }));
|
|
312
|
+
}
|
|
313
|
+
/** client_secret_basic (RFC 6749 2.3.1): the id and the secret, each form-encoded. */
|
|
314
|
+
async authorization() {
|
|
315
|
+
const form = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
316
|
+
const secret = await this.options.clientSecret();
|
|
317
|
+
const pair = `${form(this.options.clientId)}:${form(secret)}`;
|
|
318
|
+
return `Basic ${Buffer.from(pair, "utf8").toString("base64")}`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* One call to GOpenCSR's api, authenticated as the registry's client unless `anonymous`. A
|
|
322
|
+
* refusal (4xx) throws CsrRefused; anything that kept an answer from coming (the network, a
|
|
323
|
+
* timeout, a 5xx) throws CsrUnavailable, which the caller does not take as a refusal.
|
|
324
|
+
*/
|
|
325
|
+
async call(method, path, request) {
|
|
326
|
+
const base = this.options.endpoints ?? this.issuer;
|
|
327
|
+
const url = new URL(`/api/v1/${path}`, base);
|
|
328
|
+
const headers = { accept: "application/json" };
|
|
329
|
+
if (request.anonymous !== true)
|
|
330
|
+
headers.authorization = await this.authorization();
|
|
331
|
+
let body;
|
|
332
|
+
if (request.form !== undefined) {
|
|
333
|
+
headers["content-type"] = "application/x-www-form-urlencoded";
|
|
334
|
+
body = new URLSearchParams(request.form).toString();
|
|
335
|
+
}
|
|
336
|
+
else if (request.json !== undefined) {
|
|
337
|
+
headers["content-type"] = "application/json";
|
|
338
|
+
body = JSON.stringify(request.json);
|
|
339
|
+
}
|
|
340
|
+
let res;
|
|
341
|
+
try {
|
|
342
|
+
res = await this.fetch(url, {
|
|
343
|
+
method,
|
|
344
|
+
headers,
|
|
345
|
+
...(body === undefined ? {} : { body }),
|
|
346
|
+
redirect: "error",
|
|
347
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
throw new CsrUnavailable(`gopencsr: ${method} ${url.pathname} failed`, { cause: error });
|
|
352
|
+
}
|
|
353
|
+
const text = await res.text();
|
|
354
|
+
if (res.status >= 500) {
|
|
355
|
+
throw new CsrUnavailable(`gopencsr: ${method} ${url.pathname} answered ${res.status}`);
|
|
356
|
+
}
|
|
357
|
+
let parsed = undefined;
|
|
358
|
+
try {
|
|
359
|
+
parsed = text === "" ? undefined : JSON.parse(text);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// Not JSON, perhaps a proxy's page: the status says enough.
|
|
363
|
+
}
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
const reason = parsed?.error ??
|
|
366
|
+
parsed?.code;
|
|
367
|
+
throw new CsrRefused(res.status, isString(reason) ? reason : "unknown");
|
|
368
|
+
}
|
|
369
|
+
return parsed;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
exports.CsrClient = CsrClient;
|
|
373
|
+
//# sourceMappingURL=client.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ghub/gopencsr-client: what a registry needs to be GOpenCSR's client. GOpenCSR signs people in
|
|
3
|
+
* for GOpenCDR and GOpenCNR, and its signing step signs their changes; the code that talks to it
|
|
4
|
+
* is built and released here once, for every registry (GOpenCSR's shared-code rule).
|
|
5
|
+
*/
|
|
6
|
+
export { canonicalJson } from "./canonical";
|
|
7
|
+
export * from "./client";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.canonicalJson = void 0;
|
|
18
|
+
/**
|
|
19
|
+
* @ghub/gopencsr-client: what a registry needs to be GOpenCSR's client. GOpenCSR signs people in
|
|
20
|
+
* for GOpenCDR and GOpenCNR, and its signing step signs their changes; the code that talks to it
|
|
21
|
+
* is built and released here once, for every registry (GOpenCSR's shared-code rule).
|
|
22
|
+
*/
|
|
23
|
+
var canonical_1 = require("./canonical");
|
|
24
|
+
Object.defineProperty(exports, "canonicalJson", { enumerable: true, get: function () { return canonical_1.canonicalJson; } });
|
|
25
|
+
__exportStar(require("./client"), exports);
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghub/gopencsr-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "GOpenCSR's identity client for the registries it signs people in for: the OpenID Connect code flow with PKCE, refresh and revocation, ID tokens and signing receipts checked against GOpenCSR's EdDSA keys, and the signing step's requests and change hash. No framework, no dependencies.",
|
|
5
|
+
"author": "Gelhaus Solutions (Enno Gelhaus) <egelhaus@ennogelhaus.de> (https://ennogelhaus.de)",
|
|
6
|
+
"homepage": "https://ennogelhaus.de",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/**/*.test.*",
|
|
20
|
+
"!dist/**/*.map",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.20.4",
|
|
31
|
+
"typescript": "5.9.3",
|
|
32
|
+
"vitest": "^4.1.11",
|
|
33
|
+
"@gopencsr/config-tsconfig": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"clean": "rm -rf dist .turbo *.tsbuildinfo"
|
|
40
|
+
}
|
|
6
41
|
}
|
package/README.md
DELETED