@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.
- package/README.md +49 -9
- package/dist/__generated__/gateway.d.ts +4546 -0
- package/dist/__generated__/gateway.d.ts.map +1 -0
- package/dist/__generated__/gateway.js +9 -0
- package/dist/__generated__/gateway.js.map +1 -0
- package/dist/__generated__/talent-profile-zod-schemas.d.ts +1187 -0
- package/dist/__generated__/talent-profile-zod-schemas.d.ts.map +1 -0
- package/dist/__generated__/talent-profile-zod-schemas.js +1136 -0
- package/dist/__generated__/talent-profile-zod-schemas.js.map +1 -0
- package/dist/__generated__/talent-profile.d.ts +1397 -0
- package/dist/__generated__/talent-profile.d.ts.map +1 -0
- package/dist/__generated__/talent-profile.js +9 -0
- package/dist/__generated__/talent-profile.js.map +1 -0
- package/dist/__generated__/zod-schemas.d.ts +2895 -0
- package/dist/__generated__/zod-schemas.d.ts.map +1 -0
- package/dist/__generated__/zod-schemas.js +3121 -0
- package/dist/__generated__/zod-schemas.js.map +1 -0
- package/dist/__tests__/fixtures/profile/builders.d.ts +74 -0
- package/dist/__tests__/fixtures/profile/builders.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/builders.js +196 -0
- package/dist/__tests__/fixtures/profile/builders.js.map +1 -0
- package/dist/__tests__/fixtures/profile/data.d.ts +39 -0
- package/dist/__tests__/fixtures/profile/data.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/data.js +230 -0
- package/dist/__tests__/fixtures/profile/data.js.map +1 -0
- package/dist/__tests__/fixtures/profile/index.d.ts +9 -0
- package/dist/__tests__/fixtures/profile/index.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/index.js +10 -0
- package/dist/__tests__/fixtures/profile/index.js.map +1 -0
- package/dist/__tests__/fixtures/profile/types.d.ts +53 -0
- package/dist/__tests__/fixtures/profile/types.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/types.js +4 -0
- package/dist/__tests__/fixtures/profile/types.js.map +1 -0
- package/dist/auth/errors.d.ts +82 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +68 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth.d.ts +192 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +294 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +212 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +349 -0
- package/dist/config.js.map +1 -0
- package/dist/configLock.d.ts +50 -0
- package/dist/configLock.d.ts.map +1 -0
- package/dist/configLock.js +88 -0
- package/dist/configLock.js.map +1 -0
- package/dist/configWriter.d.ts +97 -0
- package/dist/configWriter.d.ts.map +1 -0
- package/dist/configWriter.js +687 -0
- package/dist/configWriter.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-switch.d.ts +161 -0
- package/dist/kill-switch.d.ts.map +1 -0
- package/dist/kill-switch.js +235 -0
- package/dist/kill-switch.js.map +1 -0
- package/dist/lib/date.d.ts +58 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +104 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/lib/diagnostic-log.d.ts +159 -0
- package/dist/lib/diagnostic-log.d.ts.map +1 -0
- package/dist/lib/diagnostic-log.js +186 -0
- package/dist/lib/diagnostic-log.js.map +1 -0
- package/dist/lib/package-version.d.ts +19 -0
- package/dist/lib/package-version.d.ts.map +1 -0
- package/dist/lib/package-version.js +38 -0
- package/dist/lib/package-version.js.map +1 -0
- package/dist/lib/redact.d.ts +153 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +207 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/text.d.ts +14 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/dist/lib/text.js +21 -0
- package/dist/lib/text.js.map +1 -0
- package/dist/lib/wire-shape.d.ts +131 -0
- package/dist/lib/wire-shape.d.ts.map +1 -0
- package/dist/lib/wire-shape.js +376 -0
- package/dist/lib/wire-shape.js.map +1 -0
- package/dist/onepassword.d.ts +29 -0
- package/dist/onepassword.d.ts.map +1 -0
- package/dist/onepassword.js +112 -0
- package/dist/onepassword.js.map +1 -0
- package/dist/services/_shared/transport.d.ts +148 -0
- package/dist/services/_shared/transport.d.ts.map +1 -0
- package/dist/services/_shared/transport.js +102 -0
- package/dist/services/_shared/transport.js.map +1 -0
- package/dist/services/applications/index.d.ts +210 -0
- package/dist/services/applications/index.d.ts.map +1 -0
- package/dist/services/applications/index.js +240 -0
- package/dist/services/applications/index.js.map +1 -0
- package/dist/services/availability/index.d.ts +254 -0
- package/dist/services/availability/index.d.ts.map +1 -0
- package/dist/services/availability/index.js +310 -0
- package/dist/services/availability/index.js.map +1 -0
- package/dist/services/contracts/index.d.ts +132 -0
- package/dist/services/contracts/index.d.ts.map +1 -0
- package/dist/services/contracts/index.js +211 -0
- package/dist/services/contracts/index.js.map +1 -0
- package/dist/services/engagements/index.d.ts +504 -0
- package/dist/services/engagements/index.d.ts.map +1 -0
- package/dist/services/engagements/index.js +613 -0
- package/dist/services/engagements/index.js.map +1 -0
- package/dist/services/jobs/index.d.ts +490 -0
- package/dist/services/jobs/index.d.ts.map +1 -0
- package/dist/services/jobs/index.js +753 -0
- package/dist/services/jobs/index.js.map +1 -0
- package/dist/services/payments/index.d.ts +415 -0
- package/dist/services/payments/index.d.ts.map +1 -0
- package/dist/services/payments/index.js +636 -0
- package/dist/services/payments/index.js.map +1 -0
- package/dist/services/profile/__tests__/fixtures.d.ts +214 -0
- package/dist/services/profile/__tests__/fixtures.d.ts.map +1 -0
- package/dist/services/profile/__tests__/fixtures.js +176 -0
- package/dist/services/profile/__tests__/fixtures.js.map +1 -0
- package/dist/services/profile/basic/index.d.ts +390 -0
- package/dist/services/profile/basic/index.d.ts.map +1 -0
- package/dist/services/profile/basic/index.js +1007 -0
- package/dist/services/profile/basic/index.js.map +1 -0
- package/dist/services/profile/certifications/index.d.ts +74 -0
- package/dist/services/profile/certifications/index.d.ts.map +1 -0
- package/dist/services/profile/certifications/index.js +169 -0
- package/dist/services/profile/certifications/index.js.map +1 -0
- package/dist/services/profile/education/index.d.ts +73 -0
- package/dist/services/profile/education/index.d.ts.map +1 -0
- package/dist/services/profile/education/index.js +168 -0
- package/dist/services/profile/education/index.js.map +1 -0
- package/dist/services/profile/employment/index.d.ts +111 -0
- package/dist/services/profile/employment/index.d.ts.map +1 -0
- package/dist/services/profile/employment/index.js +202 -0
- package/dist/services/profile/employment/index.js.map +1 -0
- package/dist/services/profile/external/index.d.ts +219 -0
- package/dist/services/profile/external/index.d.ts.map +1 -0
- package/dist/services/profile/external/index.js +560 -0
- package/dist/services/profile/external/index.js.map +1 -0
- package/dist/services/profile/index.d.ts +24 -0
- package/dist/services/profile/index.d.ts.map +1 -0
- package/dist/services/profile/index.js +26 -0
- package/dist/services/profile/index.js.map +1 -0
- package/dist/services/profile/industries/index.d.ts +130 -0
- package/dist/services/profile/industries/index.d.ts.map +1 -0
- package/dist/services/profile/industries/index.js +292 -0
- package/dist/services/profile/industries/index.js.map +1 -0
- package/dist/services/profile/portfolio/index.d.ts +352 -0
- package/dist/services/profile/portfolio/index.d.ts.map +1 -0
- package/dist/services/profile/portfolio/index.js +833 -0
- package/dist/services/profile/portfolio/index.js.map +1 -0
- package/dist/services/profile/resume/index.d.ts +60 -0
- package/dist/services/profile/resume/index.d.ts.map +1 -0
- package/dist/services/profile/resume/index.js +212 -0
- package/dist/services/profile/resume/index.js.map +1 -0
- package/dist/services/profile/reviews/index.d.ts +137 -0
- package/dist/services/profile/reviews/index.d.ts.map +1 -0
- package/dist/services/profile/reviews/index.js +431 -0
- package/dist/services/profile/reviews/index.js.map +1 -0
- package/dist/services/profile/shared.d.ts +127 -0
- package/dist/services/profile/shared.d.ts.map +1 -0
- package/dist/services/profile/shared.js +155 -0
- package/dist/services/profile/shared.js.map +1 -0
- package/dist/services/profile/skills/index.d.ts +212 -0
- package/dist/services/profile/skills/index.d.ts.map +1 -0
- package/dist/services/profile/skills/index.js +461 -0
- package/dist/services/profile/skills/index.js.map +1 -0
- package/dist/services/profile/visas/index.d.ts +74 -0
- package/dist/services/profile/visas/index.d.ts.map +1 -0
- package/dist/services/profile/visas/index.js +306 -0
- package/dist/services/profile/visas/index.js.map +1 -0
- package/dist/services/timesheet/index.d.ts +326 -0
- package/dist/services/timesheet/index.d.ts.map +1 -0
- package/dist/services/timesheet/index.js +324 -0
- package/dist/services/timesheet/index.js.map +1 -0
- package/dist/services/translations.d.ts +79 -0
- package/dist/services/translations.d.ts.map +1 -0
- package/dist/services/translations.js +136 -0
- package/dist/services/translations.js.map +1 -0
- package/dist/transport-resilience.d.ts +136 -0
- package/dist/transport-resilience.d.ts.map +1 -0
- package/dist/transport-resilience.js +247 -0
- package/dist/transport-resilience.js.map +1 -0
- package/dist/transport.d.ts +408 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +691 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -12
- 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
|