@majikah/sdk 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 +67 -0
- package/README.md +1112 -0
- package/dist/client/MajikahSDKClient.d.ts +308 -0
- package/dist/client/MajikahSDKClient.js +318 -0
- package/dist/errors/APIError.d.ts +7 -0
- package/dist/errors/APIError.js +14 -0
- package/dist/errors/AuthenticationError.d.ts +3 -0
- package/dist/errors/AuthenticationError.js +4 -0
- package/dist/errors/MajikahError.d.ts +4 -0
- package/dist/errors/MajikahError.js +9 -0
- package/dist/errors/QuotaExhaustedError.d.ts +3 -0
- package/dist/errors/QuotaExhaustedError.js +4 -0
- package/dist/errors/RateLimitError.d.ts +5 -0
- package/dist/errors/RateLimitError.js +10 -0
- package/dist/errors/ServiceUnavailableError.d.ts +3 -0
- package/dist/errors/ServiceUnavailableError.js +4 -0
- package/dist/errors/ValidationError.d.ts +5 -0
- package/dist/errors/ValidationError.js +10 -0
- package/dist/errors/index.d.ts +8 -0
- package/dist/errors/index.js +8 -0
- package/dist/errors/mapError.d.ts +3 -0
- package/dist/errors/mapError.js +22 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +4 -0
- package/dist/services/muid/MUIDClient.d.ts +95 -0
- package/dist/services/muid/MUIDClient.js +138 -0
- package/dist/services/muid/key-resolver.d.ts +24 -0
- package/dist/services/muid/key-resolver.js +32 -0
- package/dist/services/notary/NotaryClient.d.ts +188 -0
- package/dist/services/notary/NotaryClient.js +291 -0
- package/dist/services/notary/validation.d.ts +7 -0
- package/dist/services/notary/validation.js +19 -0
- package/dist/services/shared/encoding.d.ts +21 -0
- package/dist/services/shared/encoding.js +43 -0
- package/dist/services/shared/resolve-signature.d.ts +23 -0
- package/dist/services/shared/resolve-signature.js +31 -0
- package/dist/services/shared/sleep.d.ts +1 -0
- package/dist/services/shared/sleep.js +3 -0
- package/dist/services/shared/validation.d.ts +9 -0
- package/dist/services/shared/validation.js +16 -0
- package/dist/services/slink/SLinkClient.d.ts +174 -0
- package/dist/services/slink/SLinkClient.js +231 -0
- package/dist/services/slink/validation.d.ts +24 -0
- package/dist/services/slink/validation.js +31 -0
- package/dist/services/tsa/TSAClient.d.ts +101 -0
- package/dist/services/tsa/TSAClient.js +178 -0
- package/dist/services/tsa/validation.d.ts +2 -0
- package/dist/services/tsa/validation.js +12 -0
- package/dist/transport/HttpClient.d.ts +85 -0
- package/dist/transport/HttpClient.js +135 -0
- package/dist/transport/RouteResolver.d.ts +54 -0
- package/dist/transport/RouteResolver.js +67 -0
- package/dist/transport/retry-after.d.ts +17 -0
- package/dist/transport/retry-after.js +39 -0
- package/dist/transport/retry.d.ts +8 -0
- package/dist/transport/retry.js +61 -0
- package/dist/types/common.d.ts +133 -0
- package/dist/types/common.js +42 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +1 -0
- package/dist/types/muid.d.ts +80 -0
- package/dist/types/muid.js +1 -0
- package/dist/types/notary.d.ts +243 -0
- package/dist/types/notary.js +1 -0
- package/dist/types/slink.d.ts +60 -0
- package/dist/types/slink.js +1 -0
- package/dist/types/tsa.d.ts +144 -0
- package/dist/types/tsa.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { MajikSLink } from "@majikah/majik-slink";
|
|
2
|
+
import { ValidationError } from "../../errors/ValidationError";
|
|
3
|
+
import { normalizeUrl } from "./validation";
|
|
4
|
+
import { assertNonEmpty } from "../shared/validation";
|
|
5
|
+
/**
|
|
6
|
+
* Client for creating, registering, searching, and verifying Majik SLinks.
|
|
7
|
+
*
|
|
8
|
+
* SLink verification has two distinct stages:
|
|
9
|
+
* - API lookup confirms that a claim exists.
|
|
10
|
+
* - Local cryptographic verification confirms that the claim's signature
|
|
11
|
+
* is valid using trusted public keys.
|
|
12
|
+
*/
|
|
13
|
+
export class SLinkClient {
|
|
14
|
+
http;
|
|
15
|
+
/**
|
|
16
|
+
* Creates an SLink client using the provided HTTP transport.
|
|
17
|
+
*
|
|
18
|
+
* @param http HTTP client used to communicate with the Majikah API.
|
|
19
|
+
*/
|
|
20
|
+
constructor(http) {
|
|
21
|
+
this.http = http;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Registers an SLink that has already been created and signed.
|
|
25
|
+
*
|
|
26
|
+
* Accepts either a `MajikSLink` instance or its serialized JSON form.
|
|
27
|
+
*
|
|
28
|
+
* @param slink SLink instance or serialized SLink data.
|
|
29
|
+
* @returns The registered SLink.
|
|
30
|
+
* @throws ValidationError When no SLink is provided.
|
|
31
|
+
*/
|
|
32
|
+
async create(slink) {
|
|
33
|
+
if (!slink) {
|
|
34
|
+
throw new ValidationError("slink is required", slink);
|
|
35
|
+
}
|
|
36
|
+
const json = slink instanceof MajikSLink ? slink.toJSON() : slink;
|
|
37
|
+
return this.http.request("slink", "", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: json,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates, signs, and registers an SLink for a URL in one operation.
|
|
44
|
+
*
|
|
45
|
+
* This is the convenience method for callers that do not already have a
|
|
46
|
+
* `MajikSLink` instance. It requires an unlocked `MajikKey` to sign the
|
|
47
|
+
* claim.
|
|
48
|
+
*
|
|
49
|
+
* @param rawUrl URL or domain to associate with the SLink.
|
|
50
|
+
* @param key Unlocked MajikKey used to sign the SLink.
|
|
51
|
+
* @param userId User identifier recorded as the signer.
|
|
52
|
+
* @param muid MUID associated with the claim.
|
|
53
|
+
* @param options Optional SLink metadata.
|
|
54
|
+
* @returns The registered SLink.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const stored = await majikah.slink.registerUrl(
|
|
59
|
+
* "https://youtube.com/watch?v=dQw4w9WgXcQ",
|
|
60
|
+
* aliceKey,
|
|
61
|
+
* userId,
|
|
62
|
+
* muid,
|
|
63
|
+
* );
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
async registerUrl(rawUrl, key, userId, muid, options) {
|
|
67
|
+
const slink = await MajikSLink.create(rawUrl, key, userId, muid, options);
|
|
68
|
+
return this.create(slink);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Lists SLinks owned by the current API credentials.
|
|
72
|
+
*
|
|
73
|
+
* Results use cursor-based pagination. Pass the returned `next_cursor`
|
|
74
|
+
* directly into the next request's `cursor` parameter.
|
|
75
|
+
*
|
|
76
|
+
* @param params Optional pagination settings.
|
|
77
|
+
* @returns A page of SLinks and pagination metadata.
|
|
78
|
+
*/
|
|
79
|
+
async me(params) {
|
|
80
|
+
return this.http.request("slink", "/me", {
|
|
81
|
+
method: "GET",
|
|
82
|
+
query: {
|
|
83
|
+
cursor: params?.cursor,
|
|
84
|
+
limit: params?.limit !== undefined ? String(params.limit) : undefined,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Searches for SLink claims associated with a content hash.
|
|
90
|
+
*
|
|
91
|
+
* This confirms which SLink claims exist for the supplied hash but does not
|
|
92
|
+
* perform cryptographic signature verification.
|
|
93
|
+
*
|
|
94
|
+
* @param hash Content hash associated with the SLink claims.
|
|
95
|
+
* @returns Matching SLinks and their public MUID information.
|
|
96
|
+
* @throws ValidationError When the hash is empty.
|
|
97
|
+
*/
|
|
98
|
+
async verifyByHash(hash) {
|
|
99
|
+
assertNonEmpty(hash, "hash");
|
|
100
|
+
return this.http.request("slink", "/verify", {
|
|
101
|
+
method: "GET",
|
|
102
|
+
query: { hash },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Searches for SLink claims associated with a URL.
|
|
107
|
+
*
|
|
108
|
+
* Bare domains are automatically normalized to use `https://`.
|
|
109
|
+
* This method confirms that matching claims exist but does not verify their
|
|
110
|
+
* signatures cryptographically.
|
|
111
|
+
*
|
|
112
|
+
* @param url URL or domain associated with the SLink claim.
|
|
113
|
+
* @returns Matching SLinks and their public MUID information.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const results = await majikah.slink.verifyUrl("thezelijah.world");
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
async verifyUrl(url) {
|
|
121
|
+
const normalized = normalizeUrl(url);
|
|
122
|
+
return this.http.request("slink", "/verify", {
|
|
123
|
+
method: "GET",
|
|
124
|
+
query: { url: normalized },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Finds SLink claims for a URL and cryptographically verifies every
|
|
129
|
+
* matching signature locally.
|
|
130
|
+
*
|
|
131
|
+
* The API lookup only establishes that a claim exists for the URL. It does
|
|
132
|
+
* not prove that a signature is valid because SLink public keys are not
|
|
133
|
+
* stored server-side. This method resolves the required public keys through
|
|
134
|
+
* `resolvePublicKeys` and verifies each signature locally.
|
|
135
|
+
*
|
|
136
|
+
* For the common MUID-backed flow, use `createMuidPublicKeyResolver()`.
|
|
137
|
+
* It resolves each signer's public keys through `MUIDClient.lookup()` and
|
|
138
|
+
* converts the API's Base64-encoded keys into the format required for
|
|
139
|
+
* cryptographic verification.
|
|
140
|
+
*
|
|
141
|
+
* @param url URL or domain associated with the SLink claims.
|
|
142
|
+
* @param resolvePublicKeys Callback that resolves trusted public keys for
|
|
143
|
+
* each SLink signer.
|
|
144
|
+
* @returns Each matching SLink paired with its local verification result.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { createMuidPublicKeyResolver } from "@majikah/sdk";
|
|
149
|
+
*
|
|
150
|
+
* const results = await majikah.slink.verifyUrlWithProof(
|
|
151
|
+
* "thezelijah.world",
|
|
152
|
+
* createMuidPublicKeyResolver(majikah.muid),
|
|
153
|
+
* );
|
|
154
|
+
*
|
|
155
|
+
* const verified = results.filter(({ result }) => result.valid);
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* A custom resolver can be used when public keys come from another trusted
|
|
159
|
+
* registry or key source:
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const results = await majikah.slink.verifyUrlWithProof(
|
|
164
|
+
* "thezelijah.world",
|
|
165
|
+
* async (muid, signerId) => {
|
|
166
|
+
* const keys = await myKeyRegistry.get(muid);
|
|
167
|
+
*
|
|
168
|
+
* return {
|
|
169
|
+
* signerId,
|
|
170
|
+
* edPublicKey: keys.edPublicKey,
|
|
171
|
+
* mlDsaPublicKey: keys.mlDsaPublicKey,
|
|
172
|
+
* };
|
|
173
|
+
* },
|
|
174
|
+
* );
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
async verifyUrlWithProof(url, resolvePublicKeys) {
|
|
178
|
+
const { matches } = await this.verifyUrl(url);
|
|
179
|
+
return this.verifyMatches(matches, resolvePublicKeys);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Cryptographically verifies already-fetched SLink search results locally.
|
|
183
|
+
*
|
|
184
|
+
* Use this when the matches were obtained separately through `verifyUrl()`
|
|
185
|
+
* or `verifyByHash()` and you want to verify them without another API lookup.
|
|
186
|
+
*
|
|
187
|
+
* @param matches SLink matches to verify.
|
|
188
|
+
* @param resolvePublicKeys Callback used to obtain trusted public signing keys.
|
|
189
|
+
* @returns Each match paired with its local cryptographic verification result.
|
|
190
|
+
*/
|
|
191
|
+
async verifyMatches(matches, resolvePublicKeys) {
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const match of matches) {
|
|
194
|
+
const publicKeys = await resolvePublicKeys(match.slink.muid, match.slink.signature.signerId);
|
|
195
|
+
const result = MajikSLink.verifySignature(match.slink, publicKeys);
|
|
196
|
+
results.push({
|
|
197
|
+
match,
|
|
198
|
+
result,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Retrieves an SLink by its identifier.
|
|
205
|
+
*
|
|
206
|
+
* @param id SLink identifier.
|
|
207
|
+
* @returns The SLink together with its owner's public MUID information.
|
|
208
|
+
* @throws ValidationError When the identifier is empty.
|
|
209
|
+
*/
|
|
210
|
+
async lookup(id) {
|
|
211
|
+
assertNonEmpty(id, "id");
|
|
212
|
+
return this.http.request("slink", `/${encodeURIComponent(id)}`, {
|
|
213
|
+
method: "GET",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Deletes an SLink by its identifier.
|
|
218
|
+
*
|
|
219
|
+
* @param id SLink identifier.
|
|
220
|
+
* @returns The identifier of the deleted SLink.
|
|
221
|
+
* @throws ValidationError When the identifier is empty.
|
|
222
|
+
*/
|
|
223
|
+
async delete(id) {
|
|
224
|
+
assertNonEmpty(id, "id");
|
|
225
|
+
return this.http.request("slink", `/${encodeURIComponent(id)}`, {
|
|
226
|
+
method: "DELETE",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
Object.freeze(SLinkClient);
|
|
231
|
+
Object.freeze(SLinkClient.prototype);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a URL by adding `https://` when no HTTP(S) scheme is present.
|
|
3
|
+
*
|
|
4
|
+
* The returned value is intentionally not percent-encoded. Query parameter
|
|
5
|
+
* encoding is handled by the HTTP client when the request URL is built,
|
|
6
|
+
* preventing double encoding.
|
|
7
|
+
*
|
|
8
|
+
* @param input URL or domain to normalize.
|
|
9
|
+
* @returns A normalized HTTP(S) URL.
|
|
10
|
+
* @throws ValidationError When the input is empty or not a string.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* normalizeUrl("thezelijah.world");
|
|
15
|
+
* // "https://thezelijah.world"
|
|
16
|
+
*
|
|
17
|
+
* normalizeUrl("http://thezelijah.world");
|
|
18
|
+
* // "http://thezelijah.world"
|
|
19
|
+
*
|
|
20
|
+
* normalizeUrl("https://thezelijah.world");
|
|
21
|
+
* // "https://thezelijah.world"
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeUrl(input: string): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ValidationError } from "../../errors/ValidationError";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a URL by adding `https://` when no HTTP(S) scheme is present.
|
|
4
|
+
*
|
|
5
|
+
* The returned value is intentionally not percent-encoded. Query parameter
|
|
6
|
+
* encoding is handled by the HTTP client when the request URL is built,
|
|
7
|
+
* preventing double encoding.
|
|
8
|
+
*
|
|
9
|
+
* @param input URL or domain to normalize.
|
|
10
|
+
* @returns A normalized HTTP(S) URL.
|
|
11
|
+
* @throws ValidationError When the input is empty or not a string.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* normalizeUrl("thezelijah.world");
|
|
16
|
+
* // "https://thezelijah.world"
|
|
17
|
+
*
|
|
18
|
+
* normalizeUrl("http://thezelijah.world");
|
|
19
|
+
* // "http://thezelijah.world"
|
|
20
|
+
*
|
|
21
|
+
* normalizeUrl("https://thezelijah.world");
|
|
22
|
+
* // "https://thezelijah.world"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeUrl(input) {
|
|
26
|
+
if (typeof input !== "string" || input.trim().length === 0) {
|
|
27
|
+
throw new ValidationError("url must be a non-empty string", input);
|
|
28
|
+
}
|
|
29
|
+
const trimmed = input.trim();
|
|
30
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { MajikKey } from "@majikah/majik-key";
|
|
2
|
+
import { MajikSignature, type EnvelopeInput, type FileLike, type MajikSignatureJSON } from "@majikah/majik-signature";
|
|
3
|
+
import type { HttpClient } from "../../transport/HttpClient";
|
|
4
|
+
import type { MajikTimestamp, MajikTSARequest, TSAQuota, StampDetachedResult, StampFileDetachedOptions, StampFileOptions, StampResult, TimestampExistingDetachedResult, TimestampExistingOptions, TimestampExistingResult } from "../../types/tsa";
|
|
5
|
+
/**
|
|
6
|
+
* Client for interacting with the Majikah Time Stamping Authority.
|
|
7
|
+
*
|
|
8
|
+
* Provides low-level TSA requests as well as higher-level helpers for:
|
|
9
|
+
* - requesting timestamps for existing signatures
|
|
10
|
+
* - timestamping already-signed files and envelopes
|
|
11
|
+
* - signing and timestamping files in one operation
|
|
12
|
+
*/
|
|
13
|
+
export declare class TSAClient {
|
|
14
|
+
private readonly http;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a TSA client using the provided HTTP transport.
|
|
17
|
+
*
|
|
18
|
+
* @param http HTTP client used to communicate with the Majikah API.
|
|
19
|
+
*/
|
|
20
|
+
constructor(http: HttpClient);
|
|
21
|
+
/**
|
|
22
|
+
* Issues a trusted timestamp request for a TSA payload.
|
|
23
|
+
*
|
|
24
|
+
* @param request TSA request containing the signature hash and related data.
|
|
25
|
+
* @returns The trusted timestamp returned by the TSA.
|
|
26
|
+
*/
|
|
27
|
+
issue(request: MajikTSARequest): Promise<MajikTimestamp>;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the current TSA credit quota for the authenticated caller.
|
|
30
|
+
*
|
|
31
|
+
* @returns The number of available TSA credits and related quota data.
|
|
32
|
+
*/
|
|
33
|
+
quota(): Promise<TSAQuota>;
|
|
34
|
+
/**
|
|
35
|
+
* Requests a trusted timestamp using an existing signature.
|
|
36
|
+
*
|
|
37
|
+
* @param signature Signature instance or serialized signature JSON.
|
|
38
|
+
* @returns The trusted timestamp issued for the signature.
|
|
39
|
+
*/
|
|
40
|
+
issueForSignature(signature: MajikSignature | MajikSignatureJSON): Promise<MajikTimestamp>;
|
|
41
|
+
/**
|
|
42
|
+
* Attaches a trusted timestamp to an already-signed embedded file.
|
|
43
|
+
*
|
|
44
|
+
* The file must already contain at least one signature. No signing key is
|
|
45
|
+
* required because this method only timestamps an existing signature.
|
|
46
|
+
*
|
|
47
|
+
* @param file Signed file containing one or more signatures.
|
|
48
|
+
* @param options Optional signer and file-processing settings.
|
|
49
|
+
* @returns The file and signature after the TSA has been attached.
|
|
50
|
+
*/
|
|
51
|
+
timestampFile(file: FileLike, options?: TimestampExistingOptions): Promise<TimestampExistingResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Attaches a trusted timestamp to a signature in a detached envelope.
|
|
54
|
+
*
|
|
55
|
+
* The envelope must already contain at least one signature. When multiple
|
|
56
|
+
* signatures are present, `expectedSignerId` identifies which signature
|
|
57
|
+
* receives the TSA.
|
|
58
|
+
*
|
|
59
|
+
* @param envelope Existing detached signature envelope.
|
|
60
|
+
* @param options Optional signer selection and file-processing settings.
|
|
61
|
+
* @returns The updated envelope, signature, and `.mjksig` representation.
|
|
62
|
+
*/
|
|
63
|
+
timestampDetached(envelope: EnvelopeInput, options?: TimestampExistingOptions): Promise<TimestampExistingDetachedResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Signs a file and attaches a trusted timestamp in one operation.
|
|
66
|
+
*
|
|
67
|
+
* This creates a new signature using the provided MajikKey, requests a TSA
|
|
68
|
+
* timestamp for that signature, and embeds the timestamped signature into
|
|
69
|
+
* the resulting file.
|
|
70
|
+
*
|
|
71
|
+
* @param file File to sign and timestamp.
|
|
72
|
+
* @param key Unlocked MajikKey used to create the signature.
|
|
73
|
+
* @param options Optional signing and file-processing settings.
|
|
74
|
+
* @returns The timestamped file and its signature metadata.
|
|
75
|
+
*/
|
|
76
|
+
stampFile(file: FileLike, key: MajikKey, options?: StampFileOptions): Promise<StampResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Signs a file and returns its trusted timestamp as a detached envelope.
|
|
79
|
+
*
|
|
80
|
+
* Unlike {@link stampFile}, the timestamped signature is not embedded back
|
|
81
|
+
* into the file. The returned envelope can be distributed separately for
|
|
82
|
+
* out-of-band verification.
|
|
83
|
+
*
|
|
84
|
+
* @param file File to sign and timestamp.
|
|
85
|
+
* @param key Unlocked MajikKey used to create the signature.
|
|
86
|
+
* @param options Optional signing and envelope settings.
|
|
87
|
+
* @returns The signed file, updated envelope, signature, and `.mjksig` blob.
|
|
88
|
+
*/
|
|
89
|
+
stampFileDetached(file: FileLike, key: MajikKey, options?: StampFileDetachedOptions): Promise<StampDetachedResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Prevents a TSA request from being issued for a signature that is already
|
|
92
|
+
* timestamped.
|
|
93
|
+
*
|
|
94
|
+
* This check avoids consuming a TSA credit for an operation that would fail
|
|
95
|
+
* when the timestamp is attached.
|
|
96
|
+
*
|
|
97
|
+
* @param signature Signature that will receive the TSA.
|
|
98
|
+
* @throws ValidationError When the signature already contains a TSA.
|
|
99
|
+
*/
|
|
100
|
+
private assertNoExistingTSA;
|
|
101
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { MajikSignature, MajikSignatureEnvelope, } from "@majikah/majik-signature";
|
|
2
|
+
import { ValidationError } from "../../errors/ValidationError";
|
|
3
|
+
import { validateTSARequest } from "./validation";
|
|
4
|
+
import { resolveTargetSignature } from "../shared/resolve-signature";
|
|
5
|
+
/**
|
|
6
|
+
* Client for interacting with the Majikah Time Stamping Authority.
|
|
7
|
+
*
|
|
8
|
+
* Provides low-level TSA requests as well as higher-level helpers for:
|
|
9
|
+
* - requesting timestamps for existing signatures
|
|
10
|
+
* - timestamping already-signed files and envelopes
|
|
11
|
+
* - signing and timestamping files in one operation
|
|
12
|
+
*/
|
|
13
|
+
export class TSAClient {
|
|
14
|
+
http;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a TSA client using the provided HTTP transport.
|
|
17
|
+
*
|
|
18
|
+
* @param http HTTP client used to communicate with the Majikah API.
|
|
19
|
+
*/
|
|
20
|
+
constructor(http) {
|
|
21
|
+
this.http = http;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Issues a trusted timestamp request for a TSA payload.
|
|
25
|
+
*
|
|
26
|
+
* @param request TSA request containing the signature hash and related data.
|
|
27
|
+
* @returns The trusted timestamp returned by the TSA.
|
|
28
|
+
*/
|
|
29
|
+
async issue(request) {
|
|
30
|
+
validateTSARequest(request);
|
|
31
|
+
return this.http.request("tsa", "/timestamp", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: request,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the current TSA credit quota for the authenticated caller.
|
|
38
|
+
*
|
|
39
|
+
* @returns The number of available TSA credits and related quota data.
|
|
40
|
+
*/
|
|
41
|
+
async quota() {
|
|
42
|
+
const res = await this.http.request("tsa", "/quota", { method: "GET" });
|
|
43
|
+
return res.tsa_credits;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Requests a trusted timestamp using an existing signature.
|
|
47
|
+
*
|
|
48
|
+
* @param signature Signature instance or serialized signature JSON.
|
|
49
|
+
* @returns The trusted timestamp issued for the signature.
|
|
50
|
+
*/
|
|
51
|
+
async issueForSignature(signature) {
|
|
52
|
+
const sig = signature instanceof MajikSignature
|
|
53
|
+
? signature
|
|
54
|
+
: MajikSignature.fromJSON(signature);
|
|
55
|
+
return this.issue(sig.buildTSARequestPayload());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Attaches a trusted timestamp to an already-signed embedded file.
|
|
59
|
+
*
|
|
60
|
+
* The file must already contain at least one signature. No signing key is
|
|
61
|
+
* required because this method only timestamps an existing signature.
|
|
62
|
+
*
|
|
63
|
+
* @param file Signed file containing one or more signatures.
|
|
64
|
+
* @param options Optional signer and file-processing settings.
|
|
65
|
+
* @returns The file and signature after the TSA has been attached.
|
|
66
|
+
*/
|
|
67
|
+
async timestampFile(file, options) {
|
|
68
|
+
const signatures = await MajikSignature.extractFrom(file, {
|
|
69
|
+
mimeType: options?.mimeType,
|
|
70
|
+
});
|
|
71
|
+
const target = resolveTargetSignature(signatures, options?.expectedSignerId, {
|
|
72
|
+
noSignatureHint: "Sign the file first, or use stampFile()/stampFileDetached() to sign and timestamp in one call.",
|
|
73
|
+
});
|
|
74
|
+
this.assertNoExistingTSA(target);
|
|
75
|
+
const timestamp = await this.issueForSignature(target);
|
|
76
|
+
target.addTSA(timestamp);
|
|
77
|
+
const blob = await target.embedIn(file, {
|
|
78
|
+
mimeType: options?.mimeType,
|
|
79
|
+
});
|
|
80
|
+
return { blob, signature: target };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Attaches a trusted timestamp to a signature in a detached envelope.
|
|
84
|
+
*
|
|
85
|
+
* The envelope must already contain at least one signature. When multiple
|
|
86
|
+
* signatures are present, `expectedSignerId` identifies which signature
|
|
87
|
+
* receives the TSA.
|
|
88
|
+
*
|
|
89
|
+
* @param envelope Existing detached signature envelope.
|
|
90
|
+
* @param options Optional signer selection and file-processing settings.
|
|
91
|
+
* @returns The updated envelope, signature, and `.mjksig` representation.
|
|
92
|
+
*/
|
|
93
|
+
async timestampDetached(envelope, options) {
|
|
94
|
+
const env = await MajikSignatureEnvelope.from(envelope);
|
|
95
|
+
const signatures = env.signatures.map((s) => MajikSignature.fromJSON(s));
|
|
96
|
+
const target = resolveTargetSignature(signatures, options?.expectedSignerId, {
|
|
97
|
+
noSignatureHint: "Sign the file first, or use stampFile()/stampFileDetached() to sign and timestamp in one call.",
|
|
98
|
+
});
|
|
99
|
+
this.assertNoExistingTSA(target);
|
|
100
|
+
const timestamp = await this.issueForSignature(target);
|
|
101
|
+
target.addTSA(timestamp);
|
|
102
|
+
const nextEnvelope = env.withSignature(target.toJSON());
|
|
103
|
+
return {
|
|
104
|
+
envelope: nextEnvelope,
|
|
105
|
+
signature: target,
|
|
106
|
+
mjksig: nextEnvelope.toMJKSIG(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Signs a file and attaches a trusted timestamp in one operation.
|
|
111
|
+
*
|
|
112
|
+
* This creates a new signature using the provided MajikKey, requests a TSA
|
|
113
|
+
* timestamp for that signature, and embeds the timestamped signature into
|
|
114
|
+
* the resulting file.
|
|
115
|
+
*
|
|
116
|
+
* @param file File to sign and timestamp.
|
|
117
|
+
* @param key Unlocked MajikKey used to create the signature.
|
|
118
|
+
* @param options Optional signing and file-processing settings.
|
|
119
|
+
* @returns The timestamped file and its signature metadata.
|
|
120
|
+
*/
|
|
121
|
+
async stampFile(file, key, options) {
|
|
122
|
+
const { blob, signature, handler, mimeType } = await MajikSignature.signFile(file, key, options);
|
|
123
|
+
const timestamp = await this.issueForSignature(signature);
|
|
124
|
+
signature.addTSA(timestamp);
|
|
125
|
+
const finalBlob = await signature.embedIn(blob, { mimeType });
|
|
126
|
+
return {
|
|
127
|
+
blob: finalBlob,
|
|
128
|
+
signature,
|
|
129
|
+
handler,
|
|
130
|
+
mimeType,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Signs a file and returns its trusted timestamp as a detached envelope.
|
|
135
|
+
*
|
|
136
|
+
* Unlike {@link stampFile}, the timestamped signature is not embedded back
|
|
137
|
+
* into the file. The returned envelope can be distributed separately for
|
|
138
|
+
* out-of-band verification.
|
|
139
|
+
*
|
|
140
|
+
* @param file File to sign and timestamp.
|
|
141
|
+
* @param key Unlocked MajikKey used to create the signature.
|
|
142
|
+
* @param options Optional signing and envelope settings.
|
|
143
|
+
* @returns The signed file, updated envelope, signature, and `.mjksig` blob.
|
|
144
|
+
*/
|
|
145
|
+
async stampFileDetached(file, key, options) {
|
|
146
|
+
const { blob, envelope, signature, handler, mimeType } = await MajikSignature.signFileDetached(file, key, options);
|
|
147
|
+
const timestamp = await this.issueForSignature(signature);
|
|
148
|
+
signature.addTSA(timestamp);
|
|
149
|
+
const nextEnvelope = envelope.withSignature(signature.toJSON());
|
|
150
|
+
return {
|
|
151
|
+
blob,
|
|
152
|
+
envelope: nextEnvelope,
|
|
153
|
+
signature,
|
|
154
|
+
mjksig: nextEnvelope.toMJKSIG(),
|
|
155
|
+
handler,
|
|
156
|
+
mimeType,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Prevents a TSA request from being issued for a signature that is already
|
|
161
|
+
* timestamped.
|
|
162
|
+
*
|
|
163
|
+
* This check avoids consuming a TSA credit for an operation that would fail
|
|
164
|
+
* when the timestamp is attached.
|
|
165
|
+
*
|
|
166
|
+
* @param signature Signature that will receive the TSA.
|
|
167
|
+
* @throws ValidationError When the signature already contains a TSA.
|
|
168
|
+
*/
|
|
169
|
+
assertNoExistingTSA(signature) {
|
|
170
|
+
if (signature.hasTSA) {
|
|
171
|
+
throw new ValidationError(`Signature for signerId "${signature.signerId}" already has a TSA attached — a TSA cannot be replaced once set.`, signature.signerId);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Freeze static methods
|
|
176
|
+
Object.freeze(TSAClient);
|
|
177
|
+
// Freeze instance methods
|
|
178
|
+
Object.freeze(TSAClient.prototype);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// services/tsa/validation.ts
|
|
2
|
+
import { ValidationError } from "../../errors/ValidationError";
|
|
3
|
+
import { CONTENT_HASH_B64_LEN } from "@majikah/majik-signature";
|
|
4
|
+
export function validateTSARequest(req) {
|
|
5
|
+
if (req?.digest?.algorithm !== "SHA-256") {
|
|
6
|
+
throw new ValidationError('digest.algorithm must be "SHA-256"', req);
|
|
7
|
+
}
|
|
8
|
+
if (typeof req.digest.value !== "string" ||
|
|
9
|
+
req.digest.value.length !== CONTENT_HASH_B64_LEN) {
|
|
10
|
+
throw new ValidationError("digest.value must be a valid SHA-256 hex digest", req);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type MajikahClientOptions, type ServiceGroup } from "../types/common";
|
|
2
|
+
/**
|
|
3
|
+
* Options for an individual HTTP request made through the API client.
|
|
4
|
+
*/
|
|
5
|
+
interface RequestOptions {
|
|
6
|
+
/** HTTP method used for the request. */
|
|
7
|
+
method: "GET" | "POST" | "DELETE";
|
|
8
|
+
/** Query parameters appended to the request URL. */
|
|
9
|
+
query?: Record<string, string | undefined>;
|
|
10
|
+
/** Request body that will be serialized as JSON. */
|
|
11
|
+
body?: unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Marks a POST request as safe to retry.
|
|
14
|
+
*
|
|
15
|
+
* Use this only when the operation is idempotent and repeating it cannot
|
|
16
|
+
* create an unintended side effect.
|
|
17
|
+
*/
|
|
18
|
+
idempotent?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Internal HTTP transport used by the Majikah SDK service clients.
|
|
22
|
+
*
|
|
23
|
+
* Handles API URL resolution, authentication headers, request timeouts,
|
|
24
|
+
* response parsing, error mapping, and retry behavior for retry-safe requests.
|
|
25
|
+
*/
|
|
26
|
+
export declare class HttpClient {
|
|
27
|
+
private readonly routes;
|
|
28
|
+
private readonly apiKey;
|
|
29
|
+
private readonly timeoutMs;
|
|
30
|
+
private readonly retry;
|
|
31
|
+
private readonly fetchImpl;
|
|
32
|
+
private readonly extraHeaders;
|
|
33
|
+
/**
|
|
34
|
+
* Creates an HTTP client from the SDK configuration.
|
|
35
|
+
*
|
|
36
|
+
* @param options API client configuration including authentication,
|
|
37
|
+
* timeout, retry, and transport settings.
|
|
38
|
+
* @throws MajikahError When no API key is provided.
|
|
39
|
+
*/
|
|
40
|
+
constructor(options: MajikahClientOptions);
|
|
41
|
+
/**
|
|
42
|
+
* Sends a request to a Majikah API service.
|
|
43
|
+
*
|
|
44
|
+
* GET requests are automatically retried according to the configured retry
|
|
45
|
+
* policy. POST requests are only retried when explicitly marked as
|
|
46
|
+
* idempotent.
|
|
47
|
+
*
|
|
48
|
+
* Successful API responses are unwrapped and return their `data` value
|
|
49
|
+
* directly. API errors are converted into SDK-specific error types.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam T Expected type of the unwrapped response data.
|
|
52
|
+
* @param group Service group receiving the request.
|
|
53
|
+
* @param path API path relative to the service route.
|
|
54
|
+
* @param opts HTTP method, query parameters, body, and retry settings.
|
|
55
|
+
* @returns The response data returned by the API.
|
|
56
|
+
* @throws MajikahError When the request fails or times out.
|
|
57
|
+
* @throws RateLimitError When the API rejects the request due to rate limits.
|
|
58
|
+
*/
|
|
59
|
+
request<T>(group: ServiceGroup, path: string, opts: RequestOptions): Promise<T>;
|
|
60
|
+
/**
|
|
61
|
+
* Builds a fully resolved API URL from a service route and query parameters.
|
|
62
|
+
*
|
|
63
|
+
* Undefined query values are omitted from the final URL.
|
|
64
|
+
*
|
|
65
|
+
* @param group Service group used to resolve the API route.
|
|
66
|
+
* @param path API path relative to the resolved service route.
|
|
67
|
+
* @param query Optional query parameters.
|
|
68
|
+
* @returns Fully resolved request URL.
|
|
69
|
+
*/
|
|
70
|
+
private buildUrl;
|
|
71
|
+
/**
|
|
72
|
+
* Executes a single HTTP request without retrying.
|
|
73
|
+
*
|
|
74
|
+
* The response envelope is validated and unwrapped before returning the
|
|
75
|
+
* contained data. HTTP and API errors are converted through `mapError()`.
|
|
76
|
+
*
|
|
77
|
+
* @typeParam T Expected type of the unwrapped response data.
|
|
78
|
+
* @param url Fully resolved request URL.
|
|
79
|
+
* @param opts Request configuration.
|
|
80
|
+
* @returns The response data returned by the API.
|
|
81
|
+
* @throws MajikahError When the request times out or the API returns an error.
|
|
82
|
+
*/
|
|
83
|
+
private attempt;
|
|
84
|
+
}
|
|
85
|
+
export {};
|