@doist/twist-sdk 2.8.1 → 2.9.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.
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __generator = (this && this.__generator) || function (thisArg, body) {
27
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
28
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
29
+ function verb(n) { return function (v) { return step([n, v]); }; }
30
+ function step(op) {
31
+ if (f) throw new TypeError("Generator is already executing.");
32
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
33
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
34
+ if (y = 0, t) op = [op[0] & 2, t.value];
35
+ switch (op[0]) {
36
+ case 0: case 1: t = op; break;
37
+ case 4: _.label++; return { value: op[1], done: false };
38
+ case 5: _.label++; y = op[1]; op = [0]; continue;
39
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
40
+ default:
41
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
42
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
43
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
44
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
45
+ if (t[2]) _.ops.pop();
46
+ _.trys.pop(); continue;
47
+ }
48
+ op = body.call(thisArg, _);
49
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
50
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
+ }
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.AttachmentsClient = void 0;
55
+ var uuid_1 = require("uuid");
56
+ var endpoints_1 = require("../consts/endpoints");
57
+ var entities_1 = require("../types/entities");
58
+ var multipart_upload_1 = require("../utils/multipart-upload");
59
+ var base_client_1 = require("./base-client");
60
+ /**
61
+ * Client for uploading file attachments to Twist.
62
+ *
63
+ * Attachments are uploaded independently, then referenced by passing the returned
64
+ * {@link Attachment} into the `attachments` array of `comments.createComment`,
65
+ * `conversationMessages.createMessage`, and similar calls.
66
+ */
67
+ var AttachmentsClient = /** @class */ (function (_super) {
68
+ __extends(AttachmentsClient, _super);
69
+ function AttachmentsClient() {
70
+ return _super !== null && _super.apply(this, arguments) || this;
71
+ }
72
+ /**
73
+ * Uploads a file and returns the created {@link Attachment}.
74
+ *
75
+ * Mirrors the canonical multipart upload used by twist-web: `POST /attachments/upload`
76
+ * with the `file` binary plus `file_name`, `file_size`, `attachment_id`, and
77
+ * `underlying_type` form fields.
78
+ *
79
+ * @param args - The file to upload and optional metadata.
80
+ * @returns The created attachment, ready to attach to a comment or message.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * import { readFile } from 'node:fs/promises'
85
+ *
86
+ * const attachment = await api.attachments.upload({
87
+ * file: await readFile('./diagram.png'),
88
+ * fileName: 'diagram.png',
89
+ * })
90
+ *
91
+ * await api.comments.createComment({
92
+ * threadId: 789,
93
+ * content: 'See attached',
94
+ * attachments: [attachment],
95
+ * })
96
+ * ```
97
+ */
98
+ AttachmentsClient.prototype.upload = function (args) {
99
+ return __awaiter(this, void 0, void 0, function () {
100
+ var data;
101
+ return __generator(this, function (_a) {
102
+ switch (_a.label) {
103
+ case 0: return [4 /*yield*/, (0, multipart_upload_1.uploadMultipartFile)({
104
+ baseUrl: this.getBaseUri(),
105
+ authToken: this.apiToken,
106
+ endpoint: "".concat(endpoints_1.ENDPOINT_ATTACHMENTS, "/upload"),
107
+ file: args.file,
108
+ fileName: args.fileName,
109
+ contentType: args.contentType,
110
+ additionalFields: {
111
+ attachment_id: args.attachmentId || (0, uuid_1.v4)(),
112
+ },
113
+ customFetch: this.customFetch,
114
+ })];
115
+ case 1:
116
+ data = _a.sent();
117
+ return [2 /*return*/, entities_1.AttachmentSchema.parse(data)];
118
+ }
119
+ });
120
+ });
121
+ };
122
+ return AttachmentsClient;
123
+ }(base_client_1.BaseClient));
124
+ exports.AttachmentsClient = AttachmentsClient;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ENDPOINT_CONVERSATION_MESSAGES = exports.ENDPOINT_SEARCH = exports.ENDPOINT_REACTIONS = exports.ENDPOINT_INBOX = exports.ENDPOINT_NOTIFICATIONS = exports.ENDPOINT_COMMENTS = exports.ENDPOINT_CONVERSATIONS = exports.ENDPOINT_GROUPS = exports.ENDPOINT_THREADS = exports.ENDPOINT_CHANNELS = exports.ENDPOINT_WORKSPACES = exports.ENDPOINT_USERS = exports.API_BASE_URI = exports.API_VERSION = void 0;
3
+ exports.ENDPOINT_ATTACHMENTS = exports.ENDPOINT_CONVERSATION_MESSAGES = exports.ENDPOINT_SEARCH = exports.ENDPOINT_REACTIONS = exports.ENDPOINT_INBOX = exports.ENDPOINT_NOTIFICATIONS = exports.ENDPOINT_COMMENTS = exports.ENDPOINT_CONVERSATIONS = exports.ENDPOINT_GROUPS = exports.ENDPOINT_THREADS = exports.ENDPOINT_CHANNELS = exports.ENDPOINT_WORKSPACES = exports.ENDPOINT_USERS = exports.API_BASE_URI = exports.API_VERSION = void 0;
4
4
  exports.getTwistBaseUri = getTwistBaseUri;
5
5
  var api_version_1 = require("../types/api-version");
6
6
  var BASE_URI = 'https://api.twist.com';
@@ -33,3 +33,4 @@ exports.ENDPOINT_INBOX = 'inbox';
33
33
  exports.ENDPOINT_REACTIONS = 'reactions';
34
34
  exports.ENDPOINT_SEARCH = 'search';
35
35
  exports.ENDPOINT_CONVERSATION_MESSAGES = 'conversation_messages';
36
+ exports.ENDPOINT_ATTACHMENTS = 'attachments';
package/dist/cjs/index.js CHANGED
@@ -14,10 +14,12 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.TwistApi = exports.SearchClient = exports.ReactionsClient = exports.InboxClient = exports.ConversationMessagesClient = exports.BatchBuilder = void 0;
17
+ exports.TwistApi = exports.SearchClient = exports.ReactionsClient = exports.InboxClient = exports.ConversationMessagesClient = exports.AttachmentsClient = exports.BatchBuilder = void 0;
18
18
  __exportStar(require("./authentication"), exports);
19
19
  var batch_builder_1 = require("./batch-builder");
20
20
  Object.defineProperty(exports, "BatchBuilder", { enumerable: true, get: function () { return batch_builder_1.BatchBuilder; } });
21
+ var attachments_client_1 = require("./clients/attachments-client");
22
+ Object.defineProperty(exports, "AttachmentsClient", { enumerable: true, get: function () { return attachments_client_1.AttachmentsClient; } });
21
23
  var conversation_messages_client_1 = require("./clients/conversation-messages-client");
22
24
  Object.defineProperty(exports, "ConversationMessagesClient", { enumerable: true, get: function () { return conversation_messages_client_1.ConversationMessagesClient; } });
23
25
  var inbox_client_1 = require("./clients/inbox-client");
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TwistApi = void 0;
4
4
  var batch_builder_1 = require("./batch-builder");
5
+ var attachments_client_1 = require("./clients/attachments-client");
5
6
  var channels_client_1 = require("./clients/channels-client");
6
7
  var comments_client_1 = require("./clients/comments-client");
7
8
  var conversation_messages_client_1 = require("./clients/conversation-messages-client");
@@ -73,6 +74,7 @@ var TwistApi = /** @class */ (function () {
73
74
  this.inbox = new inbox_client_1.InboxClient(clientConfig);
74
75
  this.reactions = new reactions_client_1.ReactionsClient(clientConfig);
75
76
  this.search = new search_client_1.SearchClient(clientConfig);
77
+ this.attachments = new attachments_client_1.AttachmentsClient(clientConfig);
76
78
  }
77
79
  /**
78
80
  * Executes multiple API requests in a single HTTP call using the batch endpoint.
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.getContentTypeFromFileName = getContentTypeFromFileName;
51
+ exports.uploadMultipartFile = uploadMultipartFile;
52
+ var fetch_with_retry_1 = require("../transport/fetch-with-retry");
53
+ /**
54
+ * Determine a content-type from a filename extension. Falls back to
55
+ * `application/octet-stream` for unknown extensions.
56
+ */
57
+ function getContentTypeFromFileName(fileName) {
58
+ var extension = fileName.toLowerCase().split('.').pop();
59
+ switch (extension) {
60
+ case 'png':
61
+ return 'image/png';
62
+ case 'jpg':
63
+ case 'jpeg':
64
+ return 'image/jpeg';
65
+ case 'gif':
66
+ return 'image/gif';
67
+ case 'webp':
68
+ return 'image/webp';
69
+ case 'svg':
70
+ return 'image/svg+xml';
71
+ case 'pdf':
72
+ return 'application/pdf';
73
+ default:
74
+ return 'application/octet-stream';
75
+ }
76
+ }
77
+ /**
78
+ * Normalise a supported {@link UploadFile} into a `Blob` plus a resolved file name and
79
+ * content type, so uploads use the cross-platform global `FormData`/`Blob` body that
80
+ * `undici` and browsers both accept natively.
81
+ */
82
+ function toBlob(file, fileName, contentType) {
83
+ if (file instanceof Blob) {
84
+ // `File` is not a global in Node 18, so guard the check before using it.
85
+ var name_1 = fileName ||
86
+ (typeof File !== 'undefined' && file instanceof File ? file.name : undefined) ||
87
+ 'upload';
88
+ var type = contentType || file.type || getContentTypeFromFileName(name_1);
89
+ // Re-wrap only when stamping a type the Blob doesn't already carry.
90
+ var blob = file.type === type ? file : new Blob([file], { type: type });
91
+ return { blob: blob, fileName: name_1, contentType: type };
92
+ }
93
+ if (file instanceof Uint8Array) {
94
+ if (!fileName) {
95
+ throw new Error('fileName is required when uploading raw bytes');
96
+ }
97
+ var type = contentType || getContentTypeFromFileName(fileName);
98
+ // `Blob` accepts any `ArrayBufferView`; the cast satisfies the stricter lib
99
+ // `BlobPart` type (which pins the backing buffer to `ArrayBuffer`).
100
+ return { blob: new Blob([file], { type: type }), fileName: fileName, contentType: type };
101
+ }
102
+ throw new Error('Unsupported file type for upload: expected a Blob or Uint8Array');
103
+ }
104
+ /**
105
+ * Upload a file using `multipart/form-data`.
106
+ *
107
+ * Builds the request body with the global `FormData`/`Blob` so it works unchanged in the
108
+ * browser and in Node.js (via `undici`). The `file` part is sent alongside `file_name`,
109
+ * `file_size`, and `underlying_type` fields (the canonical Twist upload shape); any
110
+ * `additionalFields` are merged in and override the derived values. Authentication uses
111
+ * `Authorization: Bearer`, matching every other Twist SDK client, and `Content-Type` is
112
+ * intentionally left unset so the runtime adds the correct multipart boundary.
113
+ *
114
+ * The response is JSON-parsed and camel-cased by {@link fetchWithRetry}; callers validate
115
+ * the returned shape with the appropriate schema.
116
+ */
117
+ function uploadMultipartFile(args) {
118
+ return __awaiter(this, void 0, void 0, function () {
119
+ var baseUrl, authToken, endpoint, file, fileName, contentType, additionalFields, requestId, customFetch, _a, blob, resolvedFileName, resolvedType, fields, form, _i, _b, _c, key, value, headers, url, response;
120
+ return __generator(this, function (_d) {
121
+ switch (_d.label) {
122
+ case 0:
123
+ baseUrl = args.baseUrl, authToken = args.authToken, endpoint = args.endpoint, file = args.file, fileName = args.fileName, contentType = args.contentType, additionalFields = args.additionalFields, requestId = args.requestId, customFetch = args.customFetch;
124
+ _a = toBlob(file, fileName, contentType), blob = _a.blob, resolvedFileName = _a.fileName, resolvedType = _a.contentType;
125
+ fields = __assign({ file_name: resolvedFileName, file_size: blob.size, underlying_type: resolvedType }, additionalFields);
126
+ form = new FormData();
127
+ form.append('file', blob, resolvedFileName);
128
+ for (_i = 0, _b = Object.entries(fields); _i < _b.length; _i++) {
129
+ _c = _b[_i], key = _c[0], value = _c[1];
130
+ if (value !== undefined && value !== null) {
131
+ form.append(key, String(value));
132
+ }
133
+ }
134
+ headers = {
135
+ Authorization: "Bearer ".concat(authToken),
136
+ };
137
+ if (requestId) {
138
+ headers['X-Request-Id'] = requestId;
139
+ }
140
+ url = new URL(endpoint, baseUrl).toString();
141
+ return [4 /*yield*/, (0, fetch_with_retry_1.fetchWithRetry)(url, {
142
+ method: 'POST',
143
+ headers: headers,
144
+ body: form,
145
+ // Don't set Content-Type — the runtime adds the multipart boundary.
146
+ timeout: 30000,
147
+ }, 3, customFetch)];
148
+ case 1:
149
+ response = _d.sent();
150
+ return [2 /*return*/, response.data];
151
+ }
152
+ });
153
+ });
154
+ }
@@ -0,0 +1,121 @@
1
+ var __extends = (this && this.__extends) || (function () {
2
+ var extendStatics = function (d, b) {
3
+ extendStatics = Object.setPrototypeOf ||
4
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
5
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
6
+ return extendStatics(d, b);
7
+ };
8
+ return function (d, b) {
9
+ if (typeof b !== "function" && b !== null)
10
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
11
+ extendStatics(d, b);
12
+ function __() { this.constructor = d; }
13
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
14
+ };
15
+ })();
16
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
17
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
18
+ return new (P || (P = Promise))(function (resolve, reject) {
19
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
20
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
21
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
22
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
23
+ });
24
+ };
25
+ var __generator = (this && this.__generator) || function (thisArg, body) {
26
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
27
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
28
+ function verb(n) { return function (v) { return step([n, v]); }; }
29
+ function step(op) {
30
+ if (f) throw new TypeError("Generator is already executing.");
31
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
32
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
33
+ if (y = 0, t) op = [op[0] & 2, t.value];
34
+ switch (op[0]) {
35
+ case 0: case 1: t = op; break;
36
+ case 4: _.label++; return { value: op[1], done: false };
37
+ case 5: _.label++; y = op[1]; op = [0]; continue;
38
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
39
+ default:
40
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
41
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
42
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
43
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
44
+ if (t[2]) _.ops.pop();
45
+ _.trys.pop(); continue;
46
+ }
47
+ op = body.call(thisArg, _);
48
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
49
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
50
+ }
51
+ };
52
+ import { v4 as uuid } from 'uuid';
53
+ import { ENDPOINT_ATTACHMENTS } from '../consts/endpoints.js';
54
+ import { AttachmentSchema } from '../types/entities.js';
55
+ import { uploadMultipartFile } from '../utils/multipart-upload.js';
56
+ import { BaseClient } from './base-client.js';
57
+ /**
58
+ * Client for uploading file attachments to Twist.
59
+ *
60
+ * Attachments are uploaded independently, then referenced by passing the returned
61
+ * {@link Attachment} into the `attachments` array of `comments.createComment`,
62
+ * `conversationMessages.createMessage`, and similar calls.
63
+ */
64
+ var AttachmentsClient = /** @class */ (function (_super) {
65
+ __extends(AttachmentsClient, _super);
66
+ function AttachmentsClient() {
67
+ return _super !== null && _super.apply(this, arguments) || this;
68
+ }
69
+ /**
70
+ * Uploads a file and returns the created {@link Attachment}.
71
+ *
72
+ * Mirrors the canonical multipart upload used by twist-web: `POST /attachments/upload`
73
+ * with the `file` binary plus `file_name`, `file_size`, `attachment_id`, and
74
+ * `underlying_type` form fields.
75
+ *
76
+ * @param args - The file to upload and optional metadata.
77
+ * @returns The created attachment, ready to attach to a comment or message.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * import { readFile } from 'node:fs/promises'
82
+ *
83
+ * const attachment = await api.attachments.upload({
84
+ * file: await readFile('./diagram.png'),
85
+ * fileName: 'diagram.png',
86
+ * })
87
+ *
88
+ * await api.comments.createComment({
89
+ * threadId: 789,
90
+ * content: 'See attached',
91
+ * attachments: [attachment],
92
+ * })
93
+ * ```
94
+ */
95
+ AttachmentsClient.prototype.upload = function (args) {
96
+ return __awaiter(this, void 0, void 0, function () {
97
+ var data;
98
+ return __generator(this, function (_a) {
99
+ switch (_a.label) {
100
+ case 0: return [4 /*yield*/, uploadMultipartFile({
101
+ baseUrl: this.getBaseUri(),
102
+ authToken: this.apiToken,
103
+ endpoint: "".concat(ENDPOINT_ATTACHMENTS, "/upload"),
104
+ file: args.file,
105
+ fileName: args.fileName,
106
+ contentType: args.contentType,
107
+ additionalFields: {
108
+ attachment_id: args.attachmentId || uuid(),
109
+ },
110
+ customFetch: this.customFetch,
111
+ })];
112
+ case 1:
113
+ data = _a.sent();
114
+ return [2 /*return*/, AttachmentSchema.parse(data)];
115
+ }
116
+ });
117
+ });
118
+ };
119
+ return AttachmentsClient;
120
+ }(BaseClient));
121
+ export { AttachmentsClient };
@@ -29,3 +29,4 @@ export var ENDPOINT_INBOX = 'inbox';
29
29
  export var ENDPOINT_REACTIONS = 'reactions';
30
30
  export var ENDPOINT_SEARCH = 'search';
31
31
  export var ENDPOINT_CONVERSATION_MESSAGES = 'conversation_messages';
32
+ export var ENDPOINT_ATTACHMENTS = 'attachments';
package/dist/esm/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './authentication.js';
2
2
  export { BatchBuilder } from './batch-builder.js';
3
+ export { AttachmentsClient } from './clients/attachments-client.js';
3
4
  export { ConversationMessagesClient } from './clients/conversation-messages-client.js';
4
5
  export { InboxClient } from './clients/inbox-client.js';
5
6
  export { ReactionsClient } from './clients/reactions-client.js';
@@ -1,4 +1,5 @@
1
1
  import { BatchBuilder } from './batch-builder.js';
2
+ import { AttachmentsClient } from './clients/attachments-client.js';
2
3
  import { ChannelsClient } from './clients/channels-client.js';
3
4
  import { CommentsClient } from './clients/comments-client.js';
4
5
  import { ConversationMessagesClient } from './clients/conversation-messages-client.js';
@@ -70,6 +71,7 @@ var TwistApi = /** @class */ (function () {
70
71
  this.inbox = new InboxClient(clientConfig);
71
72
  this.reactions = new ReactionsClient(clientConfig);
72
73
  this.search = new SearchClient(clientConfig);
74
+ this.attachments = new AttachmentsClient(clientConfig);
73
75
  }
74
76
  /**
75
77
  * Executes multiple API requests in a single HTTP call using the batch endpoint.
@@ -0,0 +1,150 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
13
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
14
+ return new (P || (P = Promise))(function (resolve, reject) {
15
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
16
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
17
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
18
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
19
+ });
20
+ };
21
+ var __generator = (this && this.__generator) || function (thisArg, body) {
22
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
23
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
24
+ function verb(n) { return function (v) { return step([n, v]); }; }
25
+ function step(op) {
26
+ if (f) throw new TypeError("Generator is already executing.");
27
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
28
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
29
+ if (y = 0, t) op = [op[0] & 2, t.value];
30
+ switch (op[0]) {
31
+ case 0: case 1: t = op; break;
32
+ case 4: _.label++; return { value: op[1], done: false };
33
+ case 5: _.label++; y = op[1]; op = [0]; continue;
34
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
35
+ default:
36
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
37
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
38
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
39
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
40
+ if (t[2]) _.ops.pop();
41
+ _.trys.pop(); continue;
42
+ }
43
+ op = body.call(thisArg, _);
44
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
45
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
46
+ }
47
+ };
48
+ import { fetchWithRetry } from '../transport/fetch-with-retry.js';
49
+ /**
50
+ * Determine a content-type from a filename extension. Falls back to
51
+ * `application/octet-stream` for unknown extensions.
52
+ */
53
+ export function getContentTypeFromFileName(fileName) {
54
+ var extension = fileName.toLowerCase().split('.').pop();
55
+ switch (extension) {
56
+ case 'png':
57
+ return 'image/png';
58
+ case 'jpg':
59
+ case 'jpeg':
60
+ return 'image/jpeg';
61
+ case 'gif':
62
+ return 'image/gif';
63
+ case 'webp':
64
+ return 'image/webp';
65
+ case 'svg':
66
+ return 'image/svg+xml';
67
+ case 'pdf':
68
+ return 'application/pdf';
69
+ default:
70
+ return 'application/octet-stream';
71
+ }
72
+ }
73
+ /**
74
+ * Normalise a supported {@link UploadFile} into a `Blob` plus a resolved file name and
75
+ * content type, so uploads use the cross-platform global `FormData`/`Blob` body that
76
+ * `undici` and browsers both accept natively.
77
+ */
78
+ function toBlob(file, fileName, contentType) {
79
+ if (file instanceof Blob) {
80
+ // `File` is not a global in Node 18, so guard the check before using it.
81
+ var name_1 = fileName ||
82
+ (typeof File !== 'undefined' && file instanceof File ? file.name : undefined) ||
83
+ 'upload';
84
+ var type = contentType || file.type || getContentTypeFromFileName(name_1);
85
+ // Re-wrap only when stamping a type the Blob doesn't already carry.
86
+ var blob = file.type === type ? file : new Blob([file], { type: type });
87
+ return { blob: blob, fileName: name_1, contentType: type };
88
+ }
89
+ if (file instanceof Uint8Array) {
90
+ if (!fileName) {
91
+ throw new Error('fileName is required when uploading raw bytes');
92
+ }
93
+ var type = contentType || getContentTypeFromFileName(fileName);
94
+ // `Blob` accepts any `ArrayBufferView`; the cast satisfies the stricter lib
95
+ // `BlobPart` type (which pins the backing buffer to `ArrayBuffer`).
96
+ return { blob: new Blob([file], { type: type }), fileName: fileName, contentType: type };
97
+ }
98
+ throw new Error('Unsupported file type for upload: expected a Blob or Uint8Array');
99
+ }
100
+ /**
101
+ * Upload a file using `multipart/form-data`.
102
+ *
103
+ * Builds the request body with the global `FormData`/`Blob` so it works unchanged in the
104
+ * browser and in Node.js (via `undici`). The `file` part is sent alongside `file_name`,
105
+ * `file_size`, and `underlying_type` fields (the canonical Twist upload shape); any
106
+ * `additionalFields` are merged in and override the derived values. Authentication uses
107
+ * `Authorization: Bearer`, matching every other Twist SDK client, and `Content-Type` is
108
+ * intentionally left unset so the runtime adds the correct multipart boundary.
109
+ *
110
+ * The response is JSON-parsed and camel-cased by {@link fetchWithRetry}; callers validate
111
+ * the returned shape with the appropriate schema.
112
+ */
113
+ export function uploadMultipartFile(args) {
114
+ return __awaiter(this, void 0, void 0, function () {
115
+ var baseUrl, authToken, endpoint, file, fileName, contentType, additionalFields, requestId, customFetch, _a, blob, resolvedFileName, resolvedType, fields, form, _i, _b, _c, key, value, headers, url, response;
116
+ return __generator(this, function (_d) {
117
+ switch (_d.label) {
118
+ case 0:
119
+ baseUrl = args.baseUrl, authToken = args.authToken, endpoint = args.endpoint, file = args.file, fileName = args.fileName, contentType = args.contentType, additionalFields = args.additionalFields, requestId = args.requestId, customFetch = args.customFetch;
120
+ _a = toBlob(file, fileName, contentType), blob = _a.blob, resolvedFileName = _a.fileName, resolvedType = _a.contentType;
121
+ fields = __assign({ file_name: resolvedFileName, file_size: blob.size, underlying_type: resolvedType }, additionalFields);
122
+ form = new FormData();
123
+ form.append('file', blob, resolvedFileName);
124
+ for (_i = 0, _b = Object.entries(fields); _i < _b.length; _i++) {
125
+ _c = _b[_i], key = _c[0], value = _c[1];
126
+ if (value !== undefined && value !== null) {
127
+ form.append(key, String(value));
128
+ }
129
+ }
130
+ headers = {
131
+ Authorization: "Bearer ".concat(authToken),
132
+ };
133
+ if (requestId) {
134
+ headers['X-Request-Id'] = requestId;
135
+ }
136
+ url = new URL(endpoint, baseUrl).toString();
137
+ return [4 /*yield*/, fetchWithRetry(url, {
138
+ method: 'POST',
139
+ headers: headers,
140
+ body: form,
141
+ // Don't set Content-Type — the runtime adds the multipart boundary.
142
+ timeout: 30000,
143
+ }, 3, customFetch)];
144
+ case 1:
145
+ response = _d.sent();
146
+ return [2 /*return*/, response.data];
147
+ }
148
+ });
149
+ });
150
+ }
@@ -0,0 +1,39 @@
1
+ import { type Attachment } from '../types/entities.js';
2
+ import type { UploadAttachmentArgs } from '../types/requests.js';
3
+ import { BaseClient } from './base-client.js';
4
+ /**
5
+ * Client for uploading file attachments to Twist.
6
+ *
7
+ * Attachments are uploaded independently, then referenced by passing the returned
8
+ * {@link Attachment} into the `attachments` array of `comments.createComment`,
9
+ * `conversationMessages.createMessage`, and similar calls.
10
+ */
11
+ export declare class AttachmentsClient extends BaseClient {
12
+ /**
13
+ * Uploads a file and returns the created {@link Attachment}.
14
+ *
15
+ * Mirrors the canonical multipart upload used by twist-web: `POST /attachments/upload`
16
+ * with the `file` binary plus `file_name`, `file_size`, `attachment_id`, and
17
+ * `underlying_type` form fields.
18
+ *
19
+ * @param args - The file to upload and optional metadata.
20
+ * @returns The created attachment, ready to attach to a comment or message.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { readFile } from 'node:fs/promises'
25
+ *
26
+ * const attachment = await api.attachments.upload({
27
+ * file: await readFile('./diagram.png'),
28
+ * fileName: 'diagram.png',
29
+ * })
30
+ *
31
+ * await api.comments.createComment({
32
+ * threadId: 789,
33
+ * content: 'See attached',
34
+ * attachments: [attachment],
35
+ * })
36
+ * ```
37
+ */
38
+ upload(args: UploadAttachmentArgs): Promise<Attachment>;
39
+ }
@@ -24,3 +24,4 @@ export declare const ENDPOINT_INBOX = "inbox";
24
24
  export declare const ENDPOINT_REACTIONS = "reactions";
25
25
  export declare const ENDPOINT_SEARCH = "search";
26
26
  export declare const ENDPOINT_CONVERSATION_MESSAGES = "conversation_messages";
27
+ export declare const ENDPOINT_ATTACHMENTS = "attachments";
@@ -1,5 +1,6 @@
1
1
  export * from './authentication.js';
2
2
  export { BatchBuilder } from './batch-builder.js';
3
+ export { AttachmentsClient } from './clients/attachments-client.js';
3
4
  export { ConversationMessagesClient } from './clients/conversation-messages-client.js';
4
5
  export { InboxClient } from './clients/inbox-client.js';
5
6
  export { ReactionsClient } from './clients/reactions-client.js';
@@ -1,3 +1,4 @@
1
+ import { AttachmentsClient } from './clients/attachments-client.js';
1
2
  import { ChannelsClient } from './clients/channels-client.js';
2
3
  import { CommentsClient } from './clients/comments-client.js';
3
4
  import { ConversationMessagesClient } from './clients/conversation-messages-client.js';
@@ -42,6 +43,7 @@ export declare class TwistApi {
42
43
  inbox: InboxClient;
43
44
  reactions: ReactionsClient;
44
45
  search: SearchClient;
46
+ attachments: AttachmentsClient;
45
47
  private authToken;
46
48
  private baseUrl?;
47
49
  private customFetch?;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { UploadFile } from '../utils/multipart-upload.js';
2
3
  import { type Attachment } from './entities.js';
3
4
  export declare const CreateChannelArgsSchema: z.ZodObject<{
4
5
  workspaceId: z.ZodNumber;
@@ -375,4 +376,20 @@ export type GetUserLocalTimeArgs = {
375
376
  workspaceId: number;
376
377
  userId: number;
377
378
  };
379
+ export type UploadAttachmentArgs = {
380
+ /**
381
+ * The file to upload. Accepts a `Blob`/`File` (browser) or a `Uint8Array` of raw
382
+ * bytes. A Node `Buffer` is a `Uint8Array`, so `await readFile(path)` works directly.
383
+ */
384
+ file: UploadFile;
385
+ /**
386
+ * File name. Required when `file` is a `Uint8Array`; inferred from the `File.name`
387
+ * otherwise.
388
+ */
389
+ fileName?: string;
390
+ /** MIME type. Defaults to the `Blob`'s type or one inferred from the file extension. */
391
+ contentType?: string;
392
+ /** Attachment ID to use. A random UUID is generated when omitted. */
393
+ attachmentId?: string;
394
+ };
378
395
  export {};
@@ -0,0 +1,53 @@
1
+ import type { CustomFetch } from '../types/http.js';
2
+ /**
3
+ * File content accepted by the upload helpers.
4
+ *
5
+ * - `Blob`/`File` — browser, or any runtime with a global `Blob` (Node 18+).
6
+ * - `Uint8Array` — raw bytes (requires `fileName`). A Node `Buffer` is a `Uint8Array`,
7
+ * so reading a file with `fs` and passing the result works without conversion.
8
+ *
9
+ * Both are universal types, so the published `.d.ts` stays free of Node-only globals
10
+ * (`Buffer`, `NodeJS.*`) and the helper pulls in no Node built-ins — keeping the browser
11
+ * bundle clean.
12
+ */
13
+ export type UploadFile = Blob | Uint8Array;
14
+ type UploadMultipartFileArgs = {
15
+ /** Base API URI with trailing slash, e.g. `https://api.twist.com/api/v3/`. */
16
+ baseUrl: string;
17
+ /** API token used for `Authorization: Bearer`. */
18
+ authToken: string;
19
+ /** Relative endpoint path, e.g. `attachments/upload`. */
20
+ endpoint: string;
21
+ /** File content to upload. */
22
+ file: UploadFile;
23
+ /** File name. Required for raw `Uint8Array` bytes; inferred from a `File`. */
24
+ fileName?: string;
25
+ /** MIME type. Defaults to the `Blob`'s type or one inferred from the file extension. */
26
+ contentType?: string;
27
+ /** Extra multipart fields to send alongside the file metadata fields. */
28
+ additionalFields?: Record<string, string | number | boolean | undefined | null>;
29
+ /** Optional request ID for tracing. */
30
+ requestId?: string;
31
+ /** Optional custom fetch implementation. */
32
+ customFetch?: CustomFetch;
33
+ };
34
+ /**
35
+ * Determine a content-type from a filename extension. Falls back to
36
+ * `application/octet-stream` for unknown extensions.
37
+ */
38
+ export declare function getContentTypeFromFileName(fileName: string): string;
39
+ /**
40
+ * Upload a file using `multipart/form-data`.
41
+ *
42
+ * Builds the request body with the global `FormData`/`Blob` so it works unchanged in the
43
+ * browser and in Node.js (via `undici`). The `file` part is sent alongside `file_name`,
44
+ * `file_size`, and `underlying_type` fields (the canonical Twist upload shape); any
45
+ * `additionalFields` are merged in and override the derived values. Authentication uses
46
+ * `Authorization: Bearer`, matching every other Twist SDK client, and `Content-Type` is
47
+ * intentionally left unset so the runtime adds the correct multipart boundary.
48
+ *
49
+ * The response is JSON-parsed and camel-cased by {@link fetchWithRetry}; callers validate
50
+ * the returned shape with the appropriate schema.
51
+ */
52
+ export declare function uploadMultipartFile<T>(args: UploadMultipartFileArgs): Promise<T>;
53
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/twist-sdk",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "A TypeScript wrapper for the Twist REST API.",
5
5
  "author": "Doist developers",
6
6
  "homepage": "https://doist.github.io/twist-sdk-typescript/",