@linqapp/sdk 0.25.0 → 0.27.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 +2 -2
- package/README.md +2 -2
- package/api-promise.d.mts +1 -0
- package/api-promise.d.mts.map +1 -1
- package/api-promise.d.ts +1 -0
- package/api-promise.d.ts.map +1 -1
- package/api-promise.mjs +1 -0
- package/api-promise.mjs.map +1 -1
- package/client.d.mts +176 -87
- package/client.d.mts.map +1 -1
- package/client.d.ts +176 -87
- package/client.d.ts.map +1 -1
- package/client.js +167 -85
- package/client.js.map +1 -1
- package/client.mjs +167 -85
- package/client.mjs.map +1 -1
- package/core/api-promise.d.mts.map +1 -1
- package/core/api-promise.d.ts.map +1 -1
- package/core/api-promise.mjs.map +1 -1
- package/core/error.mjs.map +1 -1
- package/core/pagination.d.mts.map +1 -1
- package/core/pagination.d.ts.map +1 -1
- package/core/pagination.mjs.map +1 -1
- package/core/resource.d.mts.map +1 -1
- package/core/resource.d.ts.map +1 -1
- package/core/uploads.d.mts.map +1 -1
- package/core/uploads.d.ts.map +1 -1
- package/core/uploads.mjs.map +1 -1
- package/error.d.mts +1 -0
- package/error.d.mts.map +1 -1
- package/error.d.ts +1 -0
- package/error.d.ts.map +1 -1
- package/error.mjs +1 -0
- package/error.mjs.map +1 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts.map +1 -1
- package/index.mjs.map +1 -1
- package/internal/detect-platform.mjs.map +1 -1
- package/internal/headers.mjs.map +1 -1
- package/internal/parse.d.mts.map +1 -1
- package/internal/parse.d.ts.map +1 -1
- package/internal/parse.mjs.map +1 -1
- package/internal/request-options.d.mts.map +1 -1
- package/internal/request-options.d.ts.map +1 -1
- package/internal/shims.d.mts +6 -0
- package/internal/shims.d.mts.map +1 -1
- package/internal/shims.d.ts +6 -0
- package/internal/shims.d.ts.map +1 -1
- package/internal/to-file.d.mts.map +1 -1
- package/internal/to-file.d.ts.map +1 -1
- package/internal/to-file.mjs.map +1 -1
- package/internal/types.d.mts +4 -4
- package/internal/types.d.mts.map +1 -1
- package/internal/types.d.ts +4 -4
- package/internal/types.d.ts.map +1 -1
- package/internal/uploads.d.mts.map +1 -1
- package/internal/uploads.d.ts.map +1 -1
- package/internal/uploads.mjs.map +1 -1
- package/internal/utils/base64.mjs.map +1 -1
- package/internal/utils/log.d.mts.map +1 -1
- package/internal/utils/log.d.ts.map +1 -1
- package/internal/utils/log.mjs.map +1 -1
- package/internal/utils/path.mjs.map +1 -1
- package/internal/utils/query.mjs.map +1 -1
- package/internal/utils/values.mjs.map +1 -1
- package/internal/utils.d.mts.map +1 -1
- package/internal/utils.d.ts.map +1 -1
- package/internal/utils.mjs.map +1 -1
- package/package.json +4 -2
- package/pagination.d.mts +1 -0
- package/pagination.d.mts.map +1 -1
- package/pagination.d.ts +1 -0
- package/pagination.d.ts.map +1 -1
- package/pagination.mjs +1 -0
- package/pagination.mjs.map +1 -1
- package/resource.d.mts +1 -0
- package/resource.d.mts.map +1 -1
- package/resource.d.ts +1 -0
- package/resource.d.ts.map +1 -1
- package/resource.mjs +1 -0
- package/resource.mjs.map +1 -1
- package/resources/attachments.d.mts.map +1 -1
- package/resources/attachments.d.ts.map +1 -1
- package/resources/attachments.mjs.map +1 -1
- package/resources/capability.d.mts.map +1 -1
- package/resources/capability.d.ts.map +1 -1
- package/resources/capability.mjs.map +1 -1
- package/resources/chats/chats.d.mts +4 -0
- package/resources/chats/chats.d.mts.map +1 -1
- package/resources/chats/chats.d.ts +4 -0
- package/resources/chats/chats.d.ts.map +1 -1
- package/resources/chats/chats.js +4 -0
- package/resources/chats/chats.js.map +1 -1
- package/resources/chats/chats.mjs +4 -0
- package/resources/chats/chats.mjs.map +1 -1
- package/resources/chats/index.d.mts +1 -0
- package/resources/chats/index.d.mts.map +1 -1
- package/resources/chats/index.d.ts +1 -0
- package/resources/chats/index.d.ts.map +1 -1
- package/resources/chats/index.js +3 -1
- package/resources/chats/index.js.map +1 -1
- package/resources/chats/index.mjs +1 -0
- package/resources/chats/index.mjs.map +1 -1
- package/resources/chats/location.d.mts +107 -0
- package/resources/chats/location.d.mts.map +1 -0
- package/resources/chats/location.d.ts +107 -0
- package/resources/chats/location.d.ts.map +1 -0
- package/resources/chats/location.js +68 -0
- package/resources/chats/location.js.map +1 -0
- package/resources/chats/location.mjs +64 -0
- package/resources/chats/location.mjs.map +1 -0
- package/resources/chats/messages.d.mts +3 -2
- package/resources/chats/messages.d.mts.map +1 -1
- package/resources/chats/messages.d.ts +3 -2
- package/resources/chats/messages.d.ts.map +1 -1
- package/resources/chats/messages.mjs.map +1 -1
- package/resources/chats/participants.d.mts.map +1 -1
- package/resources/chats/participants.d.ts.map +1 -1
- package/resources/chats/participants.mjs.map +1 -1
- package/resources/chats/typing.d.mts.map +1 -1
- package/resources/chats/typing.d.ts.map +1 -1
- package/resources/chats/typing.mjs.map +1 -1
- package/resources/chats.d.mts.map +1 -1
- package/resources/chats.d.ts.map +1 -1
- package/resources/chats.mjs.map +1 -1
- package/resources/contact-card.d.mts.map +1 -1
- package/resources/contact-card.d.ts.map +1 -1
- package/resources/contact-card.mjs.map +1 -1
- package/resources/index.d.mts +1 -1
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +1 -1
- package/resources/index.d.ts.map +1 -1
- package/resources/index.mjs.map +1 -1
- package/resources/messages.d.mts +9 -2
- package/resources/messages.d.mts.map +1 -1
- package/resources/messages.d.ts +9 -2
- package/resources/messages.d.ts.map +1 -1
- package/resources/messages.mjs.map +1 -1
- package/resources/phone-numbers.d.mts.map +1 -1
- package/resources/phone-numbers.d.ts.map +1 -1
- package/resources/phone-numbers.mjs.map +1 -1
- package/resources/phonenumbers.d.mts.map +1 -1
- package/resources/phonenumbers.d.ts.map +1 -1
- package/resources/phonenumbers.mjs.map +1 -1
- package/resources/webhook-events.d.mts +83 -44
- package/resources/webhook-events.d.mts.map +1 -1
- package/resources/webhook-events.d.ts +83 -44
- package/resources/webhook-events.d.ts.map +1 -1
- package/resources/webhook-events.js +81 -42
- package/resources/webhook-events.js.map +1 -1
- package/resources/webhook-events.mjs +81 -42
- package/resources/webhook-events.mjs.map +1 -1
- package/resources/webhook-subscriptions.d.mts +89 -44
- package/resources/webhook-subscriptions.d.mts.map +1 -1
- package/resources/webhook-subscriptions.d.ts +89 -44
- package/resources/webhook-subscriptions.d.ts.map +1 -1
- package/resources/webhook-subscriptions.js +90 -45
- package/resources/webhook-subscriptions.js.map +1 -1
- package/resources/webhook-subscriptions.mjs +90 -45
- package/resources/webhook-subscriptions.mjs.map +1 -1
- package/resources/webhooks.d.mts +10 -7
- package/resources/webhooks.d.mts.map +1 -1
- package/resources/webhooks.d.ts +10 -7
- package/resources/webhooks.d.ts.map +1 -1
- package/resources/webhooks.js +9 -1
- package/resources/webhooks.js.map +1 -1
- package/resources/webhooks.mjs +9 -1
- package/resources/webhooks.mjs.map +1 -1
- package/resources.d.mts.map +1 -1
- package/resources.d.ts.map +1 -1
- package/resources.mjs.map +1 -1
- package/src/client.ts +180 -86
- package/src/resources/chats/chats.ts +10 -0
- package/src/resources/chats/index.ts +1 -0
- package/src/resources/chats/location.ts +138 -0
- package/src/resources/chats/messages.ts +3 -2
- package/src/resources/index.ts +1 -1
- package/src/resources/messages.ts +10 -2
- package/src/resources/webhook-events.ts +85 -44
- package/src/resources/webhook-subscriptions.ts +89 -44
- package/src/resources/webhooks.ts +20 -8
- package/src/version.ts +1 -1
- package/uploads.d.mts +1 -0
- package/uploads.d.mts.map +1 -1
- package/uploads.d.ts +1 -0
- package/uploads.d.ts.map +1 -1
- package/uploads.mjs +1 -0
- package/uploads.mjs.map +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/client.ts
CHANGED
|
@@ -77,7 +77,6 @@ import {
|
|
|
77
77
|
ChatGroupNameUpdatedWebhookEvent,
|
|
78
78
|
ChatTypingIndicatorStartedWebhookEvent,
|
|
79
79
|
ChatTypingIndicatorStoppedWebhookEvent,
|
|
80
|
-
EventsWebhookEvent,
|
|
81
80
|
MessageDeliveredWebhookEvent,
|
|
82
81
|
MessageEditedWebhookEvent,
|
|
83
82
|
MessageEventV2,
|
|
@@ -95,6 +94,7 @@ import {
|
|
|
95
94
|
SchemasMediaPartResponse,
|
|
96
95
|
SchemasMessageEffect,
|
|
97
96
|
SchemasTextPartResponse,
|
|
97
|
+
UnwrapWebhookEvent,
|
|
98
98
|
Webhooks,
|
|
99
99
|
} from './resources/webhooks';
|
|
100
100
|
import {
|
|
@@ -136,6 +136,16 @@ export interface ClientOptions {
|
|
|
136
136
|
*/
|
|
137
137
|
apiKey?: string | undefined;
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Webhook signing secret used by `client.webhooks.unwrap()` to verify the
|
|
141
|
+
* Standard Webhooks signature on incoming webhook requests.
|
|
142
|
+
*
|
|
143
|
+
* Format: a base64-encoded key, optionally with a `whsec_` prefix
|
|
144
|
+
* (e.g. `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
|
|
145
|
+
*
|
|
146
|
+
*/
|
|
147
|
+
webhookSecret?: string | null | undefined;
|
|
148
|
+
|
|
139
149
|
/**
|
|
140
150
|
* Override the default base URL for the API, e.g., "https://api.example.com/v2/"
|
|
141
151
|
*
|
|
@@ -210,6 +220,7 @@ export interface ClientOptions {
|
|
|
210
220
|
*/
|
|
211
221
|
export class LinqAPIV3 {
|
|
212
222
|
apiKey: string;
|
|
223
|
+
webhookSecret: string | null;
|
|
213
224
|
|
|
214
225
|
baseURL: string;
|
|
215
226
|
maxRetries: number;
|
|
@@ -227,6 +238,7 @@ export class LinqAPIV3 {
|
|
|
227
238
|
* API Client for interfacing with the Linq API V3 API.
|
|
228
239
|
*
|
|
229
240
|
* @param {string | undefined} [opts.apiKey=process.env['LINQ_API_V3_API_KEY'] ?? undefined]
|
|
241
|
+
* @param {string | null | undefined} [opts.webhookSecret=process.env['LINQ_WEBHOOK_SECRET'] ?? null]
|
|
230
242
|
* @param {string} [opts.baseURL=process.env['LINQ_API_V3_BASE_URL'] ?? https://api.linqapp.com/api/partner] - Override the default base URL for the API.
|
|
231
243
|
* @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
|
|
232
244
|
* @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
|
|
@@ -238,6 +250,7 @@ export class LinqAPIV3 {
|
|
|
238
250
|
constructor({
|
|
239
251
|
baseURL = readEnv('LINQ_API_V3_BASE_URL'),
|
|
240
252
|
apiKey = readEnv('LINQ_API_V3_API_KEY'),
|
|
253
|
+
webhookSecret = readEnv('LINQ_WEBHOOK_SECRET') ?? null,
|
|
241
254
|
...opts
|
|
242
255
|
}: ClientOptions = {}) {
|
|
243
256
|
if (apiKey === undefined) {
|
|
@@ -248,6 +261,7 @@ export class LinqAPIV3 {
|
|
|
248
261
|
|
|
249
262
|
const options: ClientOptions = {
|
|
250
263
|
apiKey,
|
|
264
|
+
webhookSecret,
|
|
251
265
|
...opts,
|
|
252
266
|
baseURL: baseURL || `https://api.linqapp.com/api/partner`,
|
|
253
267
|
};
|
|
@@ -282,6 +296,7 @@ export class LinqAPIV3 {
|
|
|
282
296
|
this._options = options;
|
|
283
297
|
|
|
284
298
|
this.apiKey = apiKey;
|
|
299
|
+
this.webhookSecret = webhookSecret;
|
|
285
300
|
}
|
|
286
301
|
|
|
287
302
|
/**
|
|
@@ -298,6 +313,7 @@ export class LinqAPIV3 {
|
|
|
298
313
|
fetch: this.fetch,
|
|
299
314
|
fetchOptions: this.fetchOptions,
|
|
300
315
|
apiKey: this.apiKey,
|
|
316
|
+
webhookSecret: this.webhookSecret,
|
|
301
317
|
...options,
|
|
302
318
|
});
|
|
303
319
|
return client;
|
|
@@ -1093,55 +1109,82 @@ export class LinqAPIV3 {
|
|
|
1093
1109
|
*
|
|
1094
1110
|
* ## Webhook Headers
|
|
1095
1111
|
*
|
|
1096
|
-
*
|
|
1112
|
+
* All webhook requests include two sets of headers. **If you have an existing integration
|
|
1113
|
+
* using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
|
|
1114
|
+
* every delivery and work exactly as before. The new `webhook-*` headers follow the
|
|
1115
|
+
* [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
|
|
1116
|
+
* You can safely ignore them if your current verification code works and you don't want to use this convention.
|
|
1117
|
+
*
|
|
1118
|
+
* ### Standard Webhooks Headers (Recommended)
|
|
1119
|
+
*
|
|
1120
|
+
* Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
|
|
1097
1121
|
*
|
|
1098
1122
|
* | Header | Description |
|
|
1099
1123
|
* |--------|-------------|
|
|
1100
|
-
* | `
|
|
1101
|
-
* | `
|
|
1102
|
-
* | `
|
|
1103
|
-
* | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
|
|
1124
|
+
* | `webhook-id` | Unique event identifier (use as idempotency key) |
|
|
1125
|
+
* | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
|
|
1126
|
+
* | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
|
|
1104
1127
|
*
|
|
1105
|
-
*
|
|
1128
|
+
* ### Legacy Headers (Deprecated)
|
|
1106
1129
|
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1130
|
+
* Still sent on every delivery for backwards compatibility. Existing verification code
|
|
1131
|
+
* using these headers continues to work — no changes required.
|
|
1109
1132
|
*
|
|
1110
|
-
*
|
|
1133
|
+
* | Header | Description |
|
|
1134
|
+
* |--------|-------------|
|
|
1135
|
+
* | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
|
|
1136
|
+
* | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
|
|
1137
|
+
* | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
|
|
1138
|
+
* | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
|
|
1111
1139
|
*
|
|
1112
|
-
*
|
|
1140
|
+
* ## Signing Secrets
|
|
1113
1141
|
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1142
|
+
* Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
|
|
1143
|
+
* by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
|
|
1144
|
+
*
|
|
1145
|
+
* Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
|
|
1146
|
+
*
|
|
1147
|
+
* ## Verifying Webhook Signatures
|
|
1117
1148
|
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1149
|
+
* Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
|
|
1150
|
+
* You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
|
|
1151
|
+
* signatures, or implement verification manually:
|
|
1152
|
+
*
|
|
1153
|
+
* **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
|
|
1121
1154
|
*
|
|
1122
1155
|
* **Verification Steps:**
|
|
1123
1156
|
*
|
|
1124
|
-
* 1. Extract the `
|
|
1125
|
-
* 2.
|
|
1126
|
-
* 3.
|
|
1127
|
-
* 4.
|
|
1128
|
-
* 5.
|
|
1129
|
-
* 6.
|
|
1157
|
+
* 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
|
|
1158
|
+
* 2. Reject if the timestamp is more than 5 minutes old (replay protection)
|
|
1159
|
+
* 3. Get the raw request body bytes (do not parse and re-serialize)
|
|
1160
|
+
* 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
|
|
1161
|
+
* 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
|
|
1162
|
+
* 6. Compute HMAC-SHA256 using the key bytes over the signed content
|
|
1163
|
+
* 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
|
|
1164
|
+
* 8. Use constant-time comparison to prevent timing attacks
|
|
1130
1165
|
*
|
|
1131
1166
|
* **Example (Python):**
|
|
1132
1167
|
*
|
|
1133
1168
|
* ```python
|
|
1134
|
-
* import hmac
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1169
|
+
* import base64, hmac, hashlib
|
|
1170
|
+
*
|
|
1171
|
+
* def verify_webhook(secret, body, headers):
|
|
1172
|
+
* msg_id = headers['webhook-id']
|
|
1173
|
+
* timestamp = headers['webhook-timestamp']
|
|
1174
|
+
* signature = headers['webhook-signature']
|
|
1175
|
+
*
|
|
1176
|
+
* secret_str = secret.removeprefix('whsec_')
|
|
1177
|
+
* key = base64.b64decode(secret_str)
|
|
1178
|
+
*
|
|
1179
|
+
* signed_content = f"{msg_id}.{timestamp}.{body}"
|
|
1180
|
+
* expected = base64.b64encode(
|
|
1181
|
+
* hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
|
|
1182
|
+
* ).decode()
|
|
1183
|
+
*
|
|
1184
|
+
* for sig in signature.split(' '):
|
|
1185
|
+
* if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
|
|
1186
|
+
* return True
|
|
1187
|
+
* return False
|
|
1145
1188
|
* ```
|
|
1146
1189
|
*
|
|
1147
1190
|
* **Example (Node.js):**
|
|
@@ -1149,16 +1192,28 @@ export class LinqAPIV3 {
|
|
|
1149
1192
|
* ```javascript
|
|
1150
1193
|
* const crypto = require('crypto');
|
|
1151
1194
|
*
|
|
1152
|
-
* function verifyWebhook(
|
|
1153
|
-
* const
|
|
1195
|
+
* function verifyWebhook(secret, rawBody, headers) {
|
|
1196
|
+
* const msgId = headers['webhook-id'];
|
|
1197
|
+
* const timestamp = headers['webhook-timestamp'];
|
|
1198
|
+
* const signature = headers['webhook-signature'];
|
|
1199
|
+
*
|
|
1200
|
+
* const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
|
|
1201
|
+
* const keyBytes = Buffer.from(secretStr, 'base64');
|
|
1202
|
+
* const signedContent = `${msgId}.${timestamp}.${rawBody}`;
|
|
1154
1203
|
* const expected = crypto
|
|
1155
|
-
* .createHmac('sha256',
|
|
1156
|
-
* .update(
|
|
1157
|
-
* .digest('
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1204
|
+
* .createHmac('sha256', keyBytes)
|
|
1205
|
+
* .update(signedContent)
|
|
1206
|
+
* .digest('base64');
|
|
1207
|
+
*
|
|
1208
|
+
* return signature.split(' ').some(sig => {
|
|
1209
|
+
* if (!sig.startsWith('v1,')) return false;
|
|
1210
|
+
* try {
|
|
1211
|
+
* return crypto.timingSafeEqual(
|
|
1212
|
+
* Buffer.from(expected, 'base64'),
|
|
1213
|
+
* Buffer.from(sig.slice(3), 'base64')
|
|
1214
|
+
* );
|
|
1215
|
+
* } catch { return false; }
|
|
1216
|
+
* });
|
|
1162
1217
|
* }
|
|
1163
1218
|
* ```
|
|
1164
1219
|
*
|
|
@@ -1184,55 +1239,82 @@ export class LinqAPIV3 {
|
|
|
1184
1239
|
*
|
|
1185
1240
|
* ## Webhook Headers
|
|
1186
1241
|
*
|
|
1187
|
-
*
|
|
1242
|
+
* All webhook requests include two sets of headers. **If you have an existing integration
|
|
1243
|
+
* using the `X-Webhook-*` headers, nothing changes** — those headers are still sent on
|
|
1244
|
+
* every delivery and work exactly as before. The new `webhook-*` headers follow the
|
|
1245
|
+
* [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification.
|
|
1246
|
+
* You can safely ignore them if your current verification code works and you don't want to use this convention.
|
|
1247
|
+
*
|
|
1248
|
+
* ### Standard Webhooks Headers (Recommended)
|
|
1249
|
+
*
|
|
1250
|
+
* Used by [our SDK](https://github.com/linq-team/linq-node) and any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks).
|
|
1188
1251
|
*
|
|
1189
1252
|
* | Header | Description |
|
|
1190
1253
|
* |--------|-------------|
|
|
1191
|
-
* | `
|
|
1192
|
-
* | `
|
|
1193
|
-
* | `
|
|
1194
|
-
* | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
|
|
1254
|
+
* | `webhook-id` | Unique event identifier (use as idempotency key) |
|
|
1255
|
+
* | `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent |
|
|
1256
|
+
* | `webhook-signature` | Standard Webhooks signature (`v1,{base64}` format) |
|
|
1195
1257
|
*
|
|
1196
|
-
*
|
|
1258
|
+
* ### Legacy Headers (Deprecated)
|
|
1197
1259
|
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1260
|
+
* Still sent on every delivery for backwards compatibility. Existing verification code
|
|
1261
|
+
* using these headers continues to work — no changes required.
|
|
1200
1262
|
*
|
|
1201
|
-
*
|
|
1263
|
+
* | Header | Description |
|
|
1264
|
+
* |--------|-------------|
|
|
1265
|
+
* | `X-Webhook-Event` | *(deprecated)* Event type (e.g., `message.sent`) |
|
|
1266
|
+
* | `X-Webhook-Subscription-ID` | *(deprecated)* Webhook subscription ID |
|
|
1267
|
+
* | `X-Webhook-Timestamp` | *(deprecated)* Unix timestamp (seconds) |
|
|
1268
|
+
* | `X-Webhook-Signature` | *(deprecated)* HMAC-SHA256 signature (hex-encoded) |
|
|
1202
1269
|
*
|
|
1203
|
-
*
|
|
1270
|
+
* ## Signing Secrets
|
|
1204
1271
|
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
* ```
|
|
1272
|
+
* Signing secrets use the Standard Webhooks format: a `whsec_` prefix followed
|
|
1273
|
+
* by base64-encoded random bytes (e.g., `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw7Jxx2Oll+OE=`).
|
|
1208
1274
|
*
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1275
|
+
* Strip the `whsec_` prefix and base64-decode the remainder to get the raw key bytes.
|
|
1276
|
+
*
|
|
1277
|
+
* ## Verifying Webhook Signatures
|
|
1278
|
+
*
|
|
1279
|
+
* Webhooks are signed following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks).
|
|
1280
|
+
* You can use any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) to verify
|
|
1281
|
+
* signatures, or implement verification manually:
|
|
1282
|
+
*
|
|
1283
|
+
* **Signed content:** `{webhook-id}.{webhook-timestamp}.{body}`
|
|
1212
1284
|
*
|
|
1213
1285
|
* **Verification Steps:**
|
|
1214
1286
|
*
|
|
1215
|
-
* 1. Extract the `
|
|
1216
|
-
* 2.
|
|
1217
|
-
* 3.
|
|
1218
|
-
* 4.
|
|
1219
|
-
* 5.
|
|
1220
|
-
* 6.
|
|
1287
|
+
* 1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
|
|
1288
|
+
* 2. Reject if the timestamp is more than 5 minutes old (replay protection)
|
|
1289
|
+
* 3. Get the raw request body bytes (do not parse and re-serialize)
|
|
1290
|
+
* 4. Construct signed content: `"{webhook-id}.{webhook-timestamp}.{body}"`
|
|
1291
|
+
* 5. Strip the `whsec_` prefix from your secret and base64-decode to get key bytes
|
|
1292
|
+
* 6. Compute HMAC-SHA256 using the key bytes over the signed content
|
|
1293
|
+
* 7. Base64-encode the result and compare with the value after `v1,` in `webhook-signature`
|
|
1294
|
+
* 8. Use constant-time comparison to prevent timing attacks
|
|
1221
1295
|
*
|
|
1222
1296
|
* **Example (Python):**
|
|
1223
1297
|
*
|
|
1224
1298
|
* ```python
|
|
1225
|
-
* import hmac
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1299
|
+
* import base64, hmac, hashlib
|
|
1300
|
+
*
|
|
1301
|
+
* def verify_webhook(secret, body, headers):
|
|
1302
|
+
* msg_id = headers['webhook-id']
|
|
1303
|
+
* timestamp = headers['webhook-timestamp']
|
|
1304
|
+
* signature = headers['webhook-signature']
|
|
1305
|
+
*
|
|
1306
|
+
* secret_str = secret.removeprefix('whsec_')
|
|
1307
|
+
* key = base64.b64decode(secret_str)
|
|
1308
|
+
*
|
|
1309
|
+
* signed_content = f"{msg_id}.{timestamp}.{body}"
|
|
1310
|
+
* expected = base64.b64encode(
|
|
1311
|
+
* hmac.new(key, signed_content.encode(), hashlib.sha256).digest()
|
|
1312
|
+
* ).decode()
|
|
1313
|
+
*
|
|
1314
|
+
* for sig in signature.split(' '):
|
|
1315
|
+
* if sig.startswith('v1,') and hmac.compare_digest(expected, sig[3:]):
|
|
1316
|
+
* return True
|
|
1317
|
+
* return False
|
|
1236
1318
|
* ```
|
|
1237
1319
|
*
|
|
1238
1320
|
* **Example (Node.js):**
|
|
@@ -1240,16 +1322,28 @@ export class LinqAPIV3 {
|
|
|
1240
1322
|
* ```javascript
|
|
1241
1323
|
* const crypto = require('crypto');
|
|
1242
1324
|
*
|
|
1243
|
-
* function verifyWebhook(
|
|
1244
|
-
* const
|
|
1325
|
+
* function verifyWebhook(secret, rawBody, headers) {
|
|
1326
|
+
* const msgId = headers['webhook-id'];
|
|
1327
|
+
* const timestamp = headers['webhook-timestamp'];
|
|
1328
|
+
* const signature = headers['webhook-signature'];
|
|
1329
|
+
*
|
|
1330
|
+
* const secretStr = secret.startsWith('whsec_') ? secret.slice(6) : secret;
|
|
1331
|
+
* const keyBytes = Buffer.from(secretStr, 'base64');
|
|
1332
|
+
* const signedContent = `${msgId}.${timestamp}.${rawBody}`;
|
|
1245
1333
|
* const expected = crypto
|
|
1246
|
-
* .createHmac('sha256',
|
|
1247
|
-
* .update(
|
|
1248
|
-
* .digest('
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1334
|
+
* .createHmac('sha256', keyBytes)
|
|
1335
|
+
* .update(signedContent)
|
|
1336
|
+
* .digest('base64');
|
|
1337
|
+
*
|
|
1338
|
+
* return signature.split(' ').some(sig => {
|
|
1339
|
+
* if (!sig.startsWith('v1,')) return false;
|
|
1340
|
+
* try {
|
|
1341
|
+
* return crypto.timingSafeEqual(
|
|
1342
|
+
* Buffer.from(expected, 'base64'),
|
|
1343
|
+
* Buffer.from(sig.slice(3), 'base64')
|
|
1344
|
+
* );
|
|
1345
|
+
* } catch { return false; }
|
|
1346
|
+
* });
|
|
1253
1347
|
* }
|
|
1254
1348
|
* ```
|
|
1255
1349
|
*
|
|
@@ -1398,7 +1492,7 @@ export declare namespace LinqAPIV3 {
|
|
|
1398
1492
|
type ChatTypingIndicatorStartedWebhookEvent as ChatTypingIndicatorStartedWebhookEvent,
|
|
1399
1493
|
type ChatTypingIndicatorStoppedWebhookEvent as ChatTypingIndicatorStoppedWebhookEvent,
|
|
1400
1494
|
type PhoneNumberStatusUpdatedWebhookEvent as PhoneNumberStatusUpdatedWebhookEvent,
|
|
1401
|
-
type
|
|
1495
|
+
type UnwrapWebhookEvent as UnwrapWebhookEvent,
|
|
1402
1496
|
};
|
|
1403
1497
|
|
|
1404
1498
|
export {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { APIResource } from '../../core/resource';
|
|
4
4
|
import * as MessagesAPI from '../messages';
|
|
5
5
|
import * as Shared from '../shared';
|
|
6
|
+
import * as LocationAPI from './location';
|
|
7
|
+
import { GetChatLocationResponse, Location, LocationRequestResponse } from './location';
|
|
6
8
|
import * as ChatsMessagesAPI from './messages';
|
|
7
9
|
import { MessageListParams, MessageSendParams, MessageSendResponse, Messages, SentMessage } from './messages';
|
|
8
10
|
import * as ParticipantsAPI from './participants';
|
|
@@ -25,6 +27,7 @@ export class Chats extends APIResource {
|
|
|
25
27
|
participants: ParticipantsAPI.Participants = new ParticipantsAPI.Participants(this._client);
|
|
26
28
|
typing: TypingAPI.Typing = new TypingAPI.Typing(this._client);
|
|
27
29
|
messages: ChatsMessagesAPI.Messages = new ChatsMessagesAPI.Messages(this._client);
|
|
30
|
+
location: LocationAPI.Location = new LocationAPI.Location(this._client);
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Create a new chat with specified participants and send an initial message. The
|
|
@@ -817,6 +820,7 @@ export interface ChatSendVoicememoParams {
|
|
|
817
820
|
Chats.Participants = Participants;
|
|
818
821
|
Chats.Typing = Typing;
|
|
819
822
|
Chats.Messages = Messages;
|
|
823
|
+
Chats.Location = Location;
|
|
820
824
|
|
|
821
825
|
export declare namespace Chats {
|
|
822
826
|
export {
|
|
@@ -853,4 +857,10 @@ export declare namespace Chats {
|
|
|
853
857
|
type MessageSendParams as MessageSendParams,
|
|
854
858
|
type MessageListParams as MessageListParams,
|
|
855
859
|
};
|
|
860
|
+
|
|
861
|
+
export {
|
|
862
|
+
Location as Location,
|
|
863
|
+
type GetChatLocationResponse as GetChatLocationResponse,
|
|
864
|
+
type LocationRequestResponse as LocationRequestResponse,
|
|
865
|
+
};
|
|
856
866
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
|
+
|
|
3
|
+
import { APIResource } from '../../core/resource';
|
|
4
|
+
import { APIPromise } from '../../core/api-promise';
|
|
5
|
+
import { buildHeaders } from '../../internal/headers';
|
|
6
|
+
import { RequestOptions } from '../../internal/request-options';
|
|
7
|
+
import { path } from '../../internal/utils/path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Request and retrieve real-time location data via iMessage.
|
|
11
|
+
*
|
|
12
|
+
* Use these endpoints to request a contact's location, retrieve location data
|
|
13
|
+
* for contacts who are sharing with you, and subscribe to webhooks when someone
|
|
14
|
+
* starts or stops sharing their location.
|
|
15
|
+
*
|
|
16
|
+
* **Coordinates** are returned in [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) format:
|
|
17
|
+
* `[longitude, latitude]` or `[longitude, latitude, altitude]` if altitude is available.
|
|
18
|
+
*/
|
|
19
|
+
export class Location extends APIResource {
|
|
20
|
+
/**
|
|
21
|
+
* Send a location sharing request to a contact. They will receive an iMessage
|
|
22
|
+
* prompt asking them to share their location.
|
|
23
|
+
*
|
|
24
|
+
* Location requests only work in **1:1 iMessage chats** (Apple limitation).
|
|
25
|
+
* Attempting to request location in a group chat, or in an SMS or RCS chat,
|
|
26
|
+
* returns `409` (Operation not supported on this chat's service type).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const locationRequestResponse =
|
|
31
|
+
* await client.chats.location.request(
|
|
32
|
+
* '975d0776-bd17-4273-8337-f346b4c661b0',
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
request(chatID: string, options?: RequestOptions): APIPromise<LocationRequestResponse> {
|
|
37
|
+
return this._client.post(path`/v3/chats/${chatID}/location/request`, options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Retrieve the current location for contacts sharing with you in a chat.
|
|
42
|
+
*
|
|
43
|
+
* Returns a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946)
|
|
44
|
+
* `FeatureCollection` with a `Feature` for each participant actively sharing their
|
|
45
|
+
* location.
|
|
46
|
+
*
|
|
47
|
+
* Works for both 1:1 and group chats. In group chats, returns a separate feature
|
|
48
|
+
* for each participant who is sharing. Each feature's `properties.handle`
|
|
49
|
+
* identifies the user.
|
|
50
|
+
*
|
|
51
|
+
* Returns an empty `features` array if no one is sharing or no location data is
|
|
52
|
+
* available yet.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const getChatLocationResponse =
|
|
57
|
+
* await client.chats.location.retrieve(
|
|
58
|
+
* '975d0776-bd17-4273-8337-f346b4c661b0',
|
|
59
|
+
* );
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
retrieve(chatID: string, options?: RequestOptions): APIPromise<GetChatLocationResponse> {
|
|
63
|
+
return this._client.get(path`/v3/chats/${chatID}/location`, {
|
|
64
|
+
...options,
|
|
65
|
+
headers: buildHeaders([{ Accept: 'application/geo+json' }, options?.headers]),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GetChatLocationResponse {
|
|
71
|
+
data: GetChatLocationResponse.Data;
|
|
72
|
+
|
|
73
|
+
success: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export namespace GetChatLocationResponse {
|
|
77
|
+
export interface Data {
|
|
78
|
+
features: Array<Data.Feature>;
|
|
79
|
+
|
|
80
|
+
type: 'FeatureCollection';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export namespace Data {
|
|
84
|
+
export interface Feature {
|
|
85
|
+
geometry: Feature.Geometry;
|
|
86
|
+
|
|
87
|
+
properties: Feature.Properties;
|
|
88
|
+
|
|
89
|
+
type: 'Feature';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export namespace Feature {
|
|
93
|
+
export interface Geometry {
|
|
94
|
+
/**
|
|
95
|
+
* [longitude, latitude] or [longitude, latitude, altitude]
|
|
96
|
+
*/
|
|
97
|
+
coordinates: Array<number>;
|
|
98
|
+
|
|
99
|
+
type: 'Point';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface Properties {
|
|
103
|
+
/**
|
|
104
|
+
* Phone number or email of the person sharing their location
|
|
105
|
+
*/
|
|
106
|
+
handle: string;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Full street address
|
|
110
|
+
*/
|
|
111
|
+
address?: string;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* City or locality name
|
|
115
|
+
*/
|
|
116
|
+
locality?: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* When the location was last updated
|
|
120
|
+
*/
|
|
121
|
+
updated_at?: string;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface LocationRequestResponse {
|
|
128
|
+
message: string;
|
|
129
|
+
|
|
130
|
+
success: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export declare namespace Location {
|
|
134
|
+
export {
|
|
135
|
+
type GetChatLocationResponse as GetChatLocationResponse,
|
|
136
|
+
type LocationRequestResponse as LocationRequestResponse,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -138,10 +138,11 @@ export interface SentMessage {
|
|
|
138
138
|
/**
|
|
139
139
|
* Current delivery status of a message
|
|
140
140
|
*/
|
|
141
|
-
delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'failed';
|
|
141
|
+
delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed';
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
144
|
+
* @deprecated DEPRECATED: Use `delivery_status == "read"` instead. Whether the
|
|
145
|
+
* message has been read.
|
|
145
146
|
*/
|
|
146
147
|
is_read: boolean;
|
|
147
148
|
|
package/src/resources/index.ts
CHANGED
|
@@ -159,7 +159,14 @@ export interface Message {
|
|
|
159
159
|
created_at: string;
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
162
|
+
* Current delivery status of a message
|
|
163
|
+
*/
|
|
164
|
+
delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @deprecated DEPRECATED: Use `delivery_status` instead (true when
|
|
168
|
+
* `delivery_status` is `delivered` or `read`). Whether the message has been
|
|
169
|
+
* delivered.
|
|
163
170
|
*/
|
|
164
171
|
is_delivered: boolean;
|
|
165
172
|
|
|
@@ -169,7 +176,8 @@ export interface Message {
|
|
|
169
176
|
is_from_me: boolean;
|
|
170
177
|
|
|
171
178
|
/**
|
|
172
|
-
*
|
|
179
|
+
* @deprecated DEPRECATED: Use `delivery_status == "read"` instead. Whether the
|
|
180
|
+
* message has been read.
|
|
173
181
|
*/
|
|
174
182
|
is_read: boolean;
|
|
175
183
|
|