@solutiondesk/legal 0.0.0 → 1.0.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 +21 -0
- package/README.md +176 -27
- package/dist/index.cjs +309 -0
- package/dist/index.d.cts +456 -0
- package/dist/index.d.ts +456 -0
- package/dist/index.js +298 -0
- package/package.json +58 -7
package/dist/index.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// ../sdk-core/src/http.ts
|
|
6
|
+
var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
|
|
7
|
+
var SolutionDeskError = class extends Error {
|
|
8
|
+
constructor(message, init) {
|
|
9
|
+
super(message);
|
|
10
|
+
/** The HTTP status; `0` when no answer arrived (network error, aborted). */
|
|
11
|
+
__publicField(this, "status");
|
|
12
|
+
/** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
|
|
13
|
+
__publicField(this, "body");
|
|
14
|
+
/** The requested URL. */
|
|
15
|
+
__publicField(this, "url");
|
|
16
|
+
/** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
|
|
17
|
+
__publicField(this, "fields");
|
|
18
|
+
this.name = "SolutionDeskError";
|
|
19
|
+
this.status = init.status;
|
|
20
|
+
this.body = init.body ?? null;
|
|
21
|
+
this.url = init.url;
|
|
22
|
+
this.fields = fieldsOf(init.body);
|
|
23
|
+
if (init.cause !== void 0) {
|
|
24
|
+
Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
|
|
28
|
+
get isNotFound() {
|
|
29
|
+
return this.status === 404;
|
|
30
|
+
}
|
|
31
|
+
/** `429`: over the rate limit; try again later. */
|
|
32
|
+
get isRateLimited() {
|
|
33
|
+
return this.status === 429;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function fieldsOf(body) {
|
|
37
|
+
if (!body || typeof body !== "object") return {};
|
|
38
|
+
const fields = body.fields;
|
|
39
|
+
if (!fields || typeof fields !== "object") return {};
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
42
|
+
if (typeof value === "string") out[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function messageOf(body, status) {
|
|
47
|
+
if (body && typeof body === "object") {
|
|
48
|
+
const { error, message, statusCode } = body;
|
|
49
|
+
if (typeof statusCode === "number" && typeof message === "string" && message) return message;
|
|
50
|
+
if (typeof error === "string" && error) return error;
|
|
51
|
+
if (typeof message === "string" && message) return message;
|
|
52
|
+
if (Array.isArray(message) && typeof message[0] === "string") return message[0];
|
|
53
|
+
}
|
|
54
|
+
if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
|
|
55
|
+
return `SolutionDesk answered ${status}.`;
|
|
56
|
+
}
|
|
57
|
+
function createMemoryEtagCache(maxEntries = 500) {
|
|
58
|
+
const entries = /* @__PURE__ */ new Map();
|
|
59
|
+
return {
|
|
60
|
+
get: (key) => entries.get(key),
|
|
61
|
+
set(key, value) {
|
|
62
|
+
entries.delete(key);
|
|
63
|
+
entries.set(key, value);
|
|
64
|
+
if (entries.size > maxEntries) {
|
|
65
|
+
const oldest = entries.keys().next().value;
|
|
66
|
+
if (oldest !== void 0) entries.delete(oldest);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function queryString(query) {
|
|
72
|
+
if (!query) return "";
|
|
73
|
+
const parts = [];
|
|
74
|
+
for (const [key, value] of Object.entries(query)) {
|
|
75
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
76
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
77
|
+
}
|
|
78
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
79
|
+
}
|
|
80
|
+
var seg = (value) => encodeURIComponent(String(value));
|
|
81
|
+
function parseBody(text) {
|
|
82
|
+
if (!text) return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(text);
|
|
85
|
+
} catch {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function createHttp(options = {}) {
|
|
90
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
91
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
92
|
+
const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
|
|
93
|
+
const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
|
|
94
|
+
async function requestWithMeta(req) {
|
|
95
|
+
if (!fetchImpl) {
|
|
96
|
+
throw new Error("No global fetch available: pass `fetch` to the client.");
|
|
97
|
+
}
|
|
98
|
+
const target = url(req.path, req.query);
|
|
99
|
+
const headers = {
|
|
100
|
+
accept: "application/json",
|
|
101
|
+
...options.headers,
|
|
102
|
+
...req.options?.headers
|
|
103
|
+
};
|
|
104
|
+
if (req.body !== void 0) headers["content-type"] = "application/json";
|
|
105
|
+
if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
|
|
106
|
+
const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
|
|
107
|
+
const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
|
|
108
|
+
if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
|
|
109
|
+
headers["if-none-match"] = cached.etag;
|
|
110
|
+
}
|
|
111
|
+
const init = {
|
|
112
|
+
...req.options?.fetchOptions,
|
|
113
|
+
method: req.method,
|
|
114
|
+
headers
|
|
115
|
+
};
|
|
116
|
+
if (req.body !== void 0) init.body = JSON.stringify(req.body);
|
|
117
|
+
if (req.options?.signal !== void 0) init.signal = req.options.signal;
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = await fetchImpl(target, init);
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
123
|
+
throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
|
|
124
|
+
status: 0,
|
|
125
|
+
url: target,
|
|
126
|
+
cause
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const finalUrl = response.url;
|
|
130
|
+
const meta = {
|
|
131
|
+
status: response.status,
|
|
132
|
+
finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
|
|
133
|
+
};
|
|
134
|
+
if (response.status === 304 && cached) {
|
|
135
|
+
return { data: cached.body, ...meta };
|
|
136
|
+
}
|
|
137
|
+
const body = parseBody(await response.text());
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new SolutionDeskError(messageOf(body, response.status), {
|
|
140
|
+
status: response.status,
|
|
141
|
+
body,
|
|
142
|
+
url: target
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const etag = response.headers.get("etag");
|
|
146
|
+
if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
|
|
147
|
+
return { data: body, ...meta };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
baseUrl,
|
|
151
|
+
url,
|
|
152
|
+
requestWithMeta,
|
|
153
|
+
request: async (req) => (await requestWithMeta(req)).data
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function required(value, name) {
|
|
157
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
158
|
+
throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ../sdk-core/src/webhook.ts
|
|
164
|
+
var SIGNATURE_HEADER = "x-solutiondesk-signature";
|
|
165
|
+
var EVENT_HEADER = "x-solutiondesk-event";
|
|
166
|
+
function encoder() {
|
|
167
|
+
const Encoder = globalThis.TextEncoder;
|
|
168
|
+
if (!Encoder) throw new Error("TextEncoder is not available in this runtime.");
|
|
169
|
+
return new Encoder();
|
|
170
|
+
}
|
|
171
|
+
function bytesOf(body) {
|
|
172
|
+
if (typeof body === "string") return encoder().encode(body);
|
|
173
|
+
return body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
174
|
+
}
|
|
175
|
+
function webhookBodyText(body) {
|
|
176
|
+
if (typeof body === "string") return body;
|
|
177
|
+
const Decoder = globalThis.TextDecoder;
|
|
178
|
+
if (!Decoder) throw new Error("TextDecoder is not available in this runtime.");
|
|
179
|
+
return new Decoder().decode(bytesOf(body));
|
|
180
|
+
}
|
|
181
|
+
function hex(buffer) {
|
|
182
|
+
let out = "";
|
|
183
|
+
for (const byte of new Uint8Array(buffer)) out += byte.toString(16).padStart(2, "0");
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
function constantTimeEqual(a, b) {
|
|
187
|
+
if (a.length !== b.length) return false;
|
|
188
|
+
let diff = 0;
|
|
189
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
190
|
+
return diff === 0;
|
|
191
|
+
}
|
|
192
|
+
function parseHeader(header) {
|
|
193
|
+
let t = Number.NaN;
|
|
194
|
+
const signatures = [];
|
|
195
|
+
for (const part of header.split(",")) {
|
|
196
|
+
const index = part.indexOf("=");
|
|
197
|
+
if (index < 0) continue;
|
|
198
|
+
const key = part.slice(0, index).trim();
|
|
199
|
+
const value = part.slice(index + 1).trim();
|
|
200
|
+
if (key === "t" && /^\d{1,12}$/.test(value)) t = Number(value);
|
|
201
|
+
else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) signatures.push(value.toLowerCase());
|
|
202
|
+
}
|
|
203
|
+
return Number.isFinite(t) && signatures.length > 0 ? { t, signatures } : null;
|
|
204
|
+
}
|
|
205
|
+
async function verifyWebhookSignature(secret, rawBody, header, options = {}) {
|
|
206
|
+
if (!secret || !header || header.length > 1e3) return false;
|
|
207
|
+
const parsed = parseHeader(header);
|
|
208
|
+
if (!parsed) return false;
|
|
209
|
+
const now = options.now instanceof Date ? options.now.getTime() : options.now ?? Date.now();
|
|
210
|
+
const tolerance = options.toleranceSeconds ?? 300;
|
|
211
|
+
if (Math.abs(now / 1e3 - parsed.t) > tolerance) return false;
|
|
212
|
+
const subtle = options.crypto?.subtle ?? globalThis.crypto?.subtle;
|
|
213
|
+
if (!subtle) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Web Crypto is not available: pass `crypto` (on Node 18: require('node:crypto').webcrypto)."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const text = encoder();
|
|
219
|
+
const key = await subtle.importKey(
|
|
220
|
+
"raw",
|
|
221
|
+
text.encode(secret),
|
|
222
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
223
|
+
false,
|
|
224
|
+
["sign"]
|
|
225
|
+
);
|
|
226
|
+
const prefix = text.encode(`${parsed.t}.`);
|
|
227
|
+
const body = bytesOf(rawBody);
|
|
228
|
+
const signed = new Uint8Array(prefix.length + body.length);
|
|
229
|
+
signed.set(prefix, 0);
|
|
230
|
+
signed.set(body, prefix.length);
|
|
231
|
+
const expected = hex(await subtle.sign("HMAC", key, signed));
|
|
232
|
+
let ok = false;
|
|
233
|
+
for (const signature of parsed.signatures) ok = constantTimeEqual(expected, signature) || ok;
|
|
234
|
+
return ok;
|
|
235
|
+
}
|
|
236
|
+
async function parseWebhook(secret, rawBody, header, options = {}) {
|
|
237
|
+
if (!await verifyWebhookSignature(secret, rawBody, header, options)) return null;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(webhookBodyText(rawBody));
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/client.ts
|
|
246
|
+
function createLegalClient(options) {
|
|
247
|
+
const workspace = required(options.workspace, "workspace");
|
|
248
|
+
const http = createHttp(options);
|
|
249
|
+
const base = `/legal/${seg(workspace)}`;
|
|
250
|
+
const langOf = (params) => ({ lang: params?.lang ?? options.lang });
|
|
251
|
+
const get = (path, query, o) => http.request({ method: "GET", path: `${base}${path}`, query, options: o });
|
|
252
|
+
return {
|
|
253
|
+
workspace,
|
|
254
|
+
listDocuments: async (params, o) => get("/documents", langOf(params), o),
|
|
255
|
+
getDocument: async (slug, params, o) => get(`/documents/${seg(required(slug, "slug"))}`, langOf(params), o),
|
|
256
|
+
listVersions: async (slug, params, o) => get(`/documents/${seg(required(slug, "slug"))}/versions`, langOf(params), o),
|
|
257
|
+
getVersion: async (slug, number, params, o) => {
|
|
258
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
259
|
+
throw new TypeError("SolutionDesk: `number` must be a positive whole number.");
|
|
260
|
+
}
|
|
261
|
+
return get(
|
|
262
|
+
`/documents/${seg(required(slug, "slug"))}/versions/${seg(number)}`,
|
|
263
|
+
langOf(params),
|
|
264
|
+
o
|
|
265
|
+
);
|
|
266
|
+
},
|
|
267
|
+
getSubprocessors: async (params, o) => get("/subprocessors", langOf(params), o),
|
|
268
|
+
getPreview: async (token, params, o) => get("/preview", { token: required(token, "token"), ...langOf(params) }, o)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function createLegalHostClient(options) {
|
|
272
|
+
const apiKey = required(options.apiKey, "apiKey");
|
|
273
|
+
const http = createHttp({ ...options, etagCache: false });
|
|
274
|
+
return {
|
|
275
|
+
pending: async (params, o) => http.request({
|
|
276
|
+
method: "GET",
|
|
277
|
+
path: "/integration/legal/pending",
|
|
278
|
+
query: { externalId: required(params.externalId, "externalId"), lang: params.lang },
|
|
279
|
+
apiKey,
|
|
280
|
+
options: o
|
|
281
|
+
}),
|
|
282
|
+
recordAcceptance: async (input, o) => http.request({
|
|
283
|
+
method: "POST",
|
|
284
|
+
path: "/integration/legal/acceptances",
|
|
285
|
+
body: input,
|
|
286
|
+
apiKey,
|
|
287
|
+
options: o
|
|
288
|
+
})
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function parseLegalWebhook(secret, rawBody, signatureHeader, options) {
|
|
292
|
+
return parseWebhook(secret, rawBody, signatureHeader, options);
|
|
293
|
+
}
|
|
294
|
+
function isAcceptanceRecorded(body) {
|
|
295
|
+
return body.event === "acceptance.recorded" && !!body.acceptance && body.document !== null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export { DEFAULT_BASE_URL, EVENT_HEADER, SIGNATURE_HEADER, SolutionDeskError, createLegalClient, createLegalHostClient, createMemoryEtagCache, isAcceptanceRecorded, parseLegalWebhook, verifyWebhookSignature };
|
package/package.json
CHANGED
|
@@ -1,10 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solutiondesk/legal",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Typed, zero-dependency client for the SolutionDesk Legal Hub: imprint, privacy policy, terms, sub-processors, acceptances and webhook verification.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"solutiondesk",
|
|
7
|
+
"legal",
|
|
8
|
+
"imprint",
|
|
9
|
+
"privacy-policy",
|
|
10
|
+
"terms",
|
|
11
|
+
"gdpr",
|
|
12
|
+
"subprocessors",
|
|
13
|
+
"sdk",
|
|
14
|
+
"typescript"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://help.solutiondesk.io",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/wemogy/solutiondesk/issues"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "wemogy IT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/wemogy/solutiondesk.git",
|
|
25
|
+
"directory": "packages/sdk-legal"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"main": "./dist/index.cjs",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"require": {
|
|
42
|
+
"types": "./dist/index.d.cts",
|
|
43
|
+
"default": "./dist/index.cjs"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|