@ttctl/core 0.0.0 → 0.1.0-rc.1

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.
Files changed (195) hide show
  1. package/README.md +49 -9
  2. package/dist/__generated__/gateway.d.ts +4546 -0
  3. package/dist/__generated__/gateway.d.ts.map +1 -0
  4. package/dist/__generated__/gateway.js +9 -0
  5. package/dist/__generated__/gateway.js.map +1 -0
  6. package/dist/__generated__/talent-profile-zod-schemas.d.ts +1187 -0
  7. package/dist/__generated__/talent-profile-zod-schemas.d.ts.map +1 -0
  8. package/dist/__generated__/talent-profile-zod-schemas.js +1136 -0
  9. package/dist/__generated__/talent-profile-zod-schemas.js.map +1 -0
  10. package/dist/__generated__/talent-profile.d.ts +1397 -0
  11. package/dist/__generated__/talent-profile.d.ts.map +1 -0
  12. package/dist/__generated__/talent-profile.js +9 -0
  13. package/dist/__generated__/talent-profile.js.map +1 -0
  14. package/dist/__generated__/zod-schemas.d.ts +2895 -0
  15. package/dist/__generated__/zod-schemas.d.ts.map +1 -0
  16. package/dist/__generated__/zod-schemas.js +3121 -0
  17. package/dist/__generated__/zod-schemas.js.map +1 -0
  18. package/dist/__tests__/fixtures/profile/builders.d.ts +74 -0
  19. package/dist/__tests__/fixtures/profile/builders.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/profile/builders.js +196 -0
  21. package/dist/__tests__/fixtures/profile/builders.js.map +1 -0
  22. package/dist/__tests__/fixtures/profile/data.d.ts +39 -0
  23. package/dist/__tests__/fixtures/profile/data.d.ts.map +1 -0
  24. package/dist/__tests__/fixtures/profile/data.js +230 -0
  25. package/dist/__tests__/fixtures/profile/data.js.map +1 -0
  26. package/dist/__tests__/fixtures/profile/index.d.ts +9 -0
  27. package/dist/__tests__/fixtures/profile/index.d.ts.map +1 -0
  28. package/dist/__tests__/fixtures/profile/index.js +10 -0
  29. package/dist/__tests__/fixtures/profile/index.js.map +1 -0
  30. package/dist/__tests__/fixtures/profile/types.d.ts +53 -0
  31. package/dist/__tests__/fixtures/profile/types.d.ts.map +1 -0
  32. package/dist/__tests__/fixtures/profile/types.js +4 -0
  33. package/dist/__tests__/fixtures/profile/types.js.map +1 -0
  34. package/dist/auth/errors.d.ts +82 -0
  35. package/dist/auth/errors.d.ts.map +1 -0
  36. package/dist/auth/errors.js +68 -0
  37. package/dist/auth/errors.js.map +1 -0
  38. package/dist/auth.d.ts +192 -0
  39. package/dist/auth.d.ts.map +1 -0
  40. package/dist/auth.js +294 -0
  41. package/dist/auth.js.map +1 -0
  42. package/dist/config.d.ts +212 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +349 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/configLock.d.ts +50 -0
  47. package/dist/configLock.d.ts.map +1 -0
  48. package/dist/configLock.js +88 -0
  49. package/dist/configLock.js.map +1 -0
  50. package/dist/configWriter.d.ts +97 -0
  51. package/dist/configWriter.d.ts.map +1 -0
  52. package/dist/configWriter.js +687 -0
  53. package/dist/configWriter.js.map +1 -0
  54. package/dist/index.d.ts +37 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +28 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/kill-switch.d.ts +161 -0
  59. package/dist/kill-switch.d.ts.map +1 -0
  60. package/dist/kill-switch.js +235 -0
  61. package/dist/kill-switch.js.map +1 -0
  62. package/dist/lib/date.d.ts +58 -0
  63. package/dist/lib/date.d.ts.map +1 -0
  64. package/dist/lib/date.js +104 -0
  65. package/dist/lib/date.js.map +1 -0
  66. package/dist/lib/diagnostic-log.d.ts +159 -0
  67. package/dist/lib/diagnostic-log.d.ts.map +1 -0
  68. package/dist/lib/diagnostic-log.js +186 -0
  69. package/dist/lib/diagnostic-log.js.map +1 -0
  70. package/dist/lib/package-version.d.ts +19 -0
  71. package/dist/lib/package-version.d.ts.map +1 -0
  72. package/dist/lib/package-version.js +38 -0
  73. package/dist/lib/package-version.js.map +1 -0
  74. package/dist/lib/redact.d.ts +153 -0
  75. package/dist/lib/redact.d.ts.map +1 -0
  76. package/dist/lib/redact.js +207 -0
  77. package/dist/lib/redact.js.map +1 -0
  78. package/dist/lib/text.d.ts +14 -0
  79. package/dist/lib/text.d.ts.map +1 -0
  80. package/dist/lib/text.js +21 -0
  81. package/dist/lib/text.js.map +1 -0
  82. package/dist/lib/wire-shape.d.ts +131 -0
  83. package/dist/lib/wire-shape.d.ts.map +1 -0
  84. package/dist/lib/wire-shape.js +376 -0
  85. package/dist/lib/wire-shape.js.map +1 -0
  86. package/dist/onepassword.d.ts +29 -0
  87. package/dist/onepassword.d.ts.map +1 -0
  88. package/dist/onepassword.js +112 -0
  89. package/dist/onepassword.js.map +1 -0
  90. package/dist/services/_shared/transport.d.ts +148 -0
  91. package/dist/services/_shared/transport.d.ts.map +1 -0
  92. package/dist/services/_shared/transport.js +102 -0
  93. package/dist/services/_shared/transport.js.map +1 -0
  94. package/dist/services/applications/index.d.ts +210 -0
  95. package/dist/services/applications/index.d.ts.map +1 -0
  96. package/dist/services/applications/index.js +240 -0
  97. package/dist/services/applications/index.js.map +1 -0
  98. package/dist/services/availability/index.d.ts +254 -0
  99. package/dist/services/availability/index.d.ts.map +1 -0
  100. package/dist/services/availability/index.js +310 -0
  101. package/dist/services/availability/index.js.map +1 -0
  102. package/dist/services/contracts/index.d.ts +132 -0
  103. package/dist/services/contracts/index.d.ts.map +1 -0
  104. package/dist/services/contracts/index.js +211 -0
  105. package/dist/services/contracts/index.js.map +1 -0
  106. package/dist/services/engagements/index.d.ts +504 -0
  107. package/dist/services/engagements/index.d.ts.map +1 -0
  108. package/dist/services/engagements/index.js +613 -0
  109. package/dist/services/engagements/index.js.map +1 -0
  110. package/dist/services/jobs/index.d.ts +490 -0
  111. package/dist/services/jobs/index.d.ts.map +1 -0
  112. package/dist/services/jobs/index.js +753 -0
  113. package/dist/services/jobs/index.js.map +1 -0
  114. package/dist/services/payments/index.d.ts +415 -0
  115. package/dist/services/payments/index.d.ts.map +1 -0
  116. package/dist/services/payments/index.js +636 -0
  117. package/dist/services/payments/index.js.map +1 -0
  118. package/dist/services/profile/__tests__/fixtures.d.ts +214 -0
  119. package/dist/services/profile/__tests__/fixtures.d.ts.map +1 -0
  120. package/dist/services/profile/__tests__/fixtures.js +176 -0
  121. package/dist/services/profile/__tests__/fixtures.js.map +1 -0
  122. package/dist/services/profile/basic/index.d.ts +390 -0
  123. package/dist/services/profile/basic/index.d.ts.map +1 -0
  124. package/dist/services/profile/basic/index.js +1007 -0
  125. package/dist/services/profile/basic/index.js.map +1 -0
  126. package/dist/services/profile/certifications/index.d.ts +74 -0
  127. package/dist/services/profile/certifications/index.d.ts.map +1 -0
  128. package/dist/services/profile/certifications/index.js +169 -0
  129. package/dist/services/profile/certifications/index.js.map +1 -0
  130. package/dist/services/profile/education/index.d.ts +73 -0
  131. package/dist/services/profile/education/index.d.ts.map +1 -0
  132. package/dist/services/profile/education/index.js +168 -0
  133. package/dist/services/profile/education/index.js.map +1 -0
  134. package/dist/services/profile/employment/index.d.ts +111 -0
  135. package/dist/services/profile/employment/index.d.ts.map +1 -0
  136. package/dist/services/profile/employment/index.js +202 -0
  137. package/dist/services/profile/employment/index.js.map +1 -0
  138. package/dist/services/profile/external/index.d.ts +219 -0
  139. package/dist/services/profile/external/index.d.ts.map +1 -0
  140. package/dist/services/profile/external/index.js +560 -0
  141. package/dist/services/profile/external/index.js.map +1 -0
  142. package/dist/services/profile/index.d.ts +24 -0
  143. package/dist/services/profile/index.d.ts.map +1 -0
  144. package/dist/services/profile/index.js +26 -0
  145. package/dist/services/profile/index.js.map +1 -0
  146. package/dist/services/profile/industries/index.d.ts +130 -0
  147. package/dist/services/profile/industries/index.d.ts.map +1 -0
  148. package/dist/services/profile/industries/index.js +292 -0
  149. package/dist/services/profile/industries/index.js.map +1 -0
  150. package/dist/services/profile/portfolio/index.d.ts +352 -0
  151. package/dist/services/profile/portfolio/index.d.ts.map +1 -0
  152. package/dist/services/profile/portfolio/index.js +833 -0
  153. package/dist/services/profile/portfolio/index.js.map +1 -0
  154. package/dist/services/profile/resume/index.d.ts +60 -0
  155. package/dist/services/profile/resume/index.d.ts.map +1 -0
  156. package/dist/services/profile/resume/index.js +212 -0
  157. package/dist/services/profile/resume/index.js.map +1 -0
  158. package/dist/services/profile/reviews/index.d.ts +137 -0
  159. package/dist/services/profile/reviews/index.d.ts.map +1 -0
  160. package/dist/services/profile/reviews/index.js +431 -0
  161. package/dist/services/profile/reviews/index.js.map +1 -0
  162. package/dist/services/profile/shared.d.ts +127 -0
  163. package/dist/services/profile/shared.d.ts.map +1 -0
  164. package/dist/services/profile/shared.js +155 -0
  165. package/dist/services/profile/shared.js.map +1 -0
  166. package/dist/services/profile/skills/index.d.ts +212 -0
  167. package/dist/services/profile/skills/index.d.ts.map +1 -0
  168. package/dist/services/profile/skills/index.js +461 -0
  169. package/dist/services/profile/skills/index.js.map +1 -0
  170. package/dist/services/profile/visas/index.d.ts +74 -0
  171. package/dist/services/profile/visas/index.d.ts.map +1 -0
  172. package/dist/services/profile/visas/index.js +306 -0
  173. package/dist/services/profile/visas/index.js.map +1 -0
  174. package/dist/services/timesheet/index.d.ts +326 -0
  175. package/dist/services/timesheet/index.d.ts.map +1 -0
  176. package/dist/services/timesheet/index.js +324 -0
  177. package/dist/services/timesheet/index.js.map +1 -0
  178. package/dist/services/translations.d.ts +79 -0
  179. package/dist/services/translations.d.ts.map +1 -0
  180. package/dist/services/translations.js +136 -0
  181. package/dist/services/translations.js.map +1 -0
  182. package/dist/transport-resilience.d.ts +136 -0
  183. package/dist/transport-resilience.d.ts.map +1 -0
  184. package/dist/transport-resilience.js +247 -0
  185. package/dist/transport-resilience.js.map +1 -0
  186. package/dist/transport.d.ts +408 -0
  187. package/dist/transport.d.ts.map +1 -0
  188. package/dist/transport.js +691 -0
  189. package/dist/transport.js.map +1 -0
  190. package/dist/types.d.ts +41 -0
  191. package/dist/types.d.ts.map +1 -0
  192. package/dist/types.js +18 -0
  193. package/dist/types.js.map +1 -0
  194. package/package.json +40 -12
  195. package/index.js +0 -7
@@ -0,0 +1,310 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ import { buildDryRunPreview } from "../../transport.js";
4
+ import { callGatewayShared } from "../_shared/transport.js";
5
+ export class AvailabilityError extends Error {
6
+ code;
7
+ name = "AvailabilityError";
8
+ constructor(code, message, options) {
9
+ super(message, options);
10
+ this.code = code;
11
+ }
12
+ }
13
+ // ---------------------------------------------------------------------
14
+ // GraphQL operation strings
15
+ //
16
+ // `GetAvailability` is a derived query selecting only the fields
17
+ // `workingHours.show()` / `allocatedHours.show()` / top-level `show()`
18
+ // need.
19
+ //
20
+ // `UpdateWorkingHours` is derived from the captured portal operation;
21
+ // the input shape was recovered from the portal bundle call-site (see
22
+ // module doc comment).
23
+ //
24
+ // `UpdateAllocatedHours` is used VERBATIM from
25
+ // `../research/graphql/gateway/operations/mobile/UpdateAllocatedHours.graphql`.
26
+ // ---------------------------------------------------------------------
27
+ const GET_AVAILABILITY_QUERY = `query GetAvailability {
28
+ viewer {
29
+ __typename
30
+ id
31
+ viewerRole {
32
+ __typename
33
+ allocatedHours
34
+ timeZone { __typename name value location utcOffset stdOffset }
35
+ workingTimeFrom
36
+ workingTimeTo
37
+ availableShiftRangeFrom
38
+ availableShiftRangeTo
39
+ profile { __typename id }
40
+ }
41
+ }
42
+ }`;
43
+ const UPDATE_WORKING_HOURS_MUTATION = `mutation UpdateWorkingHours($input: UpdateWorkingHoursInput!) {
44
+ updateWorkingHours(input: $input) {
45
+ __typename
46
+ success
47
+ notice
48
+ errors { __typename code key message }
49
+ profile {
50
+ __typename
51
+ id
52
+ timeZone { __typename name value location utcOffset stdOffset }
53
+ workingTimeFrom
54
+ workingTimeTo
55
+ availableShiftRangeFrom
56
+ availableShiftRangeTo
57
+ }
58
+ }
59
+ }`;
60
+ // Verbatim from `../research/graphql/gateway/operations/mobile/UpdateAllocatedHours.graphql`.
61
+ const UPDATE_ALLOCATED_HOURS_MUTATION = `mutation UpdateAllocatedHours($hours: Int!) { viewerRole { __typename update(input: { allocatedHours: $hours } ) { __typename notice ...mutationResultFields viewer { __typename id ...availabilityData } } } } fragment mutationResultFields on MutationResult { __typename errors { __typename key message code } success } fragment lastAllocatedHoursChangeRequestData on AllocatedHoursChangeRequest { __typename id allocatedHours rejectReason comment statusV2 { __typename value } futureAvailableHours returnInDate useReturnAvailability reviewedManually } fragment availabilityData on Viewer { __typename id preliminarySearchSetting { __typename enabled disablingReason comment } viewerRole { __typename allocatedHours hiredHours lastAllocatedHoursChangeRequest { __typename ...lastAllocatedHoursChangeRequestData } } }`;
62
+ /**
63
+ * Thin per-service wrapper around {@link callGatewayShared} (issue
64
+ * #329). Pins the mobile-gateway surface and the
65
+ * {@link AvailabilityError} domain class.
66
+ */
67
+ async function callGateway(token, operationName, query, variables, schema) {
68
+ return callGatewayShared("mobile-gateway", token, operationName, query, variables, AvailabilityError, { schema });
69
+ }
70
+ function formatMutationErrors(prefix, errors) {
71
+ if (errors == null || errors.length === 0) {
72
+ return `${prefix}: no error detail returned.`;
73
+ }
74
+ const parts = errors.map((e) => {
75
+ const fields = [];
76
+ if (e.code != null)
77
+ fields.push(`code=${e.code}`);
78
+ if (e.key != null)
79
+ fields.push(`key=${e.key}`);
80
+ const head = fields.length > 0 ? `[${fields.join(", ")}] ` : "";
81
+ return `${head}${e.message ?? "(no message)"}`;
82
+ });
83
+ return `${prefix}: ${parts.join("; ")}`;
84
+ }
85
+ /**
86
+ * Read the signed-in user's full availability snapshot — time zone,
87
+ * working-hours window, flexible shift range, and allocated hours.
88
+ *
89
+ * Throws `AvailabilityError("NO_VIEWER")` when the server returns a
90
+ * null viewer (defensive — auth-revoked surfaces via `AuthRevokedError`
91
+ * instead).
92
+ *
93
+ * Throws `AvailabilityError("NO_VIEWER_ROLE")` when the viewer exists
94
+ * but has no role assigned (no role = no working-hours / allocated-hours
95
+ * shape).
96
+ */
97
+ export async function show(token) {
98
+ const data = await callGateway(token, "GetAvailability", GET_AVAILABILITY_QUERY, {});
99
+ if (data.viewer === null) {
100
+ throw new AvailabilityError("NO_VIEWER", "Session is valid but no viewer is bound to it.");
101
+ }
102
+ if (data.viewer.viewerRole === null) {
103
+ throw new AvailabilityError("NO_VIEWER_ROLE", "Viewer has no role assigned — no availability data is available. (Are you signed in as a Toptal Talent?)");
104
+ }
105
+ const role = data.viewer.viewerRole;
106
+ return {
107
+ viewerId: data.viewer.id,
108
+ profileId: role.profile?.id ?? null,
109
+ timeZone: role.timeZone,
110
+ workingTimeFrom: role.workingTimeFrom,
111
+ workingTimeTo: role.workingTimeTo,
112
+ availableShiftRangeFrom: role.availableShiftRangeFrom,
113
+ availableShiftRangeTo: role.availableShiftRangeTo,
114
+ allocatedHours: role.allocatedHours,
115
+ };
116
+ }
117
+ /**
118
+ * Working-hours sub-namespace under the service. Mirrors
119
+ * `engagements.breaks` so the public surface stays
120
+ * `availability.workingHours.{show, set}` — matches the CLI verb path
121
+ * `availability working-hours {show, set}`.
122
+ */
123
+ export const workingHours = {
124
+ /**
125
+ * Read just the working-hours subset of the snapshot. Identical
126
+ * underlying query to `show()`; the projection here drops
127
+ * `allocatedHours` for callers that only care about the time-zone
128
+ * and working-hours fields.
129
+ */
130
+ async show(token) {
131
+ const snap = await show(token);
132
+ return {
133
+ viewerId: snap.viewerId,
134
+ timeZone: snap.timeZone,
135
+ workingTimeFrom: snap.workingTimeFrom,
136
+ workingTimeTo: snap.workingTimeTo,
137
+ availableShiftRangeFrom: snap.availableShiftRangeFrom,
138
+ availableShiftRangeTo: snap.availableShiftRangeTo,
139
+ };
140
+ },
141
+ /**
142
+ * Update working-hours fields. All input fields are optional — only
143
+ * the provided keys are sent in the mutation payload, supporting
144
+ * partial updates (per portal bundle's call-site behavior).
145
+ *
146
+ * Throws `AvailabilityError("MUTATION_ERROR")` when the gateway
147
+ * returns `success: false` (validation failures, malformed time
148
+ * strings, unknown time-zone identifiers).
149
+ *
150
+ * Returns the post-update working-hours shape wrapped in
151
+ * {@link WorkingHoursAppliedOutcome}.
152
+ *
153
+ * Dry-run path (issue #164): when invoked with `options.dryRun ===
154
+ * true`, builds a {@link DryRunPreview} of the mutation WITHOUT
155
+ * invoking the gateway transport — including the `show()` pre-fetch
156
+ * the apply path uses to resolve `profileId` (per the AC's "no
157
+ * GraphQL request is sent" requirement). The preview's
158
+ * `variables.input.profileId` is populated with the placeholder
159
+ * string `"<resolved at apply time>"`; the wire SHAPE (field names,
160
+ * operation, surface, redacted headers) is verbatim. Mirrors the
161
+ * skipped-prefetch pattern from `engagements.breaks.add` (issue #163).
162
+ */
163
+ async set(token, input, options = {}) {
164
+ const profileFields = {};
165
+ if (input.timeZone !== undefined)
166
+ profileFields["timeZone"] = input.timeZone;
167
+ if (input.workingTimeFrom !== undefined)
168
+ profileFields["workingTimeFrom"] = input.workingTimeFrom;
169
+ if (input.workingTimeTo !== undefined)
170
+ profileFields["workingTimeTo"] = input.workingTimeTo;
171
+ if (input.availableShiftRangeFrom !== undefined)
172
+ profileFields["availableShiftRangeFrom"] = input.availableShiftRangeFrom;
173
+ if (input.availableShiftRangeTo !== undefined)
174
+ profileFields["availableShiftRangeTo"] = input.availableShiftRangeTo;
175
+ if (Object.keys(profileFields).length === 0) {
176
+ throw new AvailabilityError("MUTATION_ERROR", "UpdateWorkingHours requires at least one field (timeZone, workingTimeFrom, workingTimeTo, availableShiftRangeFrom, availableShiftRangeTo).");
177
+ }
178
+ if (options.dryRun === true) {
179
+ // Skip the `show()` pre-fetch entirely — the AC mandates zero
180
+ // transport calls in dry-run mode. The placeholder string stands
181
+ // in for `profileId` so the preview's `variables.input` matches
182
+ // the wire shape; the actual id resolves at apply time. Same
183
+ // pattern as `engagements.breaks.add` (issue #163).
184
+ return {
185
+ kind: "preview",
186
+ preview: buildDryRunPreview({
187
+ surface: "mobile-gateway",
188
+ authToken: token,
189
+ body: {
190
+ operationName: "UpdateWorkingHours",
191
+ query: UPDATE_WORKING_HOURS_MUTATION,
192
+ variables: { input: { profileId: "<resolved at apply time>", profile: profileFields } },
193
+ },
194
+ }),
195
+ };
196
+ }
197
+ // The mutation's `UpdateWorkingHoursInput` requires `profileId: ID!` and
198
+ // `profile: WorkingHoursInput!` (verified live 2026-05-11 via wire probe
199
+ // against mobile-gateway — see `.tmp/probe-update-working-hours.mjs`).
200
+ // Fetch the profile id from the live snapshot so callers don't have to
201
+ // plumb it through.
202
+ const snap = await show(token);
203
+ if (snap.profileId === null) {
204
+ throw new AvailabilityError("NO_VIEWER_ROLE", "Viewer has a role but no bound profile id — cannot construct UpdateWorkingHours payload.");
205
+ }
206
+ const data = await callGateway(token, "UpdateWorkingHours", UPDATE_WORKING_HOURS_MUTATION, { input: { profileId: snap.profileId, profile: profileFields } });
207
+ if (data.updateWorkingHours === null) {
208
+ throw new AvailabilityError("UNKNOWN", "UpdateWorkingHours returned a null payload.");
209
+ }
210
+ const result = data.updateWorkingHours;
211
+ if (!result.success) {
212
+ throw new AvailabilityError("MUTATION_ERROR", formatMutationErrors("UpdateWorkingHours failed", result.errors));
213
+ }
214
+ if (result.profile === null) {
215
+ throw new AvailabilityError("UNKNOWN", "UpdateWorkingHours returned success but the `profile` payload was null.");
216
+ }
217
+ return {
218
+ kind: "applied",
219
+ result: {
220
+ timeZone: result.profile.timeZone,
221
+ workingTimeFrom: result.profile.workingTimeFrom,
222
+ workingTimeTo: result.profile.workingTimeTo,
223
+ availableShiftRangeFrom: result.profile.availableShiftRangeFrom,
224
+ availableShiftRangeTo: result.profile.availableShiftRangeTo,
225
+ notice: result.notice ?? null,
226
+ },
227
+ };
228
+ },
229
+ };
230
+ /**
231
+ * Allocated-hours sub-namespace. The wire mutation
232
+ * (`UpdateAllocatedHours`) operates on `viewerRole`, NOT on a specific
233
+ * engagement — `allocatedHours` is global across all of the viewer's
234
+ * active engagements. This is why the surface lives under
235
+ * `availability` rather than under `engagements` (per #147 scope
236
+ * amendment that absorbed it into #146).
237
+ */
238
+ export const allocatedHours = {
239
+ /**
240
+ * Read just the allocated-hours value. `hiredHours` is NOT
241
+ * surfaced here — it's only returned by `set()` (where the
242
+ * post-update payload includes it for context).
243
+ */
244
+ async show(token) {
245
+ const snap = await show(token);
246
+ if (snap.allocatedHours === null) {
247
+ throw new AvailabilityError("UNKNOWN", "Viewer role payload had no allocatedHours field.");
248
+ }
249
+ return { allocatedHours: snap.allocatedHours };
250
+ },
251
+ /**
252
+ * Set the viewer-scoped allocated-hours value. The platform UI caps
253
+ * this at 80 (`SetAvailability` validator); the server enforces the
254
+ * same range — pass an out-of-range value at your own risk (the
255
+ * mutation will return `success: false` with a validation error).
256
+ *
257
+ * Returns the post-update `{ allocatedHours, hiredHours, notice }`
258
+ * triple wrapped in {@link AllocatedHoursAppliedOutcome}.
259
+ *
260
+ * Dry-run path (issue #164): when invoked with `options.dryRun ===
261
+ * true`, builds a {@link DryRunPreview} of the mutation without
262
+ * invoking the gateway transport and returns it wrapped in
263
+ * {@link AvailabilityDryRunPreviewOutcome}. The integer-range
264
+ * validation runs BEFORE the dry-run short-circuit — invalid input
265
+ * still throws `AvailabilityError("MUTATION_ERROR")` rather than
266
+ * emitting a preview that would be rejected at apply time.
267
+ */
268
+ async set(token, hours, options = {}) {
269
+ if (!Number.isInteger(hours) || hours < 0) {
270
+ throw new AvailabilityError("MUTATION_ERROR", `UpdateAllocatedHours: hours must be a non-negative integer (got ${String(hours)}).`);
271
+ }
272
+ if (options.dryRun === true) {
273
+ return {
274
+ kind: "preview",
275
+ preview: buildDryRunPreview({
276
+ surface: "mobile-gateway",
277
+ authToken: token,
278
+ body: {
279
+ operationName: "UpdateAllocatedHours",
280
+ query: UPDATE_ALLOCATED_HOURS_MUTATION,
281
+ variables: { hours },
282
+ },
283
+ }),
284
+ };
285
+ }
286
+ const data = await callGateway(token, "UpdateAllocatedHours", UPDATE_ALLOCATED_HOURS_MUTATION, { hours });
287
+ if (data.viewerRole === null) {
288
+ throw new AvailabilityError("NO_VIEWER_ROLE", "Viewer has no role assigned.");
289
+ }
290
+ const result = data.viewerRole.update;
291
+ if (result === null) {
292
+ throw new AvailabilityError("UNKNOWN", "UpdateAllocatedHours returned a null payload.");
293
+ }
294
+ if (!result.success) {
295
+ throw new AvailabilityError("MUTATION_ERROR", formatMutationErrors("UpdateAllocatedHours failed", result.errors));
296
+ }
297
+ if (result.viewer === null || result.viewer.viewerRole === null) {
298
+ throw new AvailabilityError("UNKNOWN", "UpdateAllocatedHours returned success but the post-update viewer payload was null.");
299
+ }
300
+ return {
301
+ kind: "applied",
302
+ result: {
303
+ allocatedHours: result.viewer.viewerRole.allocatedHours,
304
+ hiredHours: result.viewer.viewerRole.hiredHours ?? null,
305
+ notice: result.notice ?? null,
306
+ },
307
+ };
308
+ },
309
+ };
310
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/services/availability/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Contracts-domain error codes. Mirrors the `EngagementsError` /
3
+ * `PaymentsError` shape.
4
+ *
5
+ * - `NO_TALENT`: HTTP 200 + `data.profile === null` or
6
+ * `data.profile.talent === null`. Defensive — the portal usually
7
+ * signals an auth-revoked failure differently. A null `talent` would
8
+ * indicate a non-talent profile (e.g. a client account) reached this
9
+ * surface, which TTCtl's auth model precludes.
10
+ * - `NOT_FOUND`: `show(id)` was passed a contract-id that doesn't
11
+ * appear in `profile.talent.contracts`. Two wire shapes fold here:
12
+ * an explicit list that doesn't contain the id, AND a top-level
13
+ * GraphQL `Record not found` error (defensive — the portal sometimes
14
+ * surfaces unknown-id failures this way for Relay `node(id:)`
15
+ * lookups, though `contracts` is a list rather than `node(id:)` so
16
+ * the data-shape sentinel is the primary path).
17
+ * - `GRAPHQL_ERROR`: top-level `errors[]` from the gateway, not an
18
+ * auth-revoked extension and not a `Record not found`.
19
+ * - `NETWORK_ERROR`, `UNKNOWN`: standard transport failure modes.
20
+ *
21
+ * Auth-revoked failures throw `AuthRevokedError` (cross-cutting
22
+ * `TtctlError` subclass per #77), not a code on this enum. `ProfileError`
23
+ * from `extractProfileId(token)` (the profileId-resolution round-trip)
24
+ * also propagates verbatim — a profile-read failure surfaces with the
25
+ * same actionable message as `ttctl profile show`.
26
+ */
27
+ export type ContractsErrorCode = "NO_TALENT" | "NO_VIEWER" | "NOT_FOUND" | "GRAPHQL_ERROR" | "NETWORK_ERROR" | "WIRE_SHAPE_ERROR" | "UNKNOWN";
28
+ export declare class ContractsError extends Error {
29
+ readonly code: ContractsErrorCode;
30
+ readonly name = "ContractsError";
31
+ constructor(code: ContractsErrorCode, message: string, options?: {
32
+ cause?: unknown;
33
+ });
34
+ }
35
+ /**
36
+ * A talent-level legal document — Toptal Direct, Master Service
37
+ * Agreement, etc. Projected from the `Contract` type at
38
+ * `../research/graphql/talent_profile/schema.graphql:163`. Every field
39
+ * is `Unknown`-typed in the synthesized SDL, so the runtime types here
40
+ * are best-effort INFERRED until the gated E2E test pins them.
41
+ *
42
+ * Semantics (best-effort, pending E2E confirmation):
43
+ *
44
+ * - `id`: Relay-style addressable id (string).
45
+ * - `kind`: contract kind enum value (e.g. `"TOPTAL_DIRECT"`,
46
+ * `"MASTER_SERVICE_AGREEMENT"`). Surfaced as `string` to remain
47
+ * forward-compatible with new enum values.
48
+ * - `provider`: contract provider/source (e.g. `"TOPTAL"`, a client
49
+ * name). String for forward-compat.
50
+ * - `status`: contract lifecycle status (e.g. `"SIGNED"`, `"PENDING"`,
51
+ * `"DRAFT"`). String for forward-compat.
52
+ * - `billingType`: billing arrangement (e.g. `"HOURLY"`, `"FIXED"`).
53
+ * Nullable in case some contract kinds (MSAs) carry no billing-type
54
+ * binding.
55
+ * - `signedAt`: ISO-8601 timestamp when the talent signed. Null when
56
+ * not yet signed.
57
+ * - `sentAt`: ISO-8601 timestamp when Toptal sent the document. Null
58
+ * when not yet sent.
59
+ * - `isActive`: boolean indicating active/binding state. Null shape
60
+ * allowed defensively.
61
+ * - `verificationDeadline`: ISO-8601 timestamp by which the talent
62
+ * must complete verification steps. Null when no deadline applies.
63
+ * - `title`: human-readable contract title. Null shape allowed
64
+ * defensively.
65
+ *
66
+ * Nullability is conservative — the SDL has `Unknown` placeholders
67
+ * which carry no nullability signal. The CLI / MCP rendering layer
68
+ * handles `null` defensively. Once the E2E run pins real types, tighten
69
+ * the nullability here if observation supports it.
70
+ */
71
+ export interface Contract {
72
+ id: string;
73
+ kind: string | null;
74
+ provider: string | null;
75
+ status: string | null;
76
+ billingType: string | null;
77
+ signedAt: string | null;
78
+ sentAt: string | null;
79
+ isActive: boolean | null;
80
+ verificationDeadline: string | null;
81
+ title: string | null;
82
+ }
83
+ /**
84
+ * List the talent's top-level legal contracts via
85
+ * `profile(id:).talent.contracts` on the portal surface.
86
+ *
87
+ * Two round-trips: first a mobile-gateway `profile.basic.show()` call
88
+ * via `extractProfileId(token)` to resolve the user's `profileId`, then
89
+ * the `GetContracts` query against `talent-profile/graphql` with the
90
+ * resolved id.
91
+ *
92
+ * Returns the contracts in server order (no client-side sort). An
93
+ * empty list is a legitimate return value (a talent who hasn't signed
94
+ * any contract yet); callers MUST handle the empty case explicitly.
95
+ *
96
+ * Throws (typed):
97
+ *
98
+ * - `AuthRevokedError` — session bearer is invalid or expired.
99
+ * - `Cf403Error` (from the transport) — Cloudflare blocked the
100
+ * request despite Chrome TLS impersonation; the CLI/MCP surfaces
101
+ * render the verbatim guidance to file an issue.
102
+ * - `ProfileError` — the `extractProfileId(token)` round-trip
103
+ * failed (e.g. the viewer payload had no `viewerRole.profileId`).
104
+ * Propagates verbatim so the user sees the same actionable message
105
+ * as `ttctl profile show`.
106
+ * - `ContractsError(NO_TALENT)` — `data.profile` or
107
+ * `data.profile.talent` is null (defensive; auth failures usually
108
+ * surface through `AuthRevokedError`).
109
+ * - `ContractsError(GRAPHQL_ERROR)` — top-level GraphQL error, not
110
+ * auth-revoked, not a `Record not found`.
111
+ * - `ContractsError(NETWORK_ERROR)` — transport failure (DNS, TLS,
112
+ * connection reset).
113
+ * - `ContractsError(UNKNOWN)` — non-2xx HTTP status or missing
114
+ * `data` field.
115
+ */
116
+ export declare function list(token: string): Promise<Contract[]>;
117
+ /**
118
+ * Show a single contract by id. The portal surface does not expose a
119
+ * `contract(id:)` lookup — `show()` fetches the full
120
+ * `profile(id:).talent.contracts` list and filters client-side. The
121
+ * round-trip cost is the same as `list()`; latency-conscious callers
122
+ * may prefer `list()` + their own filter when retrieving multiple
123
+ * contracts.
124
+ *
125
+ * Throws:
126
+ *
127
+ * - everything `list()` throws, plus
128
+ * - `ContractsError(NOT_FOUND)` — no contract with the supplied id
129
+ * was found in `profile.talent.contracts`.
130
+ */
131
+ export declare function show(token: string, id: string): Promise<Contract>;
132
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/contracts/index.ts"],"names":[],"mappings":"AAyEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,kBAAkB,GAC1B,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,eAAe,GACf,kBAAkB,GAClB,SAAS,CAAC;AAEd,qBAAa,cAAe,SAAQ,KAAK;aAGrB,IAAI,EAAE,kBAAkB;IAF1C,SAAkB,IAAI,oBAAoB;gBAExB,IAAI,EAAE,kBAAkB,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIhC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAoKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAqC7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAOvE"}
@@ -0,0 +1,211 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (C) 2026 Oleksii PELYKH
3
+ import { callGatewayShared } from "../_shared/transport.js";
4
+ import { extractProfileId } from "../profile/shared.js";
5
+ export class ContractsError extends Error {
6
+ code;
7
+ name = "ContractsError";
8
+ constructor(code, message, options) {
9
+ super(message, options);
10
+ this.code = code;
11
+ }
12
+ }
13
+ // ---------------------------------------------------------------------
14
+ // GraphQL operation (hand-authored)
15
+ // ---------------------------------------------------------------------
16
+ // Hand-authored — modeled on the captured
17
+ // `research/graphql/talent_profile/operations/getLegalSettingsData.graphql`,
18
+ // which is the canonical wire path the portal SPA uses to read the
19
+ // talent's contracts (`profile(id:).talent.contracts`). Two other
20
+ // candidates were rejected by the live API in pre-merge E2E validation:
21
+ //
22
+ // - `viewer.contracts` (the capture at
23
+ // `research/graphql/gateway/operations/portal/GetContracts.graphql`):
24
+ // `Field 'contracts' doesn't exist on type 'Viewer'`. The `Viewer`
25
+ // SDL declaration of `contracts: Contract` (line 671 of
26
+ // `talent_profile/schema.graphql`) is the schema synthesizer's
27
+ // hypothesis that was never verified by a capture.
28
+ // - `activation.talent.contracts` (from the captured
29
+ // `GetActivationLegalData` op): `Field 'activation' doesn't exist
30
+ // on type 'Query'`. The SDL declares `Query.activation: Unknown`
31
+ // but the live talent_profile endpoint rejects it — likely a
32
+ // non-activated-talent path that only works in specific account
33
+ // states.
34
+ //
35
+ // `profile(id: $profileId)` is the universally-accessible path. The
36
+ // `profileId` is resolved via `extractProfileId(token)` which does
37
+ // one mobile-gateway round-trip to `profile.basic.show()` to read
38
+ // `viewerRole.profileId`. The two-round-trip cost matches `payments`
39
+ // service patterns.
40
+ //
41
+ // Operation name kept as `GetContracts` so any future server-side
42
+ // allow-listing keyed on operation name continues to recognize it.
43
+ //
44
+ // Projection is the 10-field full shape from
45
+ // `talent_profile/fragments/Contract.graphql`. Every field is
46
+ // `Unknown`-typed in the synthesized SDL → INFERRED.
47
+ const GET_CONTRACTS_QUERY = `query GetContracts($profileId: ID!) {
48
+ profile(id: $profileId) {
49
+ __typename
50
+ id
51
+ talent {
52
+ __typename
53
+ id
54
+ contracts {
55
+ __typename
56
+ id
57
+ kind
58
+ provider
59
+ status
60
+ billingType
61
+ signedAt
62
+ sentAt
63
+ isActive
64
+ verificationDeadline
65
+ title
66
+ }
67
+ }
68
+ }
69
+ }`;
70
+ // Defensive remap: if the portal surface ever returns a `Record not
71
+ // found`-style top-level GraphQL error for an unknown contract id (the
72
+ // gateway convention for Relay `node(id:)` lookups), fold it into the
73
+ // typed `NOT_FOUND` code. The primary `show()` path filters
74
+ // client-side; this is belt-and-braces for any future server-side
75
+ // `viewer.contract(id:)` lookup.
76
+ const NOT_FOUND_MESSAGE_PATTERN = /Record not found/i;
77
+ // ---------------------------------------------------------------------
78
+ // Transport helper
79
+ // ---------------------------------------------------------------------
80
+ /**
81
+ * Thin per-service wrapper around {@link callGatewayShared} (issue
82
+ * #329). Pins the talent-profile surface and the {@link ContractsError}
83
+ * domain class.
84
+ *
85
+ * The `Record not found` → `NOT_FOUND` remap previously lived inside
86
+ * the local helper. It's now applied at the {@link list} call site via
87
+ * a `GRAPHQL_ERROR` → `NOT_FOUND` translation (matching the pattern
88
+ * used in `applications.show()`, `jobs.show()`, `timesheet.resolve()`).
89
+ * The remap is defensive — the primary `show()` path filters
90
+ * client-side, but the gateway might surface `Record not found` as a
91
+ * top-level GraphQL error if a future `viewer.contract(id:)` lookup
92
+ * ever lands.
93
+ */
94
+ async function callTalentProfile(token, operationName, query, variables, schema) {
95
+ return callGatewayShared("talent-profile", token, operationName, query, variables, ContractsError, { schema });
96
+ }
97
+ // ---------------------------------------------------------------------
98
+ // Projection
99
+ // ---------------------------------------------------------------------
100
+ function projectContract(wire) {
101
+ return {
102
+ id: wire.id,
103
+ kind: wire.kind,
104
+ provider: wire.provider,
105
+ status: wire.status,
106
+ billingType: wire.billingType,
107
+ signedAt: wire.signedAt,
108
+ sentAt: wire.sentAt,
109
+ isActive: wire.isActive,
110
+ verificationDeadline: wire.verificationDeadline,
111
+ title: wire.title,
112
+ };
113
+ }
114
+ // ---------------------------------------------------------------------
115
+ // Public API
116
+ // ---------------------------------------------------------------------
117
+ /**
118
+ * List the talent's top-level legal contracts via
119
+ * `profile(id:).talent.contracts` on the portal surface.
120
+ *
121
+ * Two round-trips: first a mobile-gateway `profile.basic.show()` call
122
+ * via `extractProfileId(token)` to resolve the user's `profileId`, then
123
+ * the `GetContracts` query against `talent-profile/graphql` with the
124
+ * resolved id.
125
+ *
126
+ * Returns the contracts in server order (no client-side sort). An
127
+ * empty list is a legitimate return value (a talent who hasn't signed
128
+ * any contract yet); callers MUST handle the empty case explicitly.
129
+ *
130
+ * Throws (typed):
131
+ *
132
+ * - `AuthRevokedError` — session bearer is invalid or expired.
133
+ * - `Cf403Error` (from the transport) — Cloudflare blocked the
134
+ * request despite Chrome TLS impersonation; the CLI/MCP surfaces
135
+ * render the verbatim guidance to file an issue.
136
+ * - `ProfileError` — the `extractProfileId(token)` round-trip
137
+ * failed (e.g. the viewer payload had no `viewerRole.profileId`).
138
+ * Propagates verbatim so the user sees the same actionable message
139
+ * as `ttctl profile show`.
140
+ * - `ContractsError(NO_TALENT)` — `data.profile` or
141
+ * `data.profile.talent` is null (defensive; auth failures usually
142
+ * surface through `AuthRevokedError`).
143
+ * - `ContractsError(GRAPHQL_ERROR)` — top-level GraphQL error, not
144
+ * auth-revoked, not a `Record not found`.
145
+ * - `ContractsError(NETWORK_ERROR)` — transport failure (DNS, TLS,
146
+ * connection reset).
147
+ * - `ContractsError(UNKNOWN)` — non-2xx HTTP status or missing
148
+ * `data` field.
149
+ */
150
+ export async function list(token) {
151
+ const profileId = await extractProfileId(token);
152
+ let data;
153
+ try {
154
+ data = await callTalentProfile(token, "GetContracts", GET_CONTRACTS_QUERY, {
155
+ profileId,
156
+ });
157
+ }
158
+ catch (err) {
159
+ // Defensive: if the portal surface ever switches to returning
160
+ // `Record not found` as a top-level GraphQL error for the
161
+ // contracts list (e.g. through a `viewer.contract(id:)` lookup
162
+ // path), surface it as the typed `NOT_FOUND` code instead of the
163
+ // generic `GRAPHQL_ERROR`. Matches the pattern used in
164
+ // `applications.show()` / `jobs.show()` / `timesheet.resolve()`.
165
+ //
166
+ // The wire-level GraphQL message is lifted off `err.cause` (set
167
+ // by `callGatewayShared` to the offending `GraphQLErrorEntry`) so
168
+ // the user-facing message stays "Contract not found: <wire-reason>"
169
+ // — without the `${operationName} failed:` prefix that lives in
170
+ // `err.message`.
171
+ if (err instanceof ContractsError && err.code === "GRAPHQL_ERROR" && NOT_FOUND_MESSAGE_PATTERN.test(err.message)) {
172
+ const wireMessage = err.cause?.message ?? "Record not found";
173
+ throw new ContractsError("NOT_FOUND", `Contract not found: ${wireMessage}`, { cause: err });
174
+ }
175
+ throw err;
176
+ }
177
+ if (data === null || data === undefined) {
178
+ throw new ContractsError("UNKNOWN", "GetContracts returned no `data` payload.");
179
+ }
180
+ if (data.profile === null) {
181
+ throw new ContractsError("NO_TALENT", "GetContracts response had `profile: null`.");
182
+ }
183
+ if (data.profile.talent === null) {
184
+ throw new ContractsError("NO_TALENT", "GetContracts response had `profile.talent: null`.");
185
+ }
186
+ const wire = data.profile.talent.contracts ?? [];
187
+ return wire.map(projectContract);
188
+ }
189
+ /**
190
+ * Show a single contract by id. The portal surface does not expose a
191
+ * `contract(id:)` lookup — `show()` fetches the full
192
+ * `profile(id:).talent.contracts` list and filters client-side. The
193
+ * round-trip cost is the same as `list()`; latency-conscious callers
194
+ * may prefer `list()` + their own filter when retrieving multiple
195
+ * contracts.
196
+ *
197
+ * Throws:
198
+ *
199
+ * - everything `list()` throws, plus
200
+ * - `ContractsError(NOT_FOUND)` — no contract with the supplied id
201
+ * was found in `profile.talent.contracts`.
202
+ */
203
+ export async function show(token, id) {
204
+ const items = await list(token);
205
+ const match = items.find((c) => c.id === id);
206
+ if (match === undefined) {
207
+ throw new ContractsError("NOT_FOUND", `No contract found with id "${id}" (or you don't have access to it).`);
208
+ }
209
+ return match;
210
+ }
211
+ //# sourceMappingURL=index.js.map