@andesine/sdk 0.1.0 → 0.5.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/CHANGELOG.md +48 -0
- package/README.md +263 -29
- package/WEBHOOKS.md +317 -0
- package/dist/THIRD_PARTY_LICENSES.txt +49 -0
- package/dist/client-C8wt7f3o.d.ts +1596 -0
- package/dist/client-DioqVOxl.js +1424 -0
- package/dist/client-DioqVOxl.js.map +1 -0
- package/dist/content-slug-C7CgtHmp.d.ts +13 -0
- package/dist/content-slug.d.ts +2 -0
- package/dist/content-slug.js +19 -0
- package/dist/content-slug.js.map +1 -0
- package/dist/extensions.d.ts +87 -0
- package/dist/extensions.js +220 -0
- package/dist/extensions.js.map +1 -0
- package/dist/index-B5qefVRv.d.ts +473 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +18 -1115
- package/dist/index.js.map +1 -1
- package/dist/rolldown-runtime-EAsCxrDo.js +14 -0
- package/dist/{index-DFEcYmbh.d.ts → schema-CrquN_J6.d.ts} +13318 -6242
- package/dist/streaming.d.ts +1 -1
- package/dist/webhooks.d.ts +26 -0
- package/dist/webhooks.js +7429 -0
- package/dist/webhooks.js.map +1 -0
- package/openapi.json +16724 -3910
- package/package.json +18 -3
package/WEBHOOKS.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Webhooks
|
|
2
|
+
|
|
3
|
+
Use `client.webhooks` to manage outgoing Andesine webhooks. Use
|
|
4
|
+
`@andesine/sdk/webhooks` in your receiving server to verify incoming requests.
|
|
5
|
+
These are separate tasks: an Andesine API credential manages the endpoint; its
|
|
6
|
+
`whsec_` signing secret authenticates deliveries. Keep both in server-side storage.
|
|
7
|
+
|
|
8
|
+
## Configure an endpoint
|
|
9
|
+
|
|
10
|
+
The API key needs `webhooks` and the read permission for each resource type its
|
|
11
|
+
events send (for example `read:entries` for entry events). OAuth also requires an
|
|
12
|
+
explicit workspace and the member's current permissions. The stored scope belongs
|
|
13
|
+
to the workspace, independently of the creator's account.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createClient } from "@andesine/sdk";
|
|
17
|
+
|
|
18
|
+
const client = createClient({ apiKey: process.env.ANDESINE_API_KEY });
|
|
19
|
+
const { endpoint, secret } = await client.webhooks.create({
|
|
20
|
+
name: "Search index",
|
|
21
|
+
url: "https://receiver.example.com/andesine",
|
|
22
|
+
enabled: false,
|
|
23
|
+
eventTypes: ["entry.content_saved", "entry.deleted"],
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
collections: { mode: "all" },
|
|
26
|
+
channels: { mode: "all" },
|
|
27
|
+
restrictedContent: false
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Save `secret` in your receiver's secret store. It is shown once. Do not print it
|
|
32
|
+
or place it in frontend code. Webhooks are enabled on creation unless `enabled` is
|
|
33
|
+
`false`; this example enables it after a test. Select specific collection roots
|
|
34
|
+
instead of `all` to receive events only for those subtrees. A resource moved out
|
|
35
|
+
of them is reported without its new location. Restricted content needs
|
|
36
|
+
workspace-wide restricted-content read authority and explicit approval.
|
|
37
|
+
|
|
38
|
+
After deploying the receiver with that secret, send a synthetic test:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const { deliveryID } = await client.webhooks.sendTest({
|
|
42
|
+
id: endpoint.id,
|
|
43
|
+
expectedRevision: endpoint.revision,
|
|
44
|
+
type: "entry.content_saved"
|
|
45
|
+
});
|
|
46
|
+
const details = await client.webhooks.getDelivery({ id: endpoint.id, deliveryID });
|
|
47
|
+
// Inspect details again later if details.delivery.state is still pending/in_flight.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Tests contain example resource IDs and `test: true`. They get one attempt, can
|
|
51
|
+
run while disabled, and have a ten-minute queue/completion deadline. Record the
|
|
52
|
+
result, but do not process a test as a real content change. A successful test
|
|
53
|
+
does not enable the endpoint. Enable it explicitly with `update`, using the latest
|
|
54
|
+
`revision`. Configuration conflicts require a fresh `get` and review.
|
|
55
|
+
|
|
56
|
+
The resource provides `list`, `get`, `create`, `update`, `delete`,
|
|
57
|
+
`rotateSecret`, `sendTest`, `listDeliveries`, `getDelivery`, `listRuns`,
|
|
58
|
+
`listAttempts`, and `redeliver`. Lists use the normal cursor/limit pattern. Read
|
|
59
|
+
access permits metadata; payload previews also require content read authority.
|
|
60
|
+
|
|
61
|
+
## Verify before parsing
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { verifyWebhook, WebhookVerificationError, type WebhookEvent } from "@andesine/sdk/webhooks";
|
|
65
|
+
|
|
66
|
+
const event: WebhookEvent = await verifyWebhook({
|
|
67
|
+
body: rawBodyBytes,
|
|
68
|
+
headers: incomingHeaders,
|
|
69
|
+
secret: signingSecret
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!event.test && event.type === "entry.content_saved") {
|
|
73
|
+
// The discriminated union narrows data to content_saved fields.
|
|
74
|
+
console.log(event.data.contentHash);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`body` is a `Uint8Array` (including Node `Buffer`) or unchanged UTF-8 text.
|
|
79
|
+
Never pass parsed JSON, call `JSON.stringify` before verification, or let JSON
|
|
80
|
+
middleware change the body. Byte input is preferred. `headers` accepts standard
|
|
81
|
+
`Headers` or a case-insensitive record, including Node's incoming headers.
|
|
82
|
+
Ambiguous duplicate signature headers are rejected.
|
|
83
|
+
|
|
84
|
+
Both verification helpers return a promise. They authenticate the exact
|
|
85
|
+
`webhook-id.webhook-timestamp.body` bytes with HMAC-SHA256, then parse UTF-8 JSON
|
|
86
|
+
and validate the complete public event schema. The authenticated header ID must
|
|
87
|
+
match `event.id`. Signature comparisons use native Web Crypto. Timestamps more
|
|
88
|
+
than five minutes old or in the future are rejected; keep the receiver clock
|
|
89
|
+
synchronized. There is no option to disable verification or the time check.
|
|
90
|
+
The time check uses the attempt timestamp, not `occurredAt`.
|
|
91
|
+
|
|
92
|
+
The verifier accepts at most 256 KiB and never fetches Andesine or another
|
|
93
|
+
service. `verifyWebhookRequest` consumes the body once, checks its size while
|
|
94
|
+
reading, and passes the bytes to the same verifier. It does not clone the request.
|
|
95
|
+
The receiving server must set its own body-read deadline and request/header limits.
|
|
96
|
+
|
|
97
|
+
`WebhookVerificationError.code` is one of:
|
|
98
|
+
|
|
99
|
+
| Code | Meaning |
|
|
100
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
101
|
+
| `INVALID_HEADERS` | Required signature headers are missing, malformed, or ambiguous |
|
|
102
|
+
| `INVALID_SIGNATURE` | No supplied v1 signature matches a configured key |
|
|
103
|
+
| `TIMESTAMP_TOO_OLD` / `TIMESTAMP_TOO_NEW` | Attempt timestamp is outside the five-minute window |
|
|
104
|
+
| `INVALID_PAYLOAD` | Authenticated body is not valid UTF-8 JSON or does not match the event schema |
|
|
105
|
+
| `PAYLOAD_TOO_LARGE` | Body exceeds 256 KiB |
|
|
106
|
+
| `UNSUPPORTED_EVENT` / `UNSUPPORTED_SCHEMA` | Authenticated event needs a newer SDK |
|
|
107
|
+
| `EVENT_ID_MISMATCH` | Signed header and payload event IDs differ |
|
|
108
|
+
|
|
109
|
+
Errors contain fixed messages, not the body, secret, or signature. Invalid local
|
|
110
|
+
secret configuration throws `TypeError`; unavailable cryptography or stream
|
|
111
|
+
failures propagate as runtime errors. Do not turn these failures into success.
|
|
112
|
+
Unsupported events need an SDK update before replay. Inspect only error codes in
|
|
113
|
+
logs; do not log the request headers, signing secret, or sensitive payload data.
|
|
114
|
+
|
|
115
|
+
## Standard Request receiver
|
|
116
|
+
|
|
117
|
+
`accept` below must durably and idempotently store the event before it resolves.
|
|
118
|
+
The PostgreSQL inbox example below shows that contract. Mount this handler only
|
|
119
|
+
at the configured receiver path. Load its expected workspace and secret from
|
|
120
|
+
trusted configuration, not from the incoming event.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import {
|
|
124
|
+
verifyWebhookRequest,
|
|
125
|
+
WebhookVerificationError,
|
|
126
|
+
type WebhookEvent,
|
|
127
|
+
type WebhookVerificationOptions
|
|
128
|
+
} from "@andesine/sdk/webhooks";
|
|
129
|
+
|
|
130
|
+
interface ReceiverOptions extends WebhookVerificationOptions {
|
|
131
|
+
workspaceID: string;
|
|
132
|
+
integrationID: string;
|
|
133
|
+
accept: (integrationID: string, event: WebhookEvent) => Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function receive(request: Request, options: ReceiverOptions): Promise<Response> {
|
|
137
|
+
if (request.method !== "POST") return new Response(null, { status: 405 });
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const event = await verifyWebhookRequest(request, options);
|
|
141
|
+
|
|
142
|
+
if (event.workspaceID !== options.workspaceID) return new Response(null, { status: 403 });
|
|
143
|
+
|
|
144
|
+
await options.accept(options.integrationID, event);
|
|
145
|
+
return new Response(null, { status: 204 });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const status =
|
|
148
|
+
error instanceof WebhookVerificationError
|
|
149
|
+
? error.code === "PAYLOAD_TOO_LARGE"
|
|
150
|
+
? 413
|
|
151
|
+
: 400
|
|
152
|
+
: 503;
|
|
153
|
+
|
|
154
|
+
return new Response(null, { status });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Node HTTP receiver
|
|
160
|
+
|
|
161
|
+
Use the raw stream before middleware calls `setEncoding` or parses JSON. Configure
|
|
162
|
+
an HTTP request timeout and mount the handler at your receiver route. `options`
|
|
163
|
+
has the same contract as the Request example above.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
167
|
+
import { verifyWebhook, WebhookVerificationError } from "@andesine/sdk/webhooks";
|
|
168
|
+
|
|
169
|
+
async function readRawBody(request: IncomingMessage): Promise<Buffer> {
|
|
170
|
+
const chunks: Buffer[] = [];
|
|
171
|
+
|
|
172
|
+
let size = 0;
|
|
173
|
+
|
|
174
|
+
for await (const chunk of request) {
|
|
175
|
+
if (!Buffer.isBuffer(chunk)) throw new TypeError("Read the request before text middleware");
|
|
176
|
+
|
|
177
|
+
size += chunk.byteLength;
|
|
178
|
+
|
|
179
|
+
if (size > 262_144) throw new WebhookVerificationError("PAYLOAD_TOO_LARGE");
|
|
180
|
+
|
|
181
|
+
chunks.push(chunk);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Buffer.concat(chunks, size);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function receiveNode(
|
|
188
|
+
request: IncomingMessage,
|
|
189
|
+
response: ServerResponse,
|
|
190
|
+
options: ReceiverOptions
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
if (request.method !== "POST") {
|
|
193
|
+
response.writeHead(405, { Connection: "close" }).end();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const body = await readRawBody(request);
|
|
199
|
+
const event = await verifyWebhook({ body, headers: request.headers, secret: options.secret });
|
|
200
|
+
|
|
201
|
+
if (event.workspaceID !== options.workspaceID) {
|
|
202
|
+
response.writeHead(403).end();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await options.accept(options.integrationID, event);
|
|
207
|
+
response.writeHead(204).end();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const status =
|
|
210
|
+
error instanceof WebhookVerificationError
|
|
211
|
+
? error.code === "PAYLOAD_TOO_LARGE"
|
|
212
|
+
? 413
|
|
213
|
+
: 400
|
|
214
|
+
: 503;
|
|
215
|
+
|
|
216
|
+
if (!response.destroyed) response.writeHead(status, { Connection: "close" }).end();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Durable acceptance and duplicate events
|
|
222
|
+
|
|
223
|
+
Delivery is at least once. A receiver can commit a change and lose its response;
|
|
224
|
+
Andesine then retries the same event. Manual replay also keeps the event ID.
|
|
225
|
+
Signatures and timestamps change on each attempt and are not deduplication keys.
|
|
226
|
+
There is no ordering guarantee.
|
|
227
|
+
|
|
228
|
+
For a PostgreSQL receiver, use a durable inbox like this in **your** database:
|
|
229
|
+
|
|
230
|
+
```sql
|
|
231
|
+
CREATE TABLE andesine_inbox (
|
|
232
|
+
integration_id text NOT NULL,
|
|
233
|
+
event_id text NOT NULL,
|
|
234
|
+
event jsonb NOT NULL,
|
|
235
|
+
received_at timestamptz NOT NULL DEFAULT now(),
|
|
236
|
+
processed_at timestamptz,
|
|
237
|
+
PRIMARY KEY (integration_id, event_id)
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import type { Pool } from "pg";
|
|
243
|
+
import type { WebhookEvent } from "@andesine/sdk/webhooks";
|
|
244
|
+
|
|
245
|
+
export function createInbox(pool: Pool) {
|
|
246
|
+
return async (integrationID: string, event: WebhookEvent): Promise<void> => {
|
|
247
|
+
await pool.query(
|
|
248
|
+
`INSERT INTO andesine_inbox (integration_id, event_id, event)
|
|
249
|
+
VALUES ($1, $2, $3::jsonb)
|
|
250
|
+
ON CONFLICT (integration_id, event_id) DO NOTHING`,
|
|
251
|
+
[integrationID, event.id, JSON.stringify(event)]
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
That one statement atomically records acceptance and its deduplication key.
|
|
258
|
+
Return 2xx only after it commits; duplicates also return 2xx. Process the inbox
|
|
259
|
+
with a background worker. Apply local business changes and mark the inbox row
|
|
260
|
+
processed in the **same database transaction**. Skip business changes for `test`
|
|
261
|
+
events. External side effects need their own idempotency key or transactional
|
|
262
|
+
outbox. A signature check alone does not prevent repeated processing.
|
|
263
|
+
|
|
264
|
+
Keep deduplication keys for at least the sender's maximum supported replay period:
|
|
265
|
+
workspace retention defaults to seven days Free or 30 days Pro and can be longer
|
|
266
|
+
on self-hosted systems. Keeping keys indefinitely is also valid. Payloads can be
|
|
267
|
+
removed earlier after processing if their deduplication keys remain. Use the
|
|
268
|
+
configured integration/endpoint and event ID together: two endpoints can receive
|
|
269
|
+
different authorized projections of the same event. Do not deduplicate by
|
|
270
|
+
`operationID`, which can group several distinct events.
|
|
271
|
+
|
|
272
|
+
Events report committed changes, but an API read can already return a newer
|
|
273
|
+
state. Treat events as notifications to refresh state when appropriate. Deleted
|
|
274
|
+
or restricted resources can become unavailable. Publication snapshot references
|
|
275
|
+
can expire under the workspace policy; webhooks do not pin snapshots. Handle
|
|
276
|
+
missing references explicitly.
|
|
277
|
+
|
|
278
|
+
## Rotation, replay, and recovery
|
|
279
|
+
|
|
280
|
+
Normal rotation returns a new secret once and signs with both old and new keys
|
|
281
|
+
for 24 hours. Deploy the new secret during that window. `secret` may also be an
|
|
282
|
+
array of one or two `whsec_` values during receiver rollout. Remove the old value
|
|
283
|
+
after rollout; an old configured key remains trusted by your receiver until you
|
|
284
|
+
remove it. Immediate rotation and URL changes retire previous signing keys.
|
|
285
|
+
Retire them at the receiver too.
|
|
286
|
+
|
|
287
|
+
To replay, first get the endpoint and show its current URL to the manager. Pass
|
|
288
|
+
its reviewed `destinationRevision` to `redeliver` with the delivery ID. Replay
|
|
289
|
+
requires an enabled endpoint and current authority over its full scope and the
|
|
290
|
+
retained payload. An active run returns a conflict. Replay preserves event ID,
|
|
291
|
+
payload, and original expiry; its retry deadline is at most 72 hours from replay
|
|
292
|
+
and never exceeds that expiry. An expired delivery returns not found.
|
|
293
|
+
|
|
294
|
+
Return 2xx as soon as durable acceptance is complete. Andesine uses a 15-second
|
|
295
|
+
HTTP deadline and retries normal failures within a bounded run. A disabled
|
|
296
|
+
endpoint needs explicit re-enabling. Enabling does not replay past work; inspect
|
|
297
|
+
history and request each needed replay. SDK mutation calls are not automatically
|
|
298
|
+
retried. If a test/replay response is lost, inspect history before repeating it.
|
|
299
|
+
|
|
300
|
+
## Runtime and packaging
|
|
301
|
+
|
|
302
|
+
The verifier is an isolated ESM entry point. It uses Web Crypto (`crypto.subtle`
|
|
303
|
+
HMAC-SHA256), `TextEncoder`, fatal UTF-8 `TextDecoder`, `atob`/`btoa`, and `Headers`.
|
|
304
|
+
The Request helper also needs the Fetch/Streams APIs. Node.js 22+ supplies these
|
|
305
|
+
APIs. Worker runtimes must supply the same APIs; no Node `Buffer`, backend,
|
|
306
|
+
database, network access, runtime schema compilation, or `eval` is required by
|
|
307
|
+
the verifier. This is a runtime requirement, not a claim that every worker
|
|
308
|
+
platform has been tested. Browsers are not a place to store signing secrets.
|
|
309
|
+
|
|
310
|
+
The Rolldown build generates the payload validator from the checked-in OpenAPI
|
|
311
|
+
event schema through a virtual module and bundles it directly into `dist/webhooks.js`.
|
|
312
|
+
No generated validator JavaScript or declarations are stored in `src/generated`.
|
|
313
|
+
Ajv and ajv-formats are build-time dependencies; their required helpers are
|
|
314
|
+
included in the verifier bundle. The build copies their installed license notices
|
|
315
|
+
into `dist/THIRD_PARTY_LICENSES.txt`, which is included in the published package.
|
|
316
|
+
The root SDK entry point does not import the verifier. Regenerate OpenAPI, the
|
|
317
|
+
SDK, and the CLI manifest together when public contracts change.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
ajv
|
|
2
|
+
|
|
3
|
+
The MIT License (MIT)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2015-2021 Evgeny Poberezkin
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
ajv-formats
|
|
28
|
+
|
|
29
|
+
MIT License
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2020 Evgeny Poberezkin
|
|
32
|
+
|
|
33
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
34
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
35
|
+
in the Software without restriction, including without limitation the rights
|
|
36
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
37
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
38
|
+
furnished to do so, subject to the following conditions:
|
|
39
|
+
|
|
40
|
+
The above copyright notice and this permission notice shall be included in all
|
|
41
|
+
copies or substantial portions of the Software.
|
|
42
|
+
|
|
43
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
44
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
45
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
46
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
47
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
48
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
49
|
+
SOFTWARE.
|