@superdurable/connector-asana 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/dex-connector.json +196 -0
- package/dist/chunk-BX7GRLMD.js +340 -0
- package/dist/connection-BBAfdtyU.d.ts +598 -0
- package/dist/index.d.ts +338 -0
- package/dist/index.js +878 -0
- package/dist/mock.d.ts +153 -0
- package/dist/mock.js +361 -0
- package/package.json +58 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AddCommentBranch,
|
|
3
|
+
AddCommentBranchAdded,
|
|
4
|
+
AddCommentBranchDefect,
|
|
5
|
+
AddCommentBranchNotFound,
|
|
6
|
+
AddCommentBranchProviderRejected,
|
|
7
|
+
AddCommentBranchUncertain,
|
|
8
|
+
AddCommentDefinition,
|
|
9
|
+
AddCommentInputSchema,
|
|
10
|
+
AddCommentMetadata,
|
|
11
|
+
AddCommentOutputSchema,
|
|
12
|
+
ConfigSchema,
|
|
13
|
+
Connection,
|
|
14
|
+
ConnectorID,
|
|
15
|
+
CreateTaskBranch,
|
|
16
|
+
CreateTaskBranchCreated,
|
|
17
|
+
CreateTaskBranchDefect,
|
|
18
|
+
CreateTaskBranchProviderRejected,
|
|
19
|
+
CreateTaskBranchUncertain,
|
|
20
|
+
CreateTaskDefinition,
|
|
21
|
+
CreateTaskInputSchema,
|
|
22
|
+
CreateTaskMetadata,
|
|
23
|
+
CreateTaskOutputSchema,
|
|
24
|
+
CustomFieldValueInputSchema,
|
|
25
|
+
CustomFieldValueSchema,
|
|
26
|
+
EnumOptionReferenceSchema,
|
|
27
|
+
GetTaskBranch,
|
|
28
|
+
GetTaskBranchDefect,
|
|
29
|
+
GetTaskBranchFound,
|
|
30
|
+
GetTaskBranchInvalidResponse,
|
|
31
|
+
GetTaskBranchNotFound,
|
|
32
|
+
GetTaskBranchProviderRejected,
|
|
33
|
+
GetTaskDefinition,
|
|
34
|
+
GetTaskInputSchema,
|
|
35
|
+
GetTaskMetadata,
|
|
36
|
+
ListTasksBranch,
|
|
37
|
+
ListTasksBranchDefect,
|
|
38
|
+
ListTasksBranchInvalidResponse,
|
|
39
|
+
ListTasksBranchListed,
|
|
40
|
+
ListTasksBranchNotFound,
|
|
41
|
+
ListTasksBranchProviderRejected,
|
|
42
|
+
ListTasksDefinition,
|
|
43
|
+
ListTasksInputSchema,
|
|
44
|
+
ListTasksMetadata,
|
|
45
|
+
ListTasksOutputSchema,
|
|
46
|
+
ProjectReferenceSchema,
|
|
47
|
+
ProviderID,
|
|
48
|
+
SectionReferenceSchema,
|
|
49
|
+
TaskMembershipSchema,
|
|
50
|
+
TaskReferenceSchema,
|
|
51
|
+
TaskSchema,
|
|
52
|
+
UpdateTaskBranch,
|
|
53
|
+
UpdateTaskBranchDefect,
|
|
54
|
+
UpdateTaskBranchInvalidResponse,
|
|
55
|
+
UpdateTaskBranchNotFound,
|
|
56
|
+
UpdateTaskBranchProviderRejected,
|
|
57
|
+
UpdateTaskBranchUpdated,
|
|
58
|
+
UpdateTaskDefinition,
|
|
59
|
+
UpdateTaskInputSchema,
|
|
60
|
+
UpdateTaskMetadata,
|
|
61
|
+
UpdateTaskOutputSchema,
|
|
62
|
+
UserReferenceSchema,
|
|
63
|
+
WorkspaceReferenceSchema,
|
|
64
|
+
newConnection,
|
|
65
|
+
newConnectionWithOperations
|
|
66
|
+
} from "./chunk-BX7GRLMD.js";
|
|
67
|
+
|
|
68
|
+
// src/generated/steps.ts
|
|
69
|
+
import { goToBranch, newMutationStep, newQueryStep } from "@superdurable/connectors-sdk";
|
|
70
|
+
function newListTasksStep(configuration) {
|
|
71
|
+
const { connection, connectionName, listed, notFound, providerRejected, invalidResponse, defect, ...stepConfiguration } = configuration;
|
|
72
|
+
if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
|
|
73
|
+
return newQueryStep({ ...stepConfiguration, operation: connection.operations.listTasks(), connection: connection.reference, branches: [
|
|
74
|
+
goToBranch("listed", listed),
|
|
75
|
+
...notFound ? [goToBranch("notFound", notFound)] : [],
|
|
76
|
+
...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
|
|
77
|
+
...invalidResponse ? [goToBranch("invalidResponse", invalidResponse)] : [],
|
|
78
|
+
...defect ? [goToBranch("defect", defect)] : []
|
|
79
|
+
] });
|
|
80
|
+
}
|
|
81
|
+
function newGetTaskStep(configuration) {
|
|
82
|
+
const { connection, connectionName, found, notFound, providerRejected, invalidResponse, defect, ...stepConfiguration } = configuration;
|
|
83
|
+
if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
|
|
84
|
+
return newQueryStep({ ...stepConfiguration, operation: connection.operations.getTask(), connection: connection.reference, branches: [
|
|
85
|
+
goToBranch("found", found),
|
|
86
|
+
...notFound ? [goToBranch("notFound", notFound)] : [],
|
|
87
|
+
...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
|
|
88
|
+
...invalidResponse ? [goToBranch("invalidResponse", invalidResponse)] : [],
|
|
89
|
+
...defect ? [goToBranch("defect", defect)] : []
|
|
90
|
+
] });
|
|
91
|
+
}
|
|
92
|
+
function newCreateTaskStep(configuration) {
|
|
93
|
+
const { connection, connectionName, created, providerRejected, uncertain, defect, ...stepConfiguration } = configuration;
|
|
94
|
+
if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
|
|
95
|
+
return newMutationStep({ ...stepConfiguration, operation: connection.operations.createTask(), connection: connection.reference, branches: [
|
|
96
|
+
goToBranch("created", created),
|
|
97
|
+
...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
|
|
98
|
+
...uncertain ? [goToBranch("uncertain", uncertain)] : [],
|
|
99
|
+
...defect ? [goToBranch("defect", defect)] : []
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
102
|
+
function newUpdateTaskStep(configuration) {
|
|
103
|
+
const { connection, connectionName, updated, notFound, providerRejected, invalidResponse, defect, ...stepConfiguration } = configuration;
|
|
104
|
+
if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
|
|
105
|
+
return newMutationStep({ ...stepConfiguration, operation: connection.operations.updateTask(), connection: connection.reference, branches: [
|
|
106
|
+
goToBranch("updated", updated),
|
|
107
|
+
...notFound ? [goToBranch("notFound", notFound)] : [],
|
|
108
|
+
...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
|
|
109
|
+
...invalidResponse ? [goToBranch("invalidResponse", invalidResponse)] : [],
|
|
110
|
+
...defect ? [goToBranch("defect", defect)] : []
|
|
111
|
+
] });
|
|
112
|
+
}
|
|
113
|
+
function newAddCommentStep(configuration) {
|
|
114
|
+
const { connection, connectionName, added, notFound, providerRejected, uncertain, defect, ...stepConfiguration } = configuration;
|
|
115
|
+
if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
|
|
116
|
+
return newMutationStep({ ...stepConfiguration, operation: connection.operations.addComment(), connection: connection.reference, branches: [
|
|
117
|
+
goToBranch("added", added),
|
|
118
|
+
...notFound ? [goToBranch("notFound", notFound)] : [],
|
|
119
|
+
...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
|
|
120
|
+
...uncertain ? [goToBranch("uncertain", uncertain)] : [],
|
|
121
|
+
...defect ? [goToBranch("defect", defect)] : []
|
|
122
|
+
] });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/connection.ts
|
|
126
|
+
import { SecretString as SecretString2 } from "@superdurable/connectors-sdk";
|
|
127
|
+
import { z } from "zod";
|
|
128
|
+
|
|
129
|
+
// src/client.ts
|
|
130
|
+
import { SecretString } from "@superdurable/connectors-sdk";
|
|
131
|
+
import { AddressRefusal, BodyTooLargeError, MaxErrorBodyBytes, ProviderTransportError, isHeaderSafeCredential, parseRetryAfter, sendProviderRequest, validateBaseURL } from "@superdurable/connectors-sdk/providerhttp";
|
|
132
|
+
|
|
133
|
+
// src/operations.ts
|
|
134
|
+
import { newMutationBranch, newMutationRetry, newMutationUncertain, newQueryBranch, newQueryRetry, zeroReceipt } from "@superdurable/connectors-sdk";
|
|
135
|
+
import { newFence } from "@superdurable/connectors-sdk/singledispatch";
|
|
136
|
+
|
|
137
|
+
// src/task.ts
|
|
138
|
+
var taskSummaryFields = ["name", "resource_subtype", "completed", "completed_at", "assignee.name", "due_on", "due_at", "start_on", "memberships.project.name", "memberships.section.name", "parent.name", "permalink_url", "created_at", "modified_at"].join(",");
|
|
139
|
+
var taskDetailFields = `${taskSummaryFields},workspace.name,notes,custom_fields.name,custom_fields.resource_subtype,custom_fields.display_value,custom_fields.text_value,custom_fields.number_value,custom_fields.enum_value.name,custom_fields.multi_enum_values.name`;
|
|
140
|
+
var createdTaskFields = "gid,name,permalink_url,created_at";
|
|
141
|
+
var createdStoryFields = "gid,created_at,created_by.gid,created_by.name,resource_subtype";
|
|
142
|
+
var gidPattern = /^[0-9]{1,64}$/;
|
|
143
|
+
function gid(value, field) {
|
|
144
|
+
const trimmed = value?.trim() ?? "";
|
|
145
|
+
if (!gidPattern.test(trimmed)) throw new TypeError(`${field} must be an Asana gid, a decimal string such as 1204567890123456`);
|
|
146
|
+
return trimmed;
|
|
147
|
+
}
|
|
148
|
+
function optionalGid(value, field) {
|
|
149
|
+
return value?.trim() ? gid(value, field) : "";
|
|
150
|
+
}
|
|
151
|
+
function dueDate(value, field) {
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
const date = /* @__PURE__ */ new Date(`${trimmed}T00:00:00Z`);
|
|
154
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed) || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== trimmed) throw new TypeError(`${field} must be a YYYY-MM-DD date such as 2026-10-15`);
|
|
155
|
+
return trimmed;
|
|
156
|
+
}
|
|
157
|
+
function assignee(value, field) {
|
|
158
|
+
const trimmed = value.trim();
|
|
159
|
+
if (trimmed === "me" || gidPattern.test(trimmed) || Buffer.byteLength(trimmed) <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return trimmed;
|
|
160
|
+
throw new TypeError(`${field} must be me, a user gid, or an email address`);
|
|
161
|
+
}
|
|
162
|
+
function plainText(value, field, required) {
|
|
163
|
+
if (required && !value.trim()) throw new TypeError(`${field} is required`);
|
|
164
|
+
if ([...value].length > 65536) throw new TypeError(`${field} cannot exceed 65536 characters`);
|
|
165
|
+
if ([...value].some((character) => {
|
|
166
|
+
const code = character.codePointAt(0);
|
|
167
|
+
return code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127;
|
|
168
|
+
})) throw new TypeError(`${field} cannot contain control characters`);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function taskName(value) {
|
|
172
|
+
const name = value.trim();
|
|
173
|
+
if (!name) throw new TypeError("name is required");
|
|
174
|
+
if ([...name].length > 1024) throw new TypeError("name cannot exceed 1024 characters");
|
|
175
|
+
if ([...name].some((character) => {
|
|
176
|
+
const code = character.codePointAt(0);
|
|
177
|
+
return code < 32 || code === 127;
|
|
178
|
+
})) throw new TypeError("name must be one line without control characters");
|
|
179
|
+
return name;
|
|
180
|
+
}
|
|
181
|
+
function customFieldValues(inputs) {
|
|
182
|
+
if (!inputs?.length) return void 0;
|
|
183
|
+
if (inputs.length > 20) throw new TypeError("customFields accepts at most 20 values");
|
|
184
|
+
const result = {};
|
|
185
|
+
for (const input of inputs) {
|
|
186
|
+
const fieldId = gid(input.customFieldId, "customFieldId");
|
|
187
|
+
if (fieldId in result) throw new TypeError(`custom field ${fieldId} is set twice`);
|
|
188
|
+
const selectors = [input.text !== void 0, input.number !== void 0, !!input.enumOptionId?.trim(), input.multiEnumOptionIds !== void 0];
|
|
189
|
+
if (selectors.filter(Boolean).length !== 1) throw new TypeError(`custom field ${fieldId}: set exactly one of text, number, enumOptionId, and multiEnumOptionIds`);
|
|
190
|
+
if (input.text !== void 0) result[fieldId] = plainText(input.text, "text", false);
|
|
191
|
+
else if (input.number !== void 0) {
|
|
192
|
+
if (!Number.isFinite(input.number)) throw new TypeError(`custom field ${fieldId}: number must be finite`);
|
|
193
|
+
result[fieldId] = input.number;
|
|
194
|
+
} else if (input.multiEnumOptionIds !== void 0) {
|
|
195
|
+
if (input.multiEnumOptionIds.length > 50) throw new TypeError(`custom field ${fieldId}: multiEnumOptionIds accepts at most 50 options`);
|
|
196
|
+
result[fieldId] = input.multiEnumOptionIds.map((value) => gid(value, "multiEnumOptionIds"));
|
|
197
|
+
} else result[fieldId] = gid(input.enumOptionId, "enumOptionId");
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
function record(value) {
|
|
202
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
203
|
+
}
|
|
204
|
+
function string(value) {
|
|
205
|
+
return typeof value === "string" ? value : "";
|
|
206
|
+
}
|
|
207
|
+
function named(value) {
|
|
208
|
+
const resource = record(value);
|
|
209
|
+
if (!gidPattern.test(string(resource.gid))) return void 0;
|
|
210
|
+
return { id: string(resource.gid), ...string(resource.name) ? { name: string(resource.name) } : {} };
|
|
211
|
+
}
|
|
212
|
+
function time(value) {
|
|
213
|
+
const text = string(value);
|
|
214
|
+
if (!text) return "0001-01-01T00:00:00Z";
|
|
215
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/.exec(text);
|
|
216
|
+
if (!match) throw new TypeError("timestamp is not an ISO 8601 date-time");
|
|
217
|
+
const calendarDate = /* @__PURE__ */ new Date(0);
|
|
218
|
+
calendarDate.setUTCFullYear(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
219
|
+
if (calendarDate.getUTCFullYear() !== Number(match[1]) || calendarDate.getUTCMonth() + 1 !== Number(match[2]) || calendarDate.getUTCDate() !== Number(match[3])) throw new TypeError("timestamp is not an ISO 8601 date-time");
|
|
220
|
+
const parsed = new Date(text);
|
|
221
|
+
if (Number.isNaN(parsed.getTime())) throw new TypeError("timestamp is not an ISO 8601 date-time");
|
|
222
|
+
return parsed.toISOString().replace(/\.000Z$/, "Z");
|
|
223
|
+
}
|
|
224
|
+
function optionalTime(value) {
|
|
225
|
+
return value ? time(value) : void 0;
|
|
226
|
+
}
|
|
227
|
+
function optionalDate(value) {
|
|
228
|
+
return value ? dueDate(string(value), "date") : void 0;
|
|
229
|
+
}
|
|
230
|
+
function decodeTask(value, detail) {
|
|
231
|
+
const resource = record(value);
|
|
232
|
+
const id = gid(string(resource.gid), "task gid");
|
|
233
|
+
const task = {
|
|
234
|
+
id,
|
|
235
|
+
name: string(resource.name),
|
|
236
|
+
isCompleted: resource.completed === true,
|
|
237
|
+
createdAt: time(resource.created_at),
|
|
238
|
+
modifiedAt: time(resource.modified_at)
|
|
239
|
+
};
|
|
240
|
+
if (string(resource.resource_subtype)) task.resourceSubtype = string(resource.resource_subtype);
|
|
241
|
+
if (optionalTime(resource.completed_at)) task.completedAt = optionalTime(resource.completed_at);
|
|
242
|
+
if (named(resource.assignee)) task.assignee = named(resource.assignee);
|
|
243
|
+
if (optionalDate(resource.due_on)) task.dueOn = optionalDate(resource.due_on);
|
|
244
|
+
if (optionalTime(resource.due_at)) task.dueAt = optionalTime(resource.due_at);
|
|
245
|
+
if (optionalDate(resource.start_on)) task.startOn = optionalDate(resource.start_on);
|
|
246
|
+
if (named(resource.parent)) task.parent = named(resource.parent);
|
|
247
|
+
if (string(resource.permalink_url)) task.permalinkUrl = string(resource.permalink_url);
|
|
248
|
+
const memberships = [];
|
|
249
|
+
for (const raw of Array.isArray(resource.memberships) ? resource.memberships : []) {
|
|
250
|
+
const item = record(raw);
|
|
251
|
+
const project = named(item.project);
|
|
252
|
+
if (!project || memberships.length === 20) continue;
|
|
253
|
+
const section = named(item.section);
|
|
254
|
+
memberships.push({ project, ...section ? { section } : {} });
|
|
255
|
+
}
|
|
256
|
+
if (memberships.length) task.memberships = memberships;
|
|
257
|
+
if (detail) {
|
|
258
|
+
if (named(resource.workspace)) task.workspace = named(resource.workspace);
|
|
259
|
+
const notes = string(resource.notes);
|
|
260
|
+
if (notes) {
|
|
261
|
+
task.notes = [...notes].slice(0, 65536).join("");
|
|
262
|
+
if ([...notes].length > 65536) task.isNotesTruncated = true;
|
|
263
|
+
}
|
|
264
|
+
const fields = [];
|
|
265
|
+
for (const raw of Array.isArray(resource.custom_fields) ? resource.custom_fields : []) {
|
|
266
|
+
const field = record(raw);
|
|
267
|
+
if (!gidPattern.test(string(field.gid)) || fields.length === 50) continue;
|
|
268
|
+
const view = { id: string(field.gid) };
|
|
269
|
+
if (string(field.name)) view.name = string(field.name);
|
|
270
|
+
if (string(field.resource_subtype)) view.type = string(field.resource_subtype);
|
|
271
|
+
if (typeof field.display_value === "string") view.displayValue = field.display_value;
|
|
272
|
+
if (typeof field.text_value === "string") view.textValue = field.text_value;
|
|
273
|
+
if (typeof field.number_value === "number") view.numberValue = field.number_value;
|
|
274
|
+
if (named(field.enum_value)) view.enumOption = named(field.enum_value);
|
|
275
|
+
const options = (Array.isArray(field.multi_enum_values) ? field.multi_enum_values : []).map((value2) => named(value2)).filter((value2) => value2 !== void 0);
|
|
276
|
+
if (options.length) view.multiEnumOptions = options;
|
|
277
|
+
fields.push(view);
|
|
278
|
+
}
|
|
279
|
+
if (fields.length) task.customFields = fields;
|
|
280
|
+
}
|
|
281
|
+
return task;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/operations.ts
|
|
285
|
+
var zeroTime = "0001-01-01T00:00:00Z";
|
|
286
|
+
var emptyTask = { id: "", name: "", isCompleted: false, createdAt: zeroTime, modifiedAt: zeroTime };
|
|
287
|
+
var emptyList = { tasks: [] };
|
|
288
|
+
var retryableStatus = (status) => status === 408 || status === 429 || status >= 500;
|
|
289
|
+
var successStatus = (status) => status >= 200 && status < 300;
|
|
290
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
291
|
+
var string2 = (value) => typeof value === "string" ? value : "";
|
|
292
|
+
var parseTime = (value) => {
|
|
293
|
+
const raw = string2(value);
|
|
294
|
+
if (!raw) return void 0;
|
|
295
|
+
const date = new Date(raw);
|
|
296
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString().replace(/\.000Z$/, "Z");
|
|
297
|
+
};
|
|
298
|
+
var queryForFields = (fields) => new URLSearchParams({ opt_fields: fields });
|
|
299
|
+
function readOutcome(client, operation, subject, exchange) {
|
|
300
|
+
const delay = client.retryAfter(exchange);
|
|
301
|
+
if ("error" in exchange) {
|
|
302
|
+
if (exchange.error === "notBuilt") return { kind: "defect", error: failure(operation, "LOCAL_DEFECT", "Asana request could not be built"), delay };
|
|
303
|
+
return { kind: "retry", error: failure(operation, "TRANSPORT", `Asana ${subject} is temporarily unavailable`), delay };
|
|
304
|
+
}
|
|
305
|
+
const status = exchange.response.status;
|
|
306
|
+
if (status === 404) return { kind: "notFound", error: failure(operation, "NOT_FOUND", `Asana ${subject} was not found or is not visible to the connection`), delay };
|
|
307
|
+
if (status === 429) return { kind: "retry", error: failure(operation, "RATE_LIMIT", `Asana rate limited the ${subject}`), delay };
|
|
308
|
+
if (retryableStatus(status)) return { kind: "retry", error: failure(operation, "AVAILABILITY", `Asana ${subject} is temporarily unavailable`), delay };
|
|
309
|
+
if (status >= 400 && status < 500) return { kind: "providerRejected", error: rejection(operation, subject, status), delay };
|
|
310
|
+
if (!successStatus(status)) return { kind: "invalidResponse", error: failure(operation, "PROTOCOL", `Asana returned unexpected HTTP ${status} for the ${subject}`), delay };
|
|
311
|
+
if (exchange.isTooLarge) return { kind: "invalidResponse", error: failure(operation, "RESPONSE_TOO_LARGE", `Asana ${subject} response exceeds the configured size limit`), delay };
|
|
312
|
+
if (exchange.reflectsSecret) return { kind: "invalidResponse", error: failure(operation, "PROTOCOL", `Asana ${subject} response contains credential material`), delay };
|
|
313
|
+
return { kind: "success", delay };
|
|
314
|
+
}
|
|
315
|
+
function writeOutcome(client, operation, subject, exchange, repeatable) {
|
|
316
|
+
const delay = client.retryAfter(exchange);
|
|
317
|
+
if ("error" in exchange) {
|
|
318
|
+
if (exchange.error === "notBuilt") return { kind: "defect", error: failure(operation, "LOCAL_DEFECT", "Asana request could not be built"), delay };
|
|
319
|
+
if (exchange.error === "beforeSend" || repeatable) return { kind: "retry", error: failure(operation, "TRANSPORT", `Asana ${subject} outcome is unknown, so the update is sent again`), delay };
|
|
320
|
+
return { kind: "uncertain", error: failure(operation, "TRANSPORT", `Asana ${subject} outcome is unknown`), delay };
|
|
321
|
+
}
|
|
322
|
+
const status = exchange.response.status;
|
|
323
|
+
if (successStatus(status)) {
|
|
324
|
+
if (exchange.isTooLarge) return { kind: repeatable ? "invalidResponse" : "uncertain", error: failure(operation, "RESPONSE_TOO_LARGE", `Asana ${subject} response exceeds the configured size limit`), delay };
|
|
325
|
+
if (exchange.reflectsSecret) return { kind: repeatable ? "invalidResponse" : "uncertain", error: failure(operation, "PROTOCOL", `Asana ${subject} response contains credential material`), delay };
|
|
326
|
+
return { kind: "success", delay };
|
|
327
|
+
}
|
|
328
|
+
if (status === 429) return { kind: "retry", error: failure(operation, "RATE_LIMIT", `Asana rate limited the ${subject}`), delay };
|
|
329
|
+
if (status === 503 || repeatable && retryableStatus(status)) return { kind: "retry", error: failure(operation, "AVAILABILITY", `Asana ${subject} is temporarily unavailable`), delay };
|
|
330
|
+
if (status === 404) return { kind: "notFound", error: rejection(operation, subject, status), delay };
|
|
331
|
+
if (status >= 400 && status < 500 && status !== 408) return { kind: "providerRejected", error: rejection(operation, subject, status), delay };
|
|
332
|
+
if (repeatable) return { kind: "invalidResponse", error: failure(operation, "PROTOCOL", `Asana returned unexpected HTTP ${status} for the ${subject}`), delay };
|
|
333
|
+
return { kind: "uncertain", error: failure(operation, "AVAILABILITY", `Asana ${subject} outcome is unknown after HTTP ${status}`), delay };
|
|
334
|
+
}
|
|
335
|
+
var ListTasksOperation = class {
|
|
336
|
+
/** Bind a validated Asana client. */
|
|
337
|
+
constructor(client) {
|
|
338
|
+
this.client = client;
|
|
339
|
+
}
|
|
340
|
+
client;
|
|
341
|
+
/** Return the stable manifest definition. */
|
|
342
|
+
definition() {
|
|
343
|
+
return ListTasksDefinition;
|
|
344
|
+
}
|
|
345
|
+
/** Read one page and classify one provider attempt. */
|
|
346
|
+
async invoke(call, input) {
|
|
347
|
+
let query;
|
|
348
|
+
try {
|
|
349
|
+
query = listQuery(input);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return newQueryBranch("defect", emptyList, zeroReceipt(), failure("listTasks", "VALIDATION", String(error)));
|
|
352
|
+
}
|
|
353
|
+
const token = await this.client.token(call);
|
|
354
|
+
if (!token) return newQueryBranch("defect", emptyList, zeroReceipt(), failure("listTasks", "AUTHENTICATION", "Asana connection credentials are unavailable"));
|
|
355
|
+
const exchange = await this.client.exchange(call, token, "GET", "/tasks", query);
|
|
356
|
+
const classified = readOutcome(this.client, "listTasks", "task list", exchange);
|
|
357
|
+
if (classified.kind === "retry") return newQueryRetry(classified.error, classified.delay);
|
|
358
|
+
const receipt = this.client.receipt(call);
|
|
359
|
+
if (classified.kind !== "success") return newQueryBranch(classified.kind, emptyList, receipt, classified.error);
|
|
360
|
+
if (!("response" in exchange)) throw new TypeError("successful read requires a response");
|
|
361
|
+
try {
|
|
362
|
+
const page = JSON.parse(Buffer.from(exchange.response.body).toString("utf8"));
|
|
363
|
+
if (!Array.isArray(page.data) || page.data.length > 100) throw new TypeError("response has no data array");
|
|
364
|
+
const tasks = page.data.map((value) => decodeTask(value, false));
|
|
365
|
+
const nextOffset = string2(record2(page.next_page).offset);
|
|
366
|
+
if (nextOffset && !validOffset(nextOffset)) throw new TypeError("next page offset is invalid");
|
|
367
|
+
return newQueryBranch("listed", { tasks, ...nextOffset ? { nextOffset } : {} }, receipt);
|
|
368
|
+
} catch {
|
|
369
|
+
return newQueryBranch("invalidResponse", emptyList, receipt, failure("listTasks", "PROTOCOL", "Asana returned an invalid task page"));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
var GetTaskOperation = class {
|
|
374
|
+
/** Bind a validated Asana client. */
|
|
375
|
+
constructor(client) {
|
|
376
|
+
this.client = client;
|
|
377
|
+
}
|
|
378
|
+
client;
|
|
379
|
+
/** Return the stable manifest definition. */
|
|
380
|
+
definition() {
|
|
381
|
+
return GetTaskDefinition;
|
|
382
|
+
}
|
|
383
|
+
/** Read one task and classify one provider attempt. */
|
|
384
|
+
async invoke(call, input) {
|
|
385
|
+
let taskId;
|
|
386
|
+
try {
|
|
387
|
+
taskId = gid(input.taskId, "taskId");
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return newQueryBranch("defect", emptyTask, zeroReceipt(), failure("getTask", "VALIDATION", String(error)));
|
|
390
|
+
}
|
|
391
|
+
const token = await this.client.token(call);
|
|
392
|
+
if (!token) return newQueryBranch("defect", emptyTask, zeroReceipt(), failure("getTask", "AUTHENTICATION", "Asana connection credentials are unavailable"));
|
|
393
|
+
const exchange = await this.client.exchange(call, token, "GET", `/tasks/${taskId}`, queryForFields(taskDetailFields));
|
|
394
|
+
const classified = readOutcome(this.client, "getTask", "task", exchange);
|
|
395
|
+
if (classified.kind === "retry") return newQueryRetry(classified.error, classified.delay);
|
|
396
|
+
const receipt = this.client.receipt(call, taskId);
|
|
397
|
+
if (classified.kind !== "success") return newQueryBranch(classified.kind, emptyTask, receipt, classified.error);
|
|
398
|
+
try {
|
|
399
|
+
return newQueryBranch("found", decodeTask(dataObject(exchange), true), receipt);
|
|
400
|
+
} catch {
|
|
401
|
+
return newQueryBranch("invalidResponse", emptyTask, receipt, failure("getTask", "PROTOCOL", "Asana returned an invalid task"));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
var CreateTaskOperation = class {
|
|
406
|
+
/** Bind a validated Asana client. */
|
|
407
|
+
constructor(client) {
|
|
408
|
+
this.client = client;
|
|
409
|
+
}
|
|
410
|
+
client;
|
|
411
|
+
/** Return the stable manifest definition. */
|
|
412
|
+
definition() {
|
|
413
|
+
return CreateTaskDefinition;
|
|
414
|
+
}
|
|
415
|
+
/** Use the Call ID in Dex receipts; Asana does not accept this key. */
|
|
416
|
+
idempotencyKey(callId) {
|
|
417
|
+
return callId;
|
|
418
|
+
}
|
|
419
|
+
/** Create or read back one task without repeating an ambiguous write. */
|
|
420
|
+
async invoke(call, input) {
|
|
421
|
+
const requested = { name: input.name?.trim() ?? "", ...input.projectId?.trim() ? { projectId: input.projectId.trim() } : {}, ...input.sectionId?.trim() ? { sectionId: input.sectionId.trim() } : {}, ...input.workspaceId?.trim() ? { workspaceId: input.workspaceId.trim() } : {} };
|
|
422
|
+
let fields;
|
|
423
|
+
try {
|
|
424
|
+
fields = createFields(input);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return newMutationBranch("defect", requested, zeroReceipt(), failure("createTask", "VALIDATION", String(error)));
|
|
427
|
+
}
|
|
428
|
+
const fence = newFence(call, 3e4);
|
|
429
|
+
const decision = fence.decide();
|
|
430
|
+
if (decision.kind === "failClosed") return newMutationUncertain(requested, failure("createTask", "LOCAL_DEFECT", "Asana task dispatch checkpoint is unavailable"), this.client.receipt(call));
|
|
431
|
+
const taskId = decision.marker?.providerId;
|
|
432
|
+
if (decision.kind === "readBackRequired" && !taskId) return newMutationUncertain(requested, failure("createTask", "TRANSPORT", "Asana task dispatch checkpoint does not permit another create"), this.client.receipt(call));
|
|
433
|
+
const token = await this.client.token(call);
|
|
434
|
+
if (!token) return taskId ? newMutationRetry(failure("createTask", "AUTHENTICATION", "Asana connection credentials are unavailable")) : newMutationBranch("defect", requested, zeroReceipt(), failure("createTask", "AUTHENTICATION", "Asana connection credentials are unavailable"));
|
|
435
|
+
if (taskId) return this.readBack(call, token, requested, taskId);
|
|
436
|
+
let exchange;
|
|
437
|
+
try {
|
|
438
|
+
exchange = await fence.dispatch((signal) => this.client.exchange(call, token, "POST", "/tasks", queryForFields(createdTaskFields), { data: fields }, signal));
|
|
439
|
+
} catch {
|
|
440
|
+
return newMutationRetry(failure("createTask", "AVAILABILITY", "Asana task dispatch checkpoint could not be recorded"));
|
|
441
|
+
}
|
|
442
|
+
const classified = writeOutcome(this.client, "createTask", "task", exchange, false);
|
|
443
|
+
const receipt = this.client.receipt(call);
|
|
444
|
+
if (classified.kind === "retry") {
|
|
445
|
+
try {
|
|
446
|
+
await fence.recordCanResend();
|
|
447
|
+
} catch {
|
|
448
|
+
return newMutationUncertain(requested, failure("createTask", "LOCAL_DEFECT", "Asana task retry checkpoint is unavailable"), receipt);
|
|
449
|
+
}
|
|
450
|
+
return newMutationRetry(classified.error, classified.delay);
|
|
451
|
+
}
|
|
452
|
+
if (classified.kind === "uncertain") return newMutationUncertain(requested, classified.error, receipt);
|
|
453
|
+
if (classified.kind !== "success") return newMutationBranch(classified.kind === "notFound" ? "providerRejected" : classified.kind, requested, receipt, classified.error);
|
|
454
|
+
let resource;
|
|
455
|
+
try {
|
|
456
|
+
resource = dataObject(exchange);
|
|
457
|
+
gid(string2(resource.gid), "task gid");
|
|
458
|
+
} catch {
|
|
459
|
+
return newMutationUncertain(requested, failure("createTask", "PROTOCOL", "Asana accepted the task but returned an unusable task reference"), receipt);
|
|
460
|
+
}
|
|
461
|
+
const createdId = string2(resource.gid);
|
|
462
|
+
try {
|
|
463
|
+
await fence.recordProviderId(createdId);
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
return newMutationBranch("created", { ...requested, taskId: createdId, ...string2(resource.permalink_url) ? { permalinkUrl: string2(resource.permalink_url) } : {}, ...parseTime(resource.created_at) ? { createdAt: parseTime(resource.created_at) } : {} }, this.client.receipt(call, createdId));
|
|
467
|
+
}
|
|
468
|
+
async readBack(call, token, requested, taskId) {
|
|
469
|
+
const exchange = await this.client.exchange(call, token, "GET", `/tasks/${gid(taskId, "taskId")}`, queryForFields(createdTaskFields));
|
|
470
|
+
const classified = readOutcome(this.client, "createTask", "task", exchange);
|
|
471
|
+
if (classified.kind === "retry") return newMutationRetry(classified.error, classified.delay);
|
|
472
|
+
const receipt = this.client.receipt(call, taskId);
|
|
473
|
+
if (classified.kind !== "success") return newMutationUncertain(requested, failure("createTask", "PROTOCOL", "Asana task could not be confirmed by ID"), receipt);
|
|
474
|
+
try {
|
|
475
|
+
const resource = dataObject(exchange);
|
|
476
|
+
if (resource.gid !== taskId) throw new TypeError("task ID differs");
|
|
477
|
+
return newMutationBranch("created", { ...requested, taskId, ...string2(resource.permalink_url) ? { permalinkUrl: string2(resource.permalink_url) } : {}, ...parseTime(resource.created_at) ? { createdAt: parseTime(resource.created_at) } : {} }, receipt);
|
|
478
|
+
} catch {
|
|
479
|
+
return newMutationUncertain(requested, failure("createTask", "PROTOCOL", "Asana task read-back did not match the checkpointed ID"), receipt);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
var UpdateTaskOperation = class {
|
|
484
|
+
/** Bind a validated Asana client. */
|
|
485
|
+
constructor(client) {
|
|
486
|
+
this.client = client;
|
|
487
|
+
}
|
|
488
|
+
client;
|
|
489
|
+
/** Return the stable manifest definition. */
|
|
490
|
+
definition() {
|
|
491
|
+
return UpdateTaskDefinition;
|
|
492
|
+
}
|
|
493
|
+
/** Use the stable Call ID in Dex receipts. */
|
|
494
|
+
idempotencyKey(callId) {
|
|
495
|
+
return callId;
|
|
496
|
+
}
|
|
497
|
+
/** Apply repeatable changes and read back the resulting task. */
|
|
498
|
+
async invoke(call, input) {
|
|
499
|
+
const output = { taskId: input.taskId?.trim() ?? "" };
|
|
500
|
+
let fields, taskId, sectionId;
|
|
501
|
+
try {
|
|
502
|
+
({ fields, taskId, sectionId } = updateFields(input));
|
|
503
|
+
} catch (error) {
|
|
504
|
+
return newMutationBranch("defect", output, zeroReceipt(), failure("updateTask", "VALIDATION", String(error)));
|
|
505
|
+
}
|
|
506
|
+
const token = await this.client.token(call);
|
|
507
|
+
if (!token) return newMutationBranch("defect", output, zeroReceipt(), failure("updateTask", "AUTHENTICATION", "Asana connection credentials are unavailable"));
|
|
508
|
+
const receipt = this.client.receipt(call, taskId);
|
|
509
|
+
if (sectionId) {
|
|
510
|
+
const exchange2 = await this.client.exchange(call, token, "POST", `/sections/${sectionId}/addTask`, new URLSearchParams(), { data: { task: taskId } });
|
|
511
|
+
const classified2 = writeOutcome(this.client, "updateTask", "section move", exchange2, true);
|
|
512
|
+
const attempt = updateWriteAttempt(classified2, output, receipt);
|
|
513
|
+
if (attempt) return attempt;
|
|
514
|
+
output.isMovedToSection = true;
|
|
515
|
+
}
|
|
516
|
+
if (Object.keys(fields).length) {
|
|
517
|
+
const exchange2 = await this.client.exchange(call, token, "PUT", `/tasks/${taskId}`, queryForFields(taskDetailFields), { data: fields });
|
|
518
|
+
const classified2 = writeOutcome(this.client, "updateTask", "task update", exchange2, true);
|
|
519
|
+
const attempt = updateWriteAttempt(classified2, output, receipt);
|
|
520
|
+
if (attempt) return attempt;
|
|
521
|
+
try {
|
|
522
|
+
const task = decodeTask(dataObject(exchange2), true);
|
|
523
|
+
if (task.id !== taskId) throw new TypeError("task ID differs");
|
|
524
|
+
return newMutationBranch("updated", { ...output, task }, receipt);
|
|
525
|
+
} catch {
|
|
526
|
+
return newMutationBranch("invalidResponse", output, receipt, failure("updateTask", "PROTOCOL", "Asana returned an invalid task after the update"));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const exchange = await this.client.exchange(call, token, "GET", `/tasks/${taskId}`, queryForFields(taskDetailFields));
|
|
530
|
+
const classified = readOutcome(this.client, "updateTask", "task", exchange);
|
|
531
|
+
if (classified.kind === "retry") return newMutationRetry(classified.error, classified.delay);
|
|
532
|
+
if (classified.kind !== "success") return newMutationBranch(classified.kind, output, receipt, classified.error);
|
|
533
|
+
try {
|
|
534
|
+
return newMutationBranch("updated", { ...output, task: decodeTask(dataObject(exchange), true) }, receipt);
|
|
535
|
+
} catch {
|
|
536
|
+
return newMutationBranch("invalidResponse", output, receipt, failure("updateTask", "PROTOCOL", "Asana returned an invalid task"));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function updateWriteAttempt(classified, output, receipt) {
|
|
541
|
+
if (classified.kind === "success") return void 0;
|
|
542
|
+
if (classified.kind === "retry") return newMutationRetry(classified.error, classified.delay);
|
|
543
|
+
return newMutationBranch(classified.kind === "uncertain" ? "invalidResponse" : classified.kind, output, receipt, classified.error);
|
|
544
|
+
}
|
|
545
|
+
var AddCommentOperation = class {
|
|
546
|
+
/** Bind a validated Asana client. */
|
|
547
|
+
constructor(client) {
|
|
548
|
+
this.client = client;
|
|
549
|
+
}
|
|
550
|
+
client;
|
|
551
|
+
/** Return the stable manifest definition. */
|
|
552
|
+
definition() {
|
|
553
|
+
return AddCommentDefinition;
|
|
554
|
+
}
|
|
555
|
+
/** Use the Call ID in Dex receipts; Asana does not accept this key. */
|
|
556
|
+
idempotencyKey(callId) {
|
|
557
|
+
return callId;
|
|
558
|
+
}
|
|
559
|
+
/** Add or read back one story without repeating an ambiguous write. */
|
|
560
|
+
async invoke(call, input) {
|
|
561
|
+
const requested = { taskId: input.taskId?.trim() ?? "" };
|
|
562
|
+
let taskId;
|
|
563
|
+
try {
|
|
564
|
+
taskId = gid(input.taskId, "taskId");
|
|
565
|
+
plainText(input.text, "text", true);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
return newMutationBranch("defect", requested, zeroReceipt(), failure("addComment", "VALIDATION", String(error)));
|
|
568
|
+
}
|
|
569
|
+
const fence = newFence(call, 3e4);
|
|
570
|
+
const decision = fence.decide();
|
|
571
|
+
if (decision.kind === "failClosed") return newMutationUncertain(requested, failure("addComment", "LOCAL_DEFECT", "Asana comment dispatch checkpoint is unavailable"), this.client.receipt(call));
|
|
572
|
+
const storyId = decision.marker?.providerId;
|
|
573
|
+
if (decision.kind === "readBackRequired" && !storyId) return newMutationUncertain(requested, failure("addComment", "TRANSPORT", "Asana comment dispatch checkpoint does not permit another comment"), this.client.receipt(call));
|
|
574
|
+
const token = await this.client.token(call);
|
|
575
|
+
if (!token) return storyId ? newMutationRetry(failure("addComment", "AUTHENTICATION", "Asana connection credentials are unavailable")) : newMutationBranch("defect", requested, zeroReceipt(), failure("addComment", "AUTHENTICATION", "Asana connection credentials are unavailable"));
|
|
576
|
+
if (storyId) return this.readBack(call, token, requested, storyId);
|
|
577
|
+
let exchange;
|
|
578
|
+
try {
|
|
579
|
+
exchange = await fence.dispatch((signal) => this.client.exchange(call, token, "POST", `/tasks/${taskId}/stories`, queryForFields(createdStoryFields), { data: { text: input.text } }, signal));
|
|
580
|
+
} catch {
|
|
581
|
+
return newMutationRetry(failure("addComment", "AVAILABILITY", "Asana comment dispatch checkpoint could not be recorded"));
|
|
582
|
+
}
|
|
583
|
+
const classified = writeOutcome(this.client, "addComment", "comment", exchange, false);
|
|
584
|
+
const receipt = this.client.receipt(call);
|
|
585
|
+
if (classified.kind === "retry") {
|
|
586
|
+
try {
|
|
587
|
+
await fence.recordCanResend();
|
|
588
|
+
} catch {
|
|
589
|
+
return newMutationUncertain(requested, failure("addComment", "LOCAL_DEFECT", "Asana comment retry checkpoint is unavailable"), receipt);
|
|
590
|
+
}
|
|
591
|
+
return newMutationRetry(classified.error, classified.delay);
|
|
592
|
+
}
|
|
593
|
+
if (classified.kind === "uncertain") return newMutationUncertain(requested, classified.error, receipt);
|
|
594
|
+
if (classified.kind !== "success") return newMutationBranch(classified.kind, requested, receipt, classified.error);
|
|
595
|
+
let resource;
|
|
596
|
+
try {
|
|
597
|
+
resource = dataObject(exchange);
|
|
598
|
+
gid(string2(resource.gid), "story gid");
|
|
599
|
+
} catch {
|
|
600
|
+
return newMutationUncertain(requested, failure("addComment", "PROTOCOL", "Asana accepted the comment but returned an unusable story reference"), receipt);
|
|
601
|
+
}
|
|
602
|
+
const createdId = string2(resource.gid);
|
|
603
|
+
try {
|
|
604
|
+
await fence.recordProviderId(createdId);
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
return newMutationBranch("added", storyOutput(requested, resource), this.client.receipt(call, createdId));
|
|
608
|
+
}
|
|
609
|
+
async readBack(call, token, requested, storyId) {
|
|
610
|
+
const exchange = await this.client.exchange(call, token, "GET", `/stories/${gid(storyId, "storyId")}`, queryForFields(createdStoryFields));
|
|
611
|
+
const classified = readOutcome(this.client, "addComment", "comment", exchange);
|
|
612
|
+
if (classified.kind === "retry") return newMutationRetry(classified.error, classified.delay);
|
|
613
|
+
const receipt = this.client.receipt(call, storyId);
|
|
614
|
+
if (classified.kind !== "success") return newMutationUncertain(requested, failure("addComment", "PROTOCOL", "Asana comment could not be confirmed by ID"), receipt);
|
|
615
|
+
try {
|
|
616
|
+
const resource = dataObject(exchange);
|
|
617
|
+
if (resource.gid !== storyId) throw new TypeError("story ID differs");
|
|
618
|
+
return newMutationBranch("added", storyOutput(requested, resource), receipt);
|
|
619
|
+
} catch {
|
|
620
|
+
return newMutationUncertain(requested, failure("addComment", "PROTOCOL", "Asana comment read-back did not match the checkpointed ID"), receipt);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
function storyOutput(requested, resource) {
|
|
625
|
+
const author = record2(resource.created_by);
|
|
626
|
+
return { ...requested, commentId: string2(resource.gid), .../^[0-9]{1,64}$/.test(string2(author.gid)) ? { author: { id: string2(author.gid), ...string2(author.name) ? { name: string2(author.name) } : {} } } : {}, ...parseTime(resource.created_at) ? { createdAt: parseTime(resource.created_at) } : {} };
|
|
627
|
+
}
|
|
628
|
+
function validOffset(value) {
|
|
629
|
+
return Buffer.byteLength(value) <= 4096 && /^[!-~]+$/.test(value);
|
|
630
|
+
}
|
|
631
|
+
function listQuery(input) {
|
|
632
|
+
const projectId = optionalGid(input.projectId, "projectId");
|
|
633
|
+
const sectionId = optionalGid(input.sectionId, "sectionId");
|
|
634
|
+
if (!!projectId === !!sectionId) throw new TypeError("set exactly one of projectId and sectionId");
|
|
635
|
+
const query = queryForFields(taskSummaryFields);
|
|
636
|
+
query.set(projectId ? "project" : "section", projectId || sectionId);
|
|
637
|
+
if (input.isIncompleteOnly && input.completedSince) throw new TypeError("set at most one of isIncompleteOnly and completedSince");
|
|
638
|
+
if (input.isIncompleteOnly) query.set("completed_since", "now");
|
|
639
|
+
else if (input.completedSince) query.set("completed_since", new Date(input.completedSince).toISOString().replace(/\.000Z$/, "Z"));
|
|
640
|
+
if (input.modifiedSince) query.set("modified_since", new Date(input.modifiedSince).toISOString().replace(/\.000Z$/, "Z"));
|
|
641
|
+
const size = input.pageSize || 50;
|
|
642
|
+
if (!Number.isSafeInteger(size) || size < 1 || size > 100) throw new TypeError("pageSize must be between 1 and 100");
|
|
643
|
+
query.set("limit", String(size));
|
|
644
|
+
const offset = input.offset?.trim() ?? "";
|
|
645
|
+
if (offset) {
|
|
646
|
+
if (!validOffset(offset)) throw new TypeError("offset must be the token from a previous task page");
|
|
647
|
+
query.set("offset", offset);
|
|
648
|
+
}
|
|
649
|
+
return query;
|
|
650
|
+
}
|
|
651
|
+
function createFields(input) {
|
|
652
|
+
const name = taskName(input.name);
|
|
653
|
+
const projectId = optionalGid(input.projectId, "projectId");
|
|
654
|
+
const sectionId = optionalGid(input.sectionId, "sectionId");
|
|
655
|
+
const workspaceId = optionalGid(input.workspaceId, "workspaceId");
|
|
656
|
+
if (!!projectId === !!workspaceId) throw new TypeError("set exactly one of projectId and workspaceId");
|
|
657
|
+
if (sectionId && !projectId) throw new TypeError("sectionId requires projectId");
|
|
658
|
+
const fields = { name };
|
|
659
|
+
if (input.notes && plainText(input.notes, "notes", false).trim()) fields.notes = input.notes;
|
|
660
|
+
if (sectionId) fields.memberships = [{ project: projectId, section: sectionId }];
|
|
661
|
+
else if (projectId) fields.projects = [projectId];
|
|
662
|
+
if (workspaceId) fields.workspace = workspaceId;
|
|
663
|
+
if (input.assigneeId?.trim()) fields.assignee = assignee(input.assigneeId, "assigneeId");
|
|
664
|
+
if (input.dueOn?.trim()) fields.due_on = dueDate(input.dueOn, "dueOn");
|
|
665
|
+
const custom = customFieldValues(input.customFields);
|
|
666
|
+
if (custom) fields.custom_fields = custom;
|
|
667
|
+
return fields;
|
|
668
|
+
}
|
|
669
|
+
function updateFields(input) {
|
|
670
|
+
const taskId = gid(input.taskId, "taskId");
|
|
671
|
+
const sectionId = optionalGid(input.sectionId, "sectionId");
|
|
672
|
+
const fields = {};
|
|
673
|
+
if (input.isCompleted !== void 0) fields.completed = input.isCompleted;
|
|
674
|
+
if (input.assigneeId !== void 0) fields.assignee = input.assigneeId.trim() ? assignee(input.assigneeId, "assigneeId") : null;
|
|
675
|
+
if (input.dueOn !== void 0) fields.due_on = input.dueOn.trim() ? dueDate(input.dueOn, "dueOn") : null;
|
|
676
|
+
const custom = customFieldValues(input.customFields);
|
|
677
|
+
if (custom) fields.custom_fields = custom;
|
|
678
|
+
if (!sectionId && !Object.keys(fields).length) throw new TypeError("set at least one of isCompleted, assigneeId, dueOn, customFields, and sectionId");
|
|
679
|
+
return { taskId, sectionId, fields };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/client.ts
|
|
683
|
+
var publicEndpoint = "https://app.asana.com/api/1.0";
|
|
684
|
+
var Client2 = class {
|
|
685
|
+
endpoint;
|
|
686
|
+
maxResponseBytes;
|
|
687
|
+
credentials;
|
|
688
|
+
send;
|
|
689
|
+
now;
|
|
690
|
+
/** Validate settings and own the credential source without resolving a secret. */
|
|
691
|
+
constructor(config, credentials, options = {}) {
|
|
692
|
+
if (!credentials) throw new TypeError("Asana credential provider is required");
|
|
693
|
+
this.endpoint = validateBaseURL(config.endpoint || publicEndpoint);
|
|
694
|
+
this.maxResponseBytes = config.maxResponseBytes || 4194304;
|
|
695
|
+
if (!Number.isSafeInteger(this.maxResponseBytes) || this.maxResponseBytes < 1) throw new TypeError("Asana maxResponseBytes must be positive");
|
|
696
|
+
this.credentials = credentials;
|
|
697
|
+
this.send = options.send ?? sendProviderRequest;
|
|
698
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
699
|
+
}
|
|
700
|
+
/** Bind the task list query. */
|
|
701
|
+
listTasks() {
|
|
702
|
+
return new ListTasksOperation(this);
|
|
703
|
+
}
|
|
704
|
+
/** Bind the task detail query. */
|
|
705
|
+
getTask() {
|
|
706
|
+
return new GetTaskOperation(this);
|
|
707
|
+
}
|
|
708
|
+
/** Bind the task create mutation. */
|
|
709
|
+
createTask() {
|
|
710
|
+
return new CreateTaskOperation(this);
|
|
711
|
+
}
|
|
712
|
+
/** Bind the absolute task update mutation. */
|
|
713
|
+
updateTask() {
|
|
714
|
+
return new UpdateTaskOperation(this);
|
|
715
|
+
}
|
|
716
|
+
/** Bind the comment create mutation. */
|
|
717
|
+
addComment() {
|
|
718
|
+
return new AddCommentOperation(this);
|
|
719
|
+
}
|
|
720
|
+
/** Resolve the current personal token for exactly one operation attempt. */
|
|
721
|
+
async token(call) {
|
|
722
|
+
try {
|
|
723
|
+
const credentials = await this.credentials.resolve(call, call.context.cancellationSignal);
|
|
724
|
+
if (!(credentials.accessToken instanceof SecretString)) return void 0;
|
|
725
|
+
const token = credentials.accessToken.reveal();
|
|
726
|
+
return isHeaderSafeCredential(token) ? token : void 0;
|
|
727
|
+
} catch {
|
|
728
|
+
return void 0;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/** Send one Asana JSON request while bounding response bytes and elapsed time. */
|
|
732
|
+
async exchange(call, token, method, path, query, payload, signal) {
|
|
733
|
+
const remaining = call.context.deadline ? call.context.deadline.getTime() - this.now().getTime() - 1e3 : 25e3;
|
|
734
|
+
if (remaining < 2500) return { error: "beforeSend" };
|
|
735
|
+
const sortedQuery = [...query.entries()].sort(([left], [right]) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
736
|
+
const search = new URLSearchParams(sortedQuery).toString();
|
|
737
|
+
const request = {
|
|
738
|
+
url: `${this.endpoint}${path}${search ? `?${search}` : ""}`,
|
|
739
|
+
method,
|
|
740
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json", ...payload === void 0 ? {} : { "Content-Type": "application/json" } },
|
|
741
|
+
...payload === void 0 ? {} : { body: Buffer.from(JSON.stringify(payload), "utf8") },
|
|
742
|
+
maxResponseBytes: Math.max(this.maxResponseBytes, MaxErrorBodyBytes),
|
|
743
|
+
timeoutMs: Math.min(2e4, remaining),
|
|
744
|
+
addressPolicy: this.endpoint === publicEndpoint ? "publicHTTPS" : "privateNetworkHTTP",
|
|
745
|
+
signal: signal ?? call.context.cancellationSignal
|
|
746
|
+
};
|
|
747
|
+
try {
|
|
748
|
+
const response = await this.send(request);
|
|
749
|
+
const isTooLarge = response.status >= 200 && response.status < 300 && response.body.length > this.maxResponseBytes;
|
|
750
|
+
return { response: { ...response, body: isTooLarge ? new Uint8Array() : response.body }, isTooLarge, reflectsSecret: Buffer.from(response.body).includes(Buffer.from(token)) };
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (error instanceof BodyTooLargeError && error.status !== void 0) return { response: { status: error.status, headers: error.headers ?? {}, body: new Uint8Array() }, isTooLarge: true, reflectsSecret: false };
|
|
753
|
+
if (error instanceof AddressRefusal || error instanceof TypeError) return { error: "notBuilt" };
|
|
754
|
+
if (error instanceof ProviderTransportError) return { error: error.isProvablyNotSent ? "beforeSend" : "afterSend" };
|
|
755
|
+
return { error: "afterSend" };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/** Build a secret-free receipt, with no Asana request ID header. */
|
|
759
|
+
receipt(call, providerObjectId = "") {
|
|
760
|
+
return { callId: call.id, ...call.idempotencyKey ? { idempotencyKey: call.idempotencyKey } : {}, provider: "asana", ...providerObjectId ? { providerObjectId } : {}, observedAt: this.now().toISOString() };
|
|
761
|
+
}
|
|
762
|
+
/** Interpret Retry-After with the same bounded SDK parser as other connectors. */
|
|
763
|
+
retryAfter(exchange) {
|
|
764
|
+
if (!("response" in exchange)) return 0;
|
|
765
|
+
const value = exchange.response.headers["retry-after"];
|
|
766
|
+
return parseRetryAfter(typeof value === "string" ? value : "", this.now());
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
function failure(operation, kind, message) {
|
|
770
|
+
return { kind, provider: "asana", operation, message };
|
|
771
|
+
}
|
|
772
|
+
function rejection(operation, subject, status) {
|
|
773
|
+
const kind = status === 400 ? "VALIDATION" : status === 401 ? "AUTHENTICATION" : status === 402 || status === 403 ? "AUTHORIZATION" : status === 404 ? "NOT_FOUND" : status === 409 ? "CONFLICT" : "PROVIDER_REJECTION";
|
|
774
|
+
return failure(operation, kind, `Asana rejected the ${subject} with HTTP ${status}`);
|
|
775
|
+
}
|
|
776
|
+
function dataObject(exchange) {
|
|
777
|
+
if (!("response" in exchange)) throw new TypeError("Asana response is unavailable");
|
|
778
|
+
const parsed = JSON.parse(Buffer.from(exchange.response.body).toString("utf8"));
|
|
779
|
+
if (!parsed || typeof parsed !== "object" || !("data" in parsed) || !parsed.data || typeof parsed.data !== "object" || Array.isArray(parsed.data)) throw new TypeError("response has no data object");
|
|
780
|
+
return parsed.data;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/connection.ts
|
|
784
|
+
function newProjectConnection(project, connectionName, options = {}) {
|
|
785
|
+
if (!project || !connectionName.trim()) throw new TypeError("Asana project connection is required");
|
|
786
|
+
const selected = project.configuration.connections.find((entry) => entry.connectorId === "asana" && entry.connectionName === connectionName);
|
|
787
|
+
if (!selected || selected.provider !== "asana") throw new TypeError("Asana project connection is not configured");
|
|
788
|
+
const settings = z.object({ endpoint: z.string().optional(), maxResponseBytes: z.number().int().optional() }).parse(selected.configuration);
|
|
789
|
+
const config = { ...settings.endpoint !== void 0 ? { endpoint: settings.endpoint } : {}, ...settings.maxResponseBytes !== void 0 ? { maxResponseBytes: settings.maxResponseBytes } : {} };
|
|
790
|
+
const credentials = {
|
|
791
|
+
async resolve(_call, signal) {
|
|
792
|
+
const fields = z.object({ access_token: z.string().min(1) }).parse(await project.resolveCredential("asana", connectionName, signal));
|
|
793
|
+
return { accessToken: new SecretString2(fields.access_token) };
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
return newConnection(new Client2(config, credentials, options), { provider: "asana", name: connectionName });
|
|
797
|
+
}
|
|
798
|
+
export {
|
|
799
|
+
AddCommentBranch,
|
|
800
|
+
AddCommentBranchAdded,
|
|
801
|
+
AddCommentBranchDefect,
|
|
802
|
+
AddCommentBranchNotFound,
|
|
803
|
+
AddCommentBranchProviderRejected,
|
|
804
|
+
AddCommentBranchUncertain,
|
|
805
|
+
AddCommentDefinition,
|
|
806
|
+
AddCommentInputSchema,
|
|
807
|
+
AddCommentMetadata,
|
|
808
|
+
AddCommentOperation,
|
|
809
|
+
AddCommentOutputSchema,
|
|
810
|
+
Client2 as Client,
|
|
811
|
+
ConfigSchema,
|
|
812
|
+
Connection,
|
|
813
|
+
ConnectorID,
|
|
814
|
+
CreateTaskBranch,
|
|
815
|
+
CreateTaskBranchCreated,
|
|
816
|
+
CreateTaskBranchDefect,
|
|
817
|
+
CreateTaskBranchProviderRejected,
|
|
818
|
+
CreateTaskBranchUncertain,
|
|
819
|
+
CreateTaskDefinition,
|
|
820
|
+
CreateTaskInputSchema,
|
|
821
|
+
CreateTaskMetadata,
|
|
822
|
+
CreateTaskOperation,
|
|
823
|
+
CreateTaskOutputSchema,
|
|
824
|
+
CustomFieldValueInputSchema,
|
|
825
|
+
CustomFieldValueSchema,
|
|
826
|
+
EnumOptionReferenceSchema,
|
|
827
|
+
GetTaskBranch,
|
|
828
|
+
GetTaskBranchDefect,
|
|
829
|
+
GetTaskBranchFound,
|
|
830
|
+
GetTaskBranchInvalidResponse,
|
|
831
|
+
GetTaskBranchNotFound,
|
|
832
|
+
GetTaskBranchProviderRejected,
|
|
833
|
+
GetTaskDefinition,
|
|
834
|
+
GetTaskInputSchema,
|
|
835
|
+
GetTaskMetadata,
|
|
836
|
+
GetTaskOperation,
|
|
837
|
+
ListTasksBranch,
|
|
838
|
+
ListTasksBranchDefect,
|
|
839
|
+
ListTasksBranchInvalidResponse,
|
|
840
|
+
ListTasksBranchListed,
|
|
841
|
+
ListTasksBranchNotFound,
|
|
842
|
+
ListTasksBranchProviderRejected,
|
|
843
|
+
ListTasksDefinition,
|
|
844
|
+
ListTasksInputSchema,
|
|
845
|
+
ListTasksMetadata,
|
|
846
|
+
ListTasksOperation,
|
|
847
|
+
ListTasksOutputSchema,
|
|
848
|
+
ProjectReferenceSchema,
|
|
849
|
+
ProviderID,
|
|
850
|
+
SectionReferenceSchema,
|
|
851
|
+
TaskMembershipSchema,
|
|
852
|
+
TaskReferenceSchema,
|
|
853
|
+
TaskSchema,
|
|
854
|
+
UpdateTaskBranch,
|
|
855
|
+
UpdateTaskBranchDefect,
|
|
856
|
+
UpdateTaskBranchInvalidResponse,
|
|
857
|
+
UpdateTaskBranchNotFound,
|
|
858
|
+
UpdateTaskBranchProviderRejected,
|
|
859
|
+
UpdateTaskBranchUpdated,
|
|
860
|
+
UpdateTaskDefinition,
|
|
861
|
+
UpdateTaskInputSchema,
|
|
862
|
+
UpdateTaskMetadata,
|
|
863
|
+
UpdateTaskOperation,
|
|
864
|
+
UpdateTaskOutputSchema,
|
|
865
|
+
UserReferenceSchema,
|
|
866
|
+
WorkspaceReferenceSchema,
|
|
867
|
+
dataObject,
|
|
868
|
+
failure,
|
|
869
|
+
newAddCommentStep,
|
|
870
|
+
newConnection,
|
|
871
|
+
newConnectionWithOperations,
|
|
872
|
+
newCreateTaskStep,
|
|
873
|
+
newGetTaskStep,
|
|
874
|
+
newListTasksStep,
|
|
875
|
+
newProjectConnection,
|
|
876
|
+
newUpdateTaskStep,
|
|
877
|
+
rejection
|
|
878
|
+
};
|