@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.
Files changed (71) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +1112 -0
  3. package/dist/client/MajikahSDKClient.d.ts +308 -0
  4. package/dist/client/MajikahSDKClient.js +318 -0
  5. package/dist/errors/APIError.d.ts +7 -0
  6. package/dist/errors/APIError.js +14 -0
  7. package/dist/errors/AuthenticationError.d.ts +3 -0
  8. package/dist/errors/AuthenticationError.js +4 -0
  9. package/dist/errors/MajikahError.d.ts +4 -0
  10. package/dist/errors/MajikahError.js +9 -0
  11. package/dist/errors/QuotaExhaustedError.d.ts +3 -0
  12. package/dist/errors/QuotaExhaustedError.js +4 -0
  13. package/dist/errors/RateLimitError.d.ts +5 -0
  14. package/dist/errors/RateLimitError.js +10 -0
  15. package/dist/errors/ServiceUnavailableError.d.ts +3 -0
  16. package/dist/errors/ServiceUnavailableError.js +4 -0
  17. package/dist/errors/ValidationError.d.ts +5 -0
  18. package/dist/errors/ValidationError.js +10 -0
  19. package/dist/errors/index.d.ts +8 -0
  20. package/dist/errors/index.js +8 -0
  21. package/dist/errors/mapError.d.ts +3 -0
  22. package/dist/errors/mapError.js +22 -0
  23. package/dist/index.d.ts +10 -0
  24. package/dist/index.js +10 -0
  25. package/dist/services/index.d.ts +4 -0
  26. package/dist/services/index.js +4 -0
  27. package/dist/services/muid/MUIDClient.d.ts +95 -0
  28. package/dist/services/muid/MUIDClient.js +138 -0
  29. package/dist/services/muid/key-resolver.d.ts +24 -0
  30. package/dist/services/muid/key-resolver.js +32 -0
  31. package/dist/services/notary/NotaryClient.d.ts +188 -0
  32. package/dist/services/notary/NotaryClient.js +291 -0
  33. package/dist/services/notary/validation.d.ts +7 -0
  34. package/dist/services/notary/validation.js +19 -0
  35. package/dist/services/shared/encoding.d.ts +21 -0
  36. package/dist/services/shared/encoding.js +43 -0
  37. package/dist/services/shared/resolve-signature.d.ts +23 -0
  38. package/dist/services/shared/resolve-signature.js +31 -0
  39. package/dist/services/shared/sleep.d.ts +1 -0
  40. package/dist/services/shared/sleep.js +3 -0
  41. package/dist/services/shared/validation.d.ts +9 -0
  42. package/dist/services/shared/validation.js +16 -0
  43. package/dist/services/slink/SLinkClient.d.ts +174 -0
  44. package/dist/services/slink/SLinkClient.js +231 -0
  45. package/dist/services/slink/validation.d.ts +24 -0
  46. package/dist/services/slink/validation.js +31 -0
  47. package/dist/services/tsa/TSAClient.d.ts +101 -0
  48. package/dist/services/tsa/TSAClient.js +178 -0
  49. package/dist/services/tsa/validation.d.ts +2 -0
  50. package/dist/services/tsa/validation.js +12 -0
  51. package/dist/transport/HttpClient.d.ts +85 -0
  52. package/dist/transport/HttpClient.js +135 -0
  53. package/dist/transport/RouteResolver.d.ts +54 -0
  54. package/dist/transport/RouteResolver.js +67 -0
  55. package/dist/transport/retry-after.d.ts +17 -0
  56. package/dist/transport/retry-after.js +39 -0
  57. package/dist/transport/retry.d.ts +8 -0
  58. package/dist/transport/retry.js +61 -0
  59. package/dist/types/common.d.ts +133 -0
  60. package/dist/types/common.js +42 -0
  61. package/dist/types/index.d.ts +4 -0
  62. package/dist/types/index.js +1 -0
  63. package/dist/types/muid.d.ts +80 -0
  64. package/dist/types/muid.js +1 -0
  65. package/dist/types/notary.d.ts +243 -0
  66. package/dist/types/notary.js +1 -0
  67. package/dist/types/slink.d.ts +60 -0
  68. package/dist/types/slink.js +1 -0
  69. package/dist/types/tsa.d.ts +144 -0
  70. package/dist/types/tsa.js +1 -0
  71. package/package.json +67 -0
@@ -0,0 +1,291 @@
1
+ import { MajikSignature, } from "@majikah/majik-signature";
2
+ import { APIError } from "../../errors/APIError";
3
+ import { MajikahError } from "../../errors/MajikahError";
4
+ import { assertNonEmpty } from "../shared/validation";
5
+ import { sleep } from "../shared/sleep";
6
+ import { validateSealHash } from "./validation";
7
+ import { normalizeToBlob } from "../shared/encoding";
8
+ /**
9
+ * Client for notarizing sealed Majik Signature documents on-chain.
10
+ *
11
+ * Notarization uses a two-phase flow:
12
+ *
13
+ * 1. A sealed document is identified by its `sealHash` and payment is
14
+ * initiated or detected.
15
+ * 2. After payment is complete, the seal is registered on-chain and the
16
+ * resulting anchor is embedded back into the document.
17
+ *
18
+ * Lower-level methods such as `payment()`, `register()`, and `status()`
19
+ * expose the individual API operations, while the higher-level methods
20
+ * provide complete file-aware workflows.
21
+ */
22
+ export class NotaryClient {
23
+ http;
24
+ /**
25
+ * Creates a Notary client using the provided HTTP transport.
26
+ *
27
+ * @param http HTTP client used to communicate with the Majikah API.
28
+ */
29
+ constructor(http) {
30
+ this.http = http;
31
+ }
32
+ /**
33
+ * Creates or resumes payment for a sealed document.
34
+ *
35
+ * If the seal has already been anchored, the existing chain anchor is
36
+ * returned and no payment is required.
37
+ *
38
+ * If payment has not yet been completed, the response contains checkout
39
+ * information that can be presented to the user.
40
+ *
41
+ * @param sealHash Unique hash identifying the sealed document.
42
+ * @returns Either an existing chain anchor or payment checkout information.
43
+ * @throws APIError When payment has already been completed for the seal.
44
+ */
45
+ async payment(sealHash) {
46
+ validateSealHash(sealHash);
47
+ const raw = await this.http.request("notary", "/payment", {
48
+ method: "POST",
49
+ body: { sealHash },
50
+ });
51
+ if ("anchor" in raw) {
52
+ return { status: "already_anchored", anchor: raw.anchor };
53
+ }
54
+ return { status: "checkout_required", ...raw };
55
+ }
56
+ /**
57
+ * Submits a paid seal for on-chain notarization.
58
+ *
59
+ * Registration returns immediately. If the anchor is still being confirmed,
60
+ * use {@link status} or {@link pollUntilTerminal} to track its progress.
61
+ *
62
+ * Payment must already be completed for the supplied `sealHash`.
63
+ *
64
+ * @param sealHash Unique hash identifying the paid sealed document.
65
+ * @returns The newly created or existing chain anchor.
66
+ * @throws APIError When no completed payment exists for the seal hash.
67
+ */
68
+ async register(sealHash) {
69
+ validateSealHash(sealHash);
70
+ return this.http.request("notary", "/register", {
71
+ method: "POST",
72
+ body: { sealHash },
73
+ });
74
+ }
75
+ /**
76
+ * Retrieves the current confirmation status of a notarization anchor.
77
+ *
78
+ * @param anchorId Identifier of the chain anchor.
79
+ * @returns The current chain anchor state.
80
+ * @throws ValidationError When the anchor ID is empty.
81
+ */
82
+ async status(anchorId) {
83
+ assertNonEmpty(anchorId, "anchorId");
84
+ return this.http.request("notary", `/status/${encodeURIComponent(anchorId)}`, {
85
+ method: "GET",
86
+ });
87
+ }
88
+ /**
89
+ * Polls a notarization anchor until it reaches a terminal state.
90
+ *
91
+ * Resolves when the anchor becomes `confirmed`, `finalized`, or `failed`.
92
+ * A failed anchor is returned normally rather than thrown as an exception;
93
+ * callers should inspect `anchor.status` to determine the outcome.
94
+ *
95
+ * @param anchorId Identifier of the chain anchor to monitor.
96
+ * @param options Polling interval and timeout settings.
97
+ * @returns The anchor in its terminal state.
98
+ * @throws MajikahError When the anchor remains pending until the timeout.
99
+ */
100
+ async pollUntilTerminal(anchorId, options) {
101
+ const interval = options?.intervalMs ?? 2000;
102
+ const timeout = options?.timeoutMs ?? 130_000;
103
+ const start = Date.now();
104
+ for (;;) {
105
+ const anchor = await this.status(anchorId);
106
+ if (anchor.status !== "pending") {
107
+ return anchor;
108
+ }
109
+ if (Date.now() - start >= timeout) {
110
+ throw new MajikahError(`Anchor "${anchorId}" did not reach a terminal state within ${timeout}ms (last status: pending). ` +
111
+ `Call status("${anchorId}") again later to check.`);
112
+ }
113
+ await sleep(interval);
114
+ }
115
+ }
116
+ /**
117
+ * Starts the payment-aware notarization flow for an already-sealed file.
118
+ *
119
+ * This method does not wait for payment or blockchain confirmation.
120
+ * Its result tells the caller what action is required next:
121
+ *
122
+ * - `anchored`: The document was already anchored and the returned blob
123
+ * contains the existing anchor.
124
+ * - `payment_required`: Present the checkout information to the user,
125
+ * then call {@link finalizeNotarization} after payment completes.
126
+ * - `ready_to_finalize`: Payment was previously completed, so
127
+ * {@link finalizeNotarization} can be called immediately.
128
+ *
129
+ * @param file Sealed file to notarize.
130
+ * @param options Optional MIME type used when reading and updating the file.
131
+ * @returns The current state of the notarization flow.
132
+ * @throws ValidationError When the file is not sealed.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const result = await majikah.notary.initiateNotarization(sealedFile);
137
+ *
138
+ * if (result.status === "payment_required") {
139
+ * showQrCode(result.checkout.checkout_url);
140
+ *
141
+ * // After the user completes payment:
142
+ * const { blob, anchor } =
143
+ * await majikah.notary.finalizeNotarization(
144
+ * sealedFile,
145
+ * result.sealHash,
146
+ * );
147
+ * }
148
+ * ```
149
+ */
150
+ async initiateNotarization(file, options) {
151
+ const mimeOpt = options?.mimeType
152
+ ? { mimeType: options.mimeType }
153
+ : undefined;
154
+ const sealInfo = await MajikSignature.getSealInfo(file, mimeOpt);
155
+ if (!sealInfo) {
156
+ throw new (await import("../../errors/ValidationError")).ValidationError("File is not sealed — notarization requires a sealed envelope. " +
157
+ "Seal it first via MajikSignature.seal(), or use sealAndInitiateNotarization() / signSealAndInitiateNotarization().", file);
158
+ }
159
+ const sealHash = sealInfo.sealHash;
160
+ try {
161
+ const result = await this.payment(sealHash);
162
+ if (result.status === "already_anchored") {
163
+ const blob = await MajikSignature.registerChainAnchor(file, result.anchor, mimeOpt);
164
+ return {
165
+ status: "anchored",
166
+ blob,
167
+ anchor: result.anchor,
168
+ };
169
+ }
170
+ const { status: _drop, ...checkout } = result;
171
+ const sealedBlob = normalizeToBlob(file, options?.mimeType ?? "application/octet-stream");
172
+ return {
173
+ status: "payment_required",
174
+ sealHash,
175
+ checkout,
176
+ sealedBlob,
177
+ };
178
+ }
179
+ catch (err) {
180
+ if (err instanceof APIError && err.code === "ALREADY_PAID") {
181
+ const sealedBlob = normalizeToBlob(file, options?.mimeType ?? "application/octet-stream");
182
+ return {
183
+ status: "ready_to_finalize",
184
+ sealHash,
185
+ sealedBlob,
186
+ };
187
+ }
188
+ throw err;
189
+ }
190
+ }
191
+ /**
192
+ * Completes notarization after payment has been completed.
193
+ *
194
+ * Registers the sealed document's `sealHash`, waits for confirmation when
195
+ * necessary, and embeds the resulting chain anchor back into the file.
196
+ *
197
+ * Call this after {@link initiateNotarization} returns either
198
+ * `payment_required` and the user has paid, or `ready_to_finalize`.
199
+ *
200
+ * @param file Sealed file being notarized.
201
+ * @param sealHash Seal hash returned by {@link initiateNotarization}.
202
+ * @param options MIME type and polling settings.
203
+ * @returns The notarized file and its chain anchor.
204
+ */
205
+ async finalizeNotarization(file, sealHash, options) {
206
+ validateSealHash(sealHash);
207
+ let anchor = await this.register(sealHash);
208
+ if (anchor.status === "pending") {
209
+ anchor = await this.pollUntilTerminal(anchor.id, options?.poll);
210
+ }
211
+ const blob = await MajikSignature.registerChainAnchor(file, anchor, options?.mimeType ? { mimeType: options.mimeType } : undefined);
212
+ return {
213
+ blob,
214
+ anchor,
215
+ };
216
+ }
217
+ /**
218
+ * Seals an already-signed file and starts the notarization flow.
219
+ *
220
+ * Use this when all required signers have already signed the document and
221
+ * the issuer is ready to seal the signature envelope and begin payment.
222
+ *
223
+ * @param file Already-signed file.
224
+ * @param issuerKey Unlocked MajikKey belonging to the issuer sealing the file.
225
+ * @param options MIME type and seal timestamp settings.
226
+ * @returns The current state of the notarization flow.
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * const result =
231
+ * await majikah.notary.sealAndInitiateNotarization(
232
+ * signedFile,
233
+ * issuerKey,
234
+ * );
235
+ * ```
236
+ */
237
+ async sealAndInitiateNotarization(file, issuerKey, options) {
238
+ const { blob: sealedBlob } = await MajikSignature.seal(file, issuerKey, {
239
+ mimeType: options?.mimeType,
240
+ timestamp: options?.sealTimestamp,
241
+ });
242
+ return this.initiateNotarization(sealedBlob, {
243
+ mimeType: options?.mimeType,
244
+ });
245
+ }
246
+ /**
247
+ * Signs, seals, and starts notarization for a single-signer document.
248
+ *
249
+ * This is the complete convenience flow for documents where the supplied
250
+ * key is the only signer:
251
+ *
252
+ * `sign → seal → initiate notarization`
253
+ *
254
+ * Sealing makes the envelope immutable to further signatures, so this
255
+ * method should not be used when additional signers still need to sign.
256
+ * For multi-signature documents, have all signers sign first and then use
257
+ * {@link sealAndInitiateNotarization}.
258
+ *
259
+ * @param file File to sign and notarize.
260
+ * @param key Unlocked MajikKey used for both signing and sealing.
261
+ * @param options Signing, MIME type, validity, and sealing settings.
262
+ * @returns The current state of the notarization flow.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * const result =
267
+ * await majikah.notary.signSealAndInitiateNotarization(
268
+ * file,
269
+ * aliceKey,
270
+ * {
271
+ * contentType: "application/pdf",
272
+ * },
273
+ * );
274
+ * ```
275
+ */
276
+ async signSealAndInitiateNotarization(file, key, options) {
277
+ const { blob: signedBlob } = await MajikSignature.signFile(file, key, {
278
+ contentType: options?.contentType,
279
+ timestamp: options?.timestamp,
280
+ mimeType: options?.mimeType,
281
+ expectedSigners: options?.expectedSigners,
282
+ validUntil: options?.validUntil,
283
+ });
284
+ return this.sealAndInitiateNotarization(signedBlob, key, {
285
+ mimeType: options?.mimeType,
286
+ sealTimestamp: options?.sealTimestamp,
287
+ });
288
+ }
289
+ }
290
+ Object.freeze(NotaryClient);
291
+ Object.freeze(NotaryClient.prototype);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Mirrors the gateway's own validateNotaryRequest() exactly — SHA3-512,
3
+ * hex-encoded, SEAL_HASH_HEX_LEN (128) characters. Distinct from TSA's
4
+ * content hash (base64/SHA-256) — different hash, different encoding,
5
+ * different length; don't conflate the two.
6
+ */
7
+ export declare function validateSealHash(sealHash: string): void;
@@ -0,0 +1,19 @@
1
+ import { SEAL_HASH_HEX_LEN } from "@majikah/majik-signature";
2
+ import { ValidationError } from "../../errors/ValidationError";
3
+ /**
4
+ * Mirrors the gateway's own validateNotaryRequest() exactly — SHA3-512,
5
+ * hex-encoded, SEAL_HASH_HEX_LEN (128) characters. Distinct from TSA's
6
+ * content hash (base64/SHA-256) — different hash, different encoding,
7
+ * different length; don't conflate the two.
8
+ */
9
+ export function validateSealHash(sealHash) {
10
+ if (typeof sealHash !== "string") {
11
+ throw new ValidationError("sealHash must be a string", sealHash);
12
+ }
13
+ if (sealHash.length !== SEAL_HASH_HEX_LEN) {
14
+ throw new ValidationError(`sealHash must be exactly ${SEAL_HASH_HEX_LEN} hex characters (SHA3-512), got ${sealHash.length}`, sealHash);
15
+ }
16
+ if (!/^[0-9a-f]+$/i.test(sealHash)) {
17
+ throw new ValidationError("sealHash must be a hex string", sealHash);
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ import { FileLike } from "@majikah/majik-signature";
2
+ /**
3
+ * Converts a Base64-encoded string into raw bytes.
4
+ *
5
+ * Uses the runtime's `atob()` implementation and returns the decoded
6
+ * content as a `Uint8Array`.
7
+ *
8
+ * @param b64 Base64-encoded string.
9
+ * @returns Decoded bytes.
10
+ * @throws DOMException When the input is not valid Base64.
11
+ */
12
+ export declare function base64ToBytes(b64: string): Uint8Array;
13
+ /**
14
+ * Converts raw bytes into a Base64-encoded string.
15
+ *
16
+ * @param bytes Bytes to encode.
17
+ * @returns Base64 representation of the provided bytes.
18
+ */
19
+ export declare function bytesToBase64(bytes: Uint8Array): string;
20
+ export declare function bytesToBlob(bytes: Uint8Array, mimeType: string): Blob;
21
+ export declare function normalizeToBlob(input: FileLike, mimeType?: string): Blob;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Converts a Base64-encoded string into raw bytes.
3
+ *
4
+ * Uses the runtime's `atob()` implementation and returns the decoded
5
+ * content as a `Uint8Array`.
6
+ *
7
+ * @param b64 Base64-encoded string.
8
+ * @returns Decoded bytes.
9
+ * @throws DOMException When the input is not valid Base64.
10
+ */
11
+ export function base64ToBytes(b64) {
12
+ const binary = atob(b64);
13
+ const bytes = new Uint8Array(binary.length);
14
+ for (let i = 0; i < binary.length; i++) {
15
+ bytes[i] = binary.charCodeAt(i);
16
+ }
17
+ return bytes;
18
+ }
19
+ /**
20
+ * Converts raw bytes into a Base64-encoded string.
21
+ *
22
+ * @param bytes Bytes to encode.
23
+ * @returns Base64 representation of the provided bytes.
24
+ */
25
+ export function bytesToBase64(bytes) {
26
+ let binary = "";
27
+ for (let i = 0; i < bytes.length; i++) {
28
+ binary += String.fromCharCode(bytes[i]);
29
+ }
30
+ return btoa(binary);
31
+ }
32
+ export function bytesToBlob(bytes, mimeType) {
33
+ return new Blob([bytes], { type: mimeType });
34
+ }
35
+ export function normalizeToBlob(input, mimeType = "application/octet-stream") {
36
+ if (input instanceof Blob)
37
+ return input;
38
+ if (input instanceof Uint8Array)
39
+ return bytesToBlob(input, mimeType);
40
+ if (input instanceof ArrayBuffer)
41
+ return bytesToBlob(new Uint8Array(input), mimeType);
42
+ throw new Error("Unsupported file input — expected Blob, File, Uint8Array, or ArrayBuffer.");
43
+ }
@@ -0,0 +1,23 @@
1
+ import type { MajikSignature } from "@majikah/majik-signature";
2
+ export interface ResolveSignatureOptions {
3
+ /**
4
+ * Appended to the "no signatures found" error — lets each caller point
5
+ * to its own "sign it first" guidance (TSA vs MUID have different next
6
+ * steps) without this shared helper needing to know about either.
7
+ */
8
+ noSignatureHint?: string;
9
+ }
10
+ /**
11
+ * Resolves a single target signature out of a file/envelope's signature
12
+ * list — the shared "which signer did you mean" logic used anywhere an
13
+ * SDK method needs to act on exactly one existing signature rather than
14
+ * create a new one (TSA timestamping an existing file, MUID verifying an
15
+ * existing file).
16
+ *
17
+ * - No signatures at all → throws with an actionable hint.
18
+ * - expectedSignerId provided → must match exactly, or throws.
19
+ * - No expectedSignerId, exactly one signer → returns it.
20
+ * - No expectedSignerId, multiple signers → throws asking for disambiguation,
21
+ * rather than silently guessing "the first" or "the last" one.
22
+ */
23
+ export declare function resolveTargetSignature(signatures: MajikSignature[], expectedSignerId?: string, options?: ResolveSignatureOptions): MajikSignature;
@@ -0,0 +1,31 @@
1
+ import { ValidationError } from "../../errors/ValidationError";
2
+ /**
3
+ * Resolves a single target signature out of a file/envelope's signature
4
+ * list — the shared "which signer did you mean" logic used anywhere an
5
+ * SDK method needs to act on exactly one existing signature rather than
6
+ * create a new one (TSA timestamping an existing file, MUID verifying an
7
+ * existing file).
8
+ *
9
+ * - No signatures at all → throws with an actionable hint.
10
+ * - expectedSignerId provided → must match exactly, or throws.
11
+ * - No expectedSignerId, exactly one signer → returns it.
12
+ * - No expectedSignerId, multiple signers → throws asking for disambiguation,
13
+ * rather than silently guessing "the first" or "the last" one.
14
+ */
15
+ export function resolveTargetSignature(signatures, expectedSignerId, options) {
16
+ if (signatures.length === 0) {
17
+ const hint = options?.noSignatureHint ? ` ${options.noSignatureHint}` : "";
18
+ throw new ValidationError(`No signatures found.${hint}`, signatures);
19
+ }
20
+ if (expectedSignerId) {
21
+ const match = signatures.find((s) => s.signerId === expectedSignerId);
22
+ if (!match) {
23
+ throw new ValidationError(`No signature found for signerId "${expectedSignerId}".`, expectedSignerId);
24
+ }
25
+ return match;
26
+ }
27
+ if (signatures.length > 1) {
28
+ throw new ValidationError(`This file/envelope has ${signatures.length} signers — pass expectedSignerId to disambiguate which one to use.`, signatures.map((s) => s.signerId));
29
+ }
30
+ return signatures[0];
31
+ }
@@ -0,0 +1 @@
1
+ export declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,3 @@
1
+ export function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
@@ -0,0 +1,9 @@
1
+ export declare function isSHA256Hex(value: unknown): value is string;
2
+ /**
3
+ * Validates that a string contains a non-empty, non-whitespace value.
4
+ *
5
+ * @param value Value to validate.
6
+ * @param field Field name used in the validation error message.
7
+ * @throws ValidationError When the value is empty or not a string.
8
+ */
9
+ export declare function assertNonEmpty(value: string, field: string): void;
@@ -0,0 +1,16 @@
1
+ import { ValidationError } from "../../errors";
2
+ export function isSHA256Hex(value) {
3
+ return typeof value === "string" && /^[a-f0-9]{64}$/i.test(value);
4
+ }
5
+ /**
6
+ * Validates that a string contains a non-empty, non-whitespace value.
7
+ *
8
+ * @param value Value to validate.
9
+ * @param field Field name used in the validation error message.
10
+ * @throws ValidationError When the value is empty or not a string.
11
+ */
12
+ export function assertNonEmpty(value, field) {
13
+ if (typeof value !== "string" || value.trim().length === 0) {
14
+ throw new ValidationError(`${field} must be a non-empty string`, value);
15
+ }
16
+ }
@@ -0,0 +1,174 @@
1
+ import type { MajikKey } from "@majikah/majik-key";
2
+ import { MajikSLink, type MajikSLinkJSON } from "@majikah/majik-slink";
3
+ import type { HttpClient } from "../../transport/HttpClient";
4
+ import type { PageResult, PaginationParams } from "../../types/common";
5
+ import type { PublicKeyResolver, RegisterUrlOptions, SLinkPublicView, SLinkSearchResult, VerifiedSLinkMatch } from "../../types/slink";
6
+ /**
7
+ * Client for creating, registering, searching, and verifying Majik SLinks.
8
+ *
9
+ * SLink verification has two distinct stages:
10
+ * - API lookup confirms that a claim exists.
11
+ * - Local cryptographic verification confirms that the claim's signature
12
+ * is valid using trusted public keys.
13
+ */
14
+ export declare class SLinkClient {
15
+ private readonly http;
16
+ /**
17
+ * Creates an SLink client using the provided HTTP transport.
18
+ *
19
+ * @param http HTTP client used to communicate with the Majikah API.
20
+ */
21
+ constructor(http: HttpClient);
22
+ /**
23
+ * Registers an SLink that has already been created and signed.
24
+ *
25
+ * Accepts either a `MajikSLink` instance or its serialized JSON form.
26
+ *
27
+ * @param slink SLink instance or serialized SLink data.
28
+ * @returns The registered SLink.
29
+ * @throws ValidationError When no SLink is provided.
30
+ */
31
+ create(slink: MajikSLink | MajikSLinkJSON): Promise<MajikSLinkJSON>;
32
+ /**
33
+ * Creates, signs, and registers an SLink for a URL in one operation.
34
+ *
35
+ * This is the convenience method for callers that do not already have a
36
+ * `MajikSLink` instance. It requires an unlocked `MajikKey` to sign the
37
+ * claim.
38
+ *
39
+ * @param rawUrl URL or domain to associate with the SLink.
40
+ * @param key Unlocked MajikKey used to sign the SLink.
41
+ * @param userId User identifier recorded as the signer.
42
+ * @param muid MUID associated with the claim.
43
+ * @param options Optional SLink metadata.
44
+ * @returns The registered SLink.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const stored = await majikah.slink.registerUrl(
49
+ * "https://youtube.com/watch?v=dQw4w9WgXcQ",
50
+ * aliceKey,
51
+ * userId,
52
+ * muid,
53
+ * );
54
+ * ```
55
+ */
56
+ registerUrl(rawUrl: string, key: MajikKey, userId: string, muid: string, options?: RegisterUrlOptions): Promise<MajikSLinkJSON>;
57
+ /**
58
+ * Lists SLinks owned by the current API credentials.
59
+ *
60
+ * Results use cursor-based pagination. Pass the returned `next_cursor`
61
+ * directly into the next request's `cursor` parameter.
62
+ *
63
+ * @param params Optional pagination settings.
64
+ * @returns A page of SLinks and pagination metadata.
65
+ */
66
+ me(params?: PaginationParams): Promise<PageResult<MajikSLinkJSON>>;
67
+ /**
68
+ * Searches for SLink claims associated with a content hash.
69
+ *
70
+ * This confirms which SLink claims exist for the supplied hash but does not
71
+ * perform cryptographic signature verification.
72
+ *
73
+ * @param hash Content hash associated with the SLink claims.
74
+ * @returns Matching SLinks and their public MUID information.
75
+ * @throws ValidationError When the hash is empty.
76
+ */
77
+ verifyByHash(hash: string): Promise<SLinkSearchResult>;
78
+ /**
79
+ * Searches for SLink claims associated with a URL.
80
+ *
81
+ * Bare domains are automatically normalized to use `https://`.
82
+ * This method confirms that matching claims exist but does not verify their
83
+ * signatures cryptographically.
84
+ *
85
+ * @param url URL or domain associated with the SLink claim.
86
+ * @returns Matching SLinks and their public MUID information.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const results = await majikah.slink.verifyUrl("thezelijah.world");
91
+ * ```
92
+ */
93
+ verifyUrl(url: string): Promise<SLinkSearchResult>;
94
+ /**
95
+ * Finds SLink claims for a URL and cryptographically verifies every
96
+ * matching signature locally.
97
+ *
98
+ * The API lookup only establishes that a claim exists for the URL. It does
99
+ * not prove that a signature is valid because SLink public keys are not
100
+ * stored server-side. This method resolves the required public keys through
101
+ * `resolvePublicKeys` and verifies each signature locally.
102
+ *
103
+ * For the common MUID-backed flow, use `createMuidPublicKeyResolver()`.
104
+ * It resolves each signer's public keys through `MUIDClient.lookup()` and
105
+ * converts the API's Base64-encoded keys into the format required for
106
+ * cryptographic verification.
107
+ *
108
+ * @param url URL or domain associated with the SLink claims.
109
+ * @param resolvePublicKeys Callback that resolves trusted public keys for
110
+ * each SLink signer.
111
+ * @returns Each matching SLink paired with its local verification result.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * import { createMuidPublicKeyResolver } from "@majikah/sdk";
116
+ *
117
+ * const results = await majikah.slink.verifyUrlWithProof(
118
+ * "thezelijah.world",
119
+ * createMuidPublicKeyResolver(majikah.muid),
120
+ * );
121
+ *
122
+ * const verified = results.filter(({ result }) => result.valid);
123
+ * ```
124
+ *
125
+ * A custom resolver can be used when public keys come from another trusted
126
+ * registry or key source:
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * const results = await majikah.slink.verifyUrlWithProof(
131
+ * "thezelijah.world",
132
+ * async (muid, signerId) => {
133
+ * const keys = await myKeyRegistry.get(muid);
134
+ *
135
+ * return {
136
+ * signerId,
137
+ * edPublicKey: keys.edPublicKey,
138
+ * mlDsaPublicKey: keys.mlDsaPublicKey,
139
+ * };
140
+ * },
141
+ * );
142
+ * ```
143
+ */
144
+ verifyUrlWithProof(url: string, resolvePublicKeys: PublicKeyResolver): Promise<VerifiedSLinkMatch[]>;
145
+ /**
146
+ * Cryptographically verifies already-fetched SLink search results locally.
147
+ *
148
+ * Use this when the matches were obtained separately through `verifyUrl()`
149
+ * or `verifyByHash()` and you want to verify them without another API lookup.
150
+ *
151
+ * @param matches SLink matches to verify.
152
+ * @param resolvePublicKeys Callback used to obtain trusted public signing keys.
153
+ * @returns Each match paired with its local cryptographic verification result.
154
+ */
155
+ verifyMatches(matches: SLinkPublicView[], resolvePublicKeys: PublicKeyResolver): Promise<VerifiedSLinkMatch[]>;
156
+ /**
157
+ * Retrieves an SLink by its identifier.
158
+ *
159
+ * @param id SLink identifier.
160
+ * @returns The SLink together with its owner's public MUID information.
161
+ * @throws ValidationError When the identifier is empty.
162
+ */
163
+ lookup(id: string): Promise<SLinkPublicView>;
164
+ /**
165
+ * Deletes an SLink by its identifier.
166
+ *
167
+ * @param id SLink identifier.
168
+ * @returns The identifier of the deleted SLink.
169
+ * @throws ValidationError When the identifier is empty.
170
+ */
171
+ delete(id: string): Promise<{
172
+ id: string;
173
+ }>;
174
+ }